begin working on httpx

This commit is contained in:
soypat
2025-05-22 23:24:22 -03:00
parent e6496d2b07
commit 876f10f9d8
3 changed files with 1660 additions and 0 deletions
+270
View File
@@ -0,0 +1,270 @@
package httpx
const (
slashChar = '/'
rChar = '\r'
nChar = '\n'
defaultServerName = "fasthttp"
defaultUserAgent = "fasthttp"
defaultContentType = "text/plain; charset=utf-8"
)
const (
strSlashSlash = "//"
strSlashDotDot = "/.."
strSlashDotSlash = "/./"
strSlashDotDotSlash = "/../"
strBackSlashDotDot = `\..`
strBackSlashDotBackSlash = `\.\`
strSlashDotDotBackSlash = `/..\`
strBackSlashDotDotBackSlash = `\..\`
strCRLF = "\r\n"
strHTTP = "http"
strHTTPS = "https"
strHTTP10 = "HTTP/1.0"
strHTTP11 = "HTTP/1.1"
strColon = ":"
strColonSlashSlash = "://"
strColonSpace = ": "
strCommaSpace = ", "
strGMT = "GMT"
strResponseContinue = "HTTP/1.1 100 Continue\r\n\r\n"
strExpect = HeaderExpect
strConnection = HeaderConnection
strContentLength = HeaderContentLength
strContentType = HeaderContentType
strDate = HeaderDate
strHost = HeaderHost
strReferer = HeaderReferer
strServer = HeaderServer
strTransferEncoding = HeaderTransferEncoding
strContentEncoding = HeaderContentEncoding
strAcceptEncoding = HeaderAcceptEncoding
strUserAgent = HeaderUserAgent
strCookie = HeaderCookie
strSetCookie = HeaderSetCookie
strLocation = HeaderLocation
strIfModifiedSince = HeaderIfModifiedSince
strLastModified = HeaderLastModified
strAcceptRanges = HeaderAcceptRanges
strRange = HeaderRange
strContentRange = HeaderContentRange
strAuthorization = HeaderAuthorization
strTE = HeaderTE
strTrailer = HeaderTrailer
strMaxForwards = HeaderMaxForwards
strProxyConnection = HeaderProxyConnection
strProxyAuthenticate = HeaderProxyAuthenticate
strProxyAuthorization = HeaderProxyAuthorization
strWWWAuthenticate = HeaderWWWAuthenticate
strVary = HeaderVary
strCookieExpires = "expires"
strCookieDomain = "domain"
strCookiePath = "path"
strCookieHTTPOnly = "HttpOnly"
strCookieSecure = "secure"
strCookieMaxAge = "max-age"
strCookieSameSite = "SameSite"
strCookieSameSiteLax = "Lax"
strCookieSameSiteStrict = "Strict"
strCookieSameSiteNone = "None"
strClose = "close"
strGzip = "gzip"
strBr = "br"
strDeflate = "deflate"
strKeepAlive = "keep-alive"
strUpgrade = "Upgrade"
strChunked = "chunked"
strIdentity = "identity"
str100Continue = "100-continue"
strPostArgsContentType = "application/x-www-form-urlencoded"
strDefaultContentType = "application/octet-stream"
strMultipartFormData = "multipart/form-data"
strBoundary = "boundary"
strBytes = "bytes"
strBasicSpace = "Basic "
strApplicationSlash = "application/"
strImageSVG = "image/svg"
strImageIcon = "image/x-icon"
strFontSlash = "font/"
strMultipartSlash = "multipart/"
strTextSlash = "text/"
)
// Headers.
const (
// Authentication.
HeaderAuthorization = "Authorization"
HeaderProxyAuthenticate = "Proxy-Authenticate"
HeaderProxyAuthorization = "Proxy-Authorization"
HeaderWWWAuthenticate = "WWW-Authenticate"
// Caching.
HeaderAge = "Age"
HeaderCacheControl = "Cache-Control"
HeaderClearSiteData = "Clear-Site-Data"
HeaderExpires = "Expires"
HeaderPragma = "Pragma"
HeaderWarning = "Warning"
// Client hints.
HeaderAcceptCH = "Accept-CH"
HeaderAcceptCHLifetime = "Accept-CH-Lifetime"
HeaderContentDPR = "Content-DPR"
HeaderDPR = "DPR"
HeaderEarlyData = "Early-Data"
HeaderSaveData = "Save-Data"
HeaderViewportWidth = "Viewport-Width"
HeaderWidth = "Width"
// Conditionals.
HeaderETag = "ETag"
HeaderIfMatch = "If-Match"
HeaderIfModifiedSince = "If-Modified-Since"
HeaderIfNoneMatch = "If-None-Match"
HeaderIfUnmodifiedSince = "If-Unmodified-Since"
HeaderLastModified = "Last-Modified"
HeaderVary = "Vary"
// Connection management.
HeaderConnection = "Connection"
HeaderKeepAlive = "Keep-Alive"
HeaderProxyConnection = "Proxy-Connection"
// Content negotiation.
HeaderAccept = "Accept"
HeaderAcceptCharset = "Accept-Charset"
HeaderAcceptEncoding = "Accept-Encoding"
HeaderAcceptLanguage = "Accept-Language"
// Controls.
HeaderCookie = "Cookie"
HeaderExpect = "Expect"
HeaderMaxForwards = "Max-Forwards"
HeaderSetCookie = "Set-Cookie"
// CORS.
HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials"
HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers"
HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods"
HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin"
HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers"
HeaderAccessControlMaxAge = "Access-Control-Max-Age"
HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers"
HeaderAccessControlRequestMethod = "Access-Control-Request-Method"
HeaderOrigin = "Origin"
HeaderTimingAllowOrigin = "Timing-Allow-Origin"
HeaderXPermittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies"
// Do Not Track.
HeaderDNT = "DNT"
HeaderTk = "Tk"
// Downloads.
HeaderContentDisposition = "Content-Disposition"
// Message body information.
HeaderContentEncoding = "Content-Encoding"
HeaderContentLanguage = "Content-Language"
HeaderContentLength = "Content-Length"
HeaderContentLocation = "Content-Location"
HeaderContentType = "Content-Type"
// Proxies.
HeaderForwarded = "Forwarded"
HeaderVia = "Via"
HeaderXForwardedFor = "X-Forwarded-For"
HeaderXForwardedHost = "X-Forwarded-Host"
HeaderXForwardedProto = "X-Forwarded-Proto"
// Redirects.
HeaderLocation = "Location"
// Request context.
HeaderFrom = "From"
HeaderHost = "Host"
HeaderReferer = "Referer"
HeaderReferrerPolicy = "Referrer-Policy"
HeaderUserAgent = "User-Agent"
// Response context.
HeaderAllow = "Allow"
HeaderServer = "Server"
// Range requests.
HeaderAcceptRanges = "Accept-Ranges"
HeaderContentRange = "Content-Range"
HeaderIfRange = "If-Range"
HeaderRange = "Range"
// Security.
HeaderContentSecurityPolicy = "Content-Security-Policy"
HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only"
HeaderCrossOriginResourcePolicy = "Cross-Origin-Resource-Policy"
HeaderExpectCT = "Expect-CT"
HeaderFeaturePolicy = "Feature-Policy"
HeaderPublicKeyPins = "Public-Key-Pins"
HeaderPublicKeyPinsReportOnly = "Public-Key-Pins-Report-Only"
HeaderStrictTransportSecurity = "Strict-Transport-Security"
HeaderUpgradeInsecureRequests = "Upgrade-Insecure-Requests"
HeaderXContentTypeOptions = "X-Content-Type-Options"
HeaderXDownloadOptions = "X-Download-Options"
HeaderXFrameOptions = "X-Frame-Options"
HeaderXPoweredBy = "X-Powered-By"
HeaderXXSSProtection = "X-XSS-Protection"
// Server-sent event.
HeaderLastEventID = "Last-Event-ID"
HeaderNEL = "NEL"
HeaderPingFrom = "Ping-From"
HeaderPingTo = "Ping-To"
HeaderReportTo = "Report-To"
// Transfer coding.
HeaderTE = "TE"
HeaderTrailer = "Trailer"
HeaderTransferEncoding = "Transfer-Encoding"
// WebSockets.
HeaderSecWebSocketAccept = "Sec-WebSocket-Accept"
HeaderSecWebSocketExtensions = "Sec-WebSocket-Extensions" /* #nosec G101 */
HeaderSecWebSocketKey = "Sec-WebSocket-Key"
HeaderSecWebSocketProtocol = "Sec-WebSocket-Protocol"
HeaderSecWebSocketVersion = "Sec-WebSocket-Version"
// Other.
HeaderAcceptPatch = "Accept-Patch"
HeaderAcceptPushPolicy = "Accept-Push-Policy"
HeaderAcceptSignature = "Accept-Signature"
HeaderAltSvc = "Alt-Svc"
HeaderDate = "Date"
HeaderIndex = "Index"
HeaderLargeAllocation = "Large-Allocation"
HeaderLink = "Link"
HeaderPushPolicy = "Push-Policy"
HeaderRetryAfter = "Retry-After"
HeaderServerTiming = "Server-Timing"
HeaderSignature = "Signature"
HeaderSignedHeaders = "Signed-Headers"
HeaderSourceMap = "SourceMap"
HeaderUpgrade = "Upgrade"
HeaderXDNSPrefetchControl = "X-DNS-Prefetch-Control"
HeaderXPingback = "X-Pingback"
HeaderXRequestedWith = "X-Requested-With"
HeaderXRobotsTag = "X-Robots-Tag"
HeaderXUACompatible = "X-UA-Compatible"
)
// Probably replace these with short functions to take up less program memory
const (
hex2intTable = "\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x00\x01\x02\x03\x04\x05\x06\a\b\t\x10\x10\x10\x10\x10\x10\x10\n\v\f\r\x0e\x0f\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\n\v\f\r\x0e\x0f\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10"
toLowerTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\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"
toUpperTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\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"
quotedArgShouldEscapeTable = "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\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\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\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\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"
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"
)
+768
View File
@@ -0,0 +1,768 @@
package httpx
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strings"
"unsafe"
)
var (
errNeedMore = errors.New("need more data: cannot find trailing lf")
errInvalidName = errors.New("invalid header name")
errSmallBuffer = errors.New("small read buffer. Increase ReadBufferSize")
errNonNumericChars = errors.New("non-numeric chars found")
)
type headerScanner struct {
b []byte
key []byte
value []byte
err error
// hLen stores header subslice len
hLen int
disableNormalizing bool
// by checking whether the next line contains a colon or not to tell
// it's a header entry or a multi line value of current header entry.
// the side effect of this operation is that we know the index of the
// next colon and new line, so this can be used during next iteration,
// instead of find them again.
nextColon int
nextNewLine int
initialized bool
}
type headerValueScanner struct {
b string
value string
}
func (h *header) parse(buf []byte) (int, error) {
m, err := h.parseFirstLine(buf)
if err != nil {
return 0, err
}
h.rawHeaders, _, err = readRawHeaders(h.rawHeaders[:0], b2s(buf[m:]))
if err != nil {
return 0, err
}
var n int
n, err = h.parseHeaders(buf[m:])
if err != nil {
return 0, err
}
return m + n, nil
}
func (hb *headerBuf) offBuf() []byte {
return hb.buf[hb.off:]
}
func (hb *headerBuf) scanLine() []byte {
buf := hb.scanUntilByte('\n')
if len(buf) > 0 && buf[len(buf)-1] == '\r' {
buf = buf[:len(buf)-1] // exclude carriage return.
}
if hb.off < len(hb.buf) {
hb.off++ // consume newline.
}
return buf
}
func (hb *headerBuf) scanUntilByte(c byte) []byte {
buf := hb.offBuf()
idx := bytes.IndexByte(buf, c)
if idx >= 0 {
buf = buf[:idx]
}
hb.off += len(buf)
return buf
}
func (hb *headerBuf) parseFirstLine() (method, uri, proto headerSlice, flags flags, err error) {
var b []byte
for len(b) == 0 {
b = hb.scanLine()
}
if len(b) < 5 {
return method, uri, proto, 0, errors.New("too short first HTTP line")
}
methodEnd := max(0, bytes.IndexByte(b, ' '))
reqURIEnd := bytes.IndexByte(b[methodEnd+1:], ' ')
switch {
case reqURIEnd < 0:
flags |= noHTTP11
reqURIEnd = methodEnd + 1
case reqURIEnd == 0:
return method, uri, proto, flags, errors.New("empty URI")
case b2s(b[reqURIEnd+1:]) != strHTTP11:
flags |= noHTTP11
fallthrough
default:
proto = hb.slice(b[reqURIEnd+1:])
}
uri = hb.slice(b[methodEnd+1 : reqURIEnd])
method = hb.slice(b[:methodEnd])
return method, uri, proto, flags, nil
}
func readRawHeaders(dst []byte, buf string) ([]byte, int, error) {
n := strings.IndexByte(buf, nChar)
if n < 0 {
return dst[:0], 0, errNeedMore
}
if (n == 1 && buf[0] == rChar) || n == 0 {
// empty headers
return dst, n + 1, nil
}
n++
b := buf
m := n
for {
b = b[m:]
m = strings.IndexByte(b, nChar)
if m < 0 {
return dst, 0, errNeedMore
}
m++
n += m
if (m == 2 && b[0] == rChar) || m == 1 {
dst = append(dst, buf[:n]...)
return dst, n, nil
}
}
}
func (h *header) parseHeaders(buf []byte) (int, error) {
h.contentLength = -2
h.scanner = headerScanner{}
s := &h.scanner
s.b = buf
s.disableNormalizing = h.disableNormalizing
var err error
for s.next() {
key := b2s(s.key)
value := b2s(s.value)
if len(key) > 0 {
// Spaces between the header key and colon are not allowed.
// See RFC 7230, Section 3.2.4.
if strings.IndexByte(key, ' ') != -1 || strings.IndexByte(key, '\t') != -1 {
err = fmt.Errorf("invalid header key %q", s.key)
continue
}
if h.disableSpecialHeader {
h.h = appendArg(h.h, key, value, argsHasValue)
continue
}
switch s.key[0] | 0x20 {
case 'h':
if caseInsensitiveCompare(key, strHost) {
h.host = append(h.host[:0], value...)
continue
}
case 'u':
if caseInsensitiveCompare(key, strUserAgent) {
h.userAgent = append(h.userAgent[:0], value...)
continue
}
case 'c':
if caseInsensitiveCompare(key, strContentType) {
h.contentType = append(h.contentType[:0], value...)
continue
}
if caseInsensitiveCompare(key, strContentLength) {
if h.contentLength != -1 {
var nerr error
if h.contentLength, nerr = parseContentLength(b2s(s.value)); nerr != nil {
if err == nil {
err = nerr
}
h.contentLength = -2
} else {
h.contentLengthBytes = append(h.contentLengthBytes[:0], value...)
}
}
continue
}
if caseInsensitiveCompare(key, strConnection) {
if b2s(s.value) == strClose {
h.connectionClose = true
} else {
h.connectionClose = false
h.h = appendArg(h.h, key, value, argsHasValue)
}
continue
}
case 't':
if caseInsensitiveCompare(key, strTransferEncoding) {
if value != strIdentity {
h.contentLength = -1
h.h = setArg(h.h, strTransferEncoding, strChunked, argsHasValue)
}
continue
}
if caseInsensitiveCompare(key, strTrailer) {
if nerr := h.SetTrailer(value); nerr != nil {
if err == nil {
err = nerr
}
}
continue
}
}
}
h.h = appendArg(h.h, key, value, argsHasValue)
}
if s.err != nil && err == nil {
err = s.err
}
if err != nil {
h.connectionClose = true
return 0, err
}
if h.contentLength < 0 {
h.contentLengthBytes = h.contentLengthBytes[:0]
}
if h.noHTTP11 && !h.connectionClose {
// close connection for non-http/1.1 request unless 'Connection: keep-alive' is set.
v := peekArgStr(h.h, strConnection)
h.connectionClose = !hasHeaderValue(b2s(v), strKeepAlive)
}
return s.hLen, nil
}
func (s *headerScanner) next() bool {
if !s.initialized {
s.nextColon = -1
s.nextNewLine = -1
s.initialized = true
}
bLen := len(s.b)
if bLen >= 2 && s.b[0] == rChar && s.b[1] == nChar {
s.b = s.b[2:]
s.hLen += 2
return false
}
if bLen >= 1 && s.b[0] == nChar {
s.b = s.b[1:]
s.hLen++
return false
}
var n int
if s.nextColon >= 0 {
n = s.nextColon
s.nextColon = -1
} else {
n = bytes.IndexByte(s.b, ':')
// There can't be a \n inside the header name, check for this.
x := bytes.IndexByte(s.b, nChar)
if x < 0 {
// A header name should always at some point be followed by a \n
// even if it's the one that terminates the header block.
s.err = errNeedMore
return false
}
if x < n {
// There was a \n before the :
s.err = errInvalidName
return false
}
}
if n < 0 {
s.err = errNeedMore
return false
}
s.key = s.b[:n]
normalizeHeaderKey(s.key, s.disableNormalizing)
n++
for len(s.b) > n && s.b[n] == ' ' {
n++
// the newline index is a relative index, and lines below trimmed `s.b` by `n`,
// so the relative newline index also shifted forward. it's safe to decrease
// to a minus value, it means it's invalid, and will find the newline again.
s.nextNewLine--
}
s.hLen += n
s.b = s.b[n:]
if s.nextNewLine >= 0 {
n = s.nextNewLine
s.nextNewLine = -1
} else {
n = bytes.IndexByte(s.b, nChar)
}
if n < 0 {
s.err = errNeedMore
return false
}
isMultiLineValue := false
for {
if n+1 >= len(s.b) {
break
}
if s.b[n+1] != ' ' && s.b[n+1] != '\t' {
break
}
d := bytes.IndexByte(s.b[n+1:], nChar)
if d <= 0 {
break
} else if d == 1 && s.b[n+1] == rChar {
break
}
e := n + d + 1
if c := bytes.IndexByte(s.b[n+1:e], ':'); c >= 0 {
s.nextColon = c
s.nextNewLine = d - c - 1
break
}
isMultiLineValue = true
n = e
}
if n >= len(s.b) {
s.err = errNeedMore
return false
}
oldB := s.b
s.value = s.b[:n]
s.hLen += n + 1
s.b = s.b[n+1:]
if n > 0 && s.value[n-1] == rChar {
n--
}
for n > 0 && s.value[n-1] == ' ' {
n--
}
s.value = s.value[:n]
if isMultiLineValue {
s.value, s.b, s.hLen = normalizeHeaderValue(s.value, oldB, s.hLen)
}
return true
}
func normalizeHeaderKey(b []byte, disableNormalizing bool) {
if disableNormalizing {
return
}
n := len(b)
if n == 0 {
return
}
b[0] = toUpperTable[b[0]]
for i := 1; i < n; i++ {
p := &b[i]
if *p == '-' {
i++
if i < n {
b[i] = toUpperTable[b[i]]
}
continue
}
*p = toLowerTable[*p]
}
}
func normalizeHeaderValue(ov, ob []byte, headerLength int) (nv, nb []byte, nhl int) {
nv = ov
length := len(ov)
if length <= 0 {
return
}
write := 0
shrunk := 0
lineStart := false
for read := 0; read < length; read++ {
c := ov[read]
switch {
case c == rChar || c == nChar:
shrunk++
if c == nChar {
lineStart = true
}
continue
case lineStart && c == '\t':
c = ' '
default:
lineStart = false
}
nv[write] = c
write++
}
nv = nv[:write]
copy(ob[write:], ob[write+shrunk:])
// Check if we need to skip \r\n or just \n
skip := 0
if ob[write] == rChar {
if ob[write+1] == nChar {
skip += 2
} else {
skip++
}
} else if ob[write] == nChar {
skip++
}
nb = ob[write+skip : len(ob)-shrunk]
nhl = headerLength - shrunk
return
}
func parseContentLength(b string) (int, error) {
v, n, err := parseUintBuf(b)
if err != nil {
return -1, fmt.Errorf("cannot parse Content-Length: %w", err)
}
if n != len(b) {
return -1, fmt.Errorf("cannot parse Content-Length: %w", errNonNumericChars)
}
return v, nil
}
func hasHeaderValue(s, value string) bool {
var vs headerValueScanner
vs.b = s
for vs.next() {
if caseInsensitiveCompare(vs.value, value) {
return true
}
}
return false
}
func (s *headerValueScanner) next() bool {
b := s.b
if len(b) == 0 {
return false
}
n := strings.IndexByte(b, ',')
if n < 0 {
s.value = stripSpace(b)
s.b = b[len(b):]
return true
}
s.value = stripSpace(b[:n])
s.b = b[n+1:]
return true
}
func nextLine(b []byte) ([]byte, []byte, error) {
nNext := bytes.IndexByte(b, nChar)
if nNext < 0 {
return nil, nil, errNeedMore
}
n := nNext
if n > 0 && b[n-1] == rChar {
n--
}
return b[:n], b[nNext+1:], nil
}
func stripSpace(b string) string {
for len(b) > 0 && b[0] == ' ' {
b = b[1:]
}
for len(b) > 0 && b[len(b)-1] == ' ' {
b = b[:len(b)-1]
}
return b
}
var (
errEmptyInt = errors.New("empty integer")
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 string) (int, int, error) {
n := len(b)
if n == 0 {
return -1, 0, errEmptyInt
}
v := 0
for i := 0; i < n; i++ {
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
}
/*
Request Parsing
*/
// Read reads request header from r.
//
// io.EOF is returned if r is closed before reading the first header byte.
func (h *header) Read(r *bufio.Reader) error {
return h.readLoop(r, true)
}
// readLoop reads request header from r optionally loops until it has enough data.
//
// io.EOF is returned if r is closed before reading the first header byte.
func (h *header) readLoop(r *bufio.Reader, waitForMore bool) error {
n := 1
for {
err := h.tryRead(r, n)
if err == nil {
return nil
}
if !waitForMore || err != errNeedMore {
h.resetSkipNormalize()
return err
}
n = r.Buffered() + 1
}
}
func (h *header) tryRead(r *bufio.Reader, n int) error {
h.resetSkipNormalize()
b, err := r.Peek(n)
if len(b) == 0 {
if err == io.EOF {
return err
}
if err == nil {
panic("bufio.Reader.Peek() returned nil, nil")
}
// This is for go 1.6 bug. See https://github.com/golang/go/issues/14121 .
if err == bufio.ErrBufferFull {
return &ErrSmallBuffer{
error: fmt.Errorf("error when reading request headers: %w (n=%d, r.Buffered()=%d)", errSmallBuffer, n, r.Buffered()),
}
}
// n == 1 on the first read for the request.
if n == 1 {
// We didn't read a single byte.
return ErrNothingRead{err}
}
return fmt.Errorf("error when reading request headers: %w", err)
}
b = mustPeekBuffered(r)
headersLen, errParse := h.parse(b)
if errParse != nil {
return headerError("request", err, errParse, b, false)
}
mustDiscard(r, headersLen)
return nil
}
func (h *header) resetSkipNormalize() {
h.noHTTP11 = false
h.connectionClose = false
h.contentLength = 0
h.contentLengthBytes = h.contentLengthBytes[:0]
h.method = h.method[:0]
h.proto = h.proto[:0]
h.requestURI = h.requestURI[:0]
h.host = h.host[:0]
h.contentType = h.contentType[:0]
h.userAgent = h.userAgent[:0]
h.trailer = h.trailer[:0]
h.mulHeader = h.mulHeader[:0]
h.h = h.h[:0]
h.cookies = h.cookies[:0]
h.cookiesCollected = false
h.rawHeaders = h.rawHeaders[:0]
}
func headerError(typ string, err, errParse error, b []byte, secureErrorLogMessage bool) error {
if errParse != errNeedMore {
return headerErrorMsg(typ, errParse, b, secureErrorLogMessage)
}
if err == nil {
return errNeedMore
}
// Buggy servers may leave trailing CRLFs after http body.
// Treat this case as EOF.
if isOnlyCRLF(b) {
return io.EOF
}
if err != bufio.ErrBufferFull {
return headerErrorMsg(typ, err, b, secureErrorLogMessage)
}
return &ErrSmallBuffer{
error: headerErrorMsg(typ, errSmallBuffer, b, secureErrorLogMessage),
}
}
func isOnlyCRLF(b []byte) bool {
for _, ch := range b {
if ch != rChar && ch != nChar {
return false
}
}
return true
}
func headerErrorMsg(typ string, err error, b []byte, secureErrorLogMessage bool) error {
return fmt.Errorf("error when reading %s headers: %w. Buffer size=%d", typ, err, len(b))
}
// ErrNothingRead is returned when a keep-alive connection is closed,
// either because the remote closed it or because of a read timeout.
type ErrNothingRead struct {
error
}
// ErrSmallBuffer is returned when the provided buffer size is too small
// for reading request and/or response headers.
//
// ReadBufferSize value from Server or clients should reduce the number
// of such errors.
type ErrSmallBuffer struct {
error
}
func mustPeekBuffered(r *bufio.Reader) []byte {
buf, err := r.Peek(r.Buffered())
if len(buf) == 0 || err != nil {
panic(fmt.Sprintf("bufio.Reader.Peek() returned unexpected data (%q, %v)", buf, err))
}
return buf
}
func mustDiscard(r *bufio.Reader, n int) {
if _, err := r.Discard(n); err != nil {
panic(fmt.Sprintf("bufio.Reader.Discard(%d) failed: %v", n, err))
}
}
// Peek returns header value for the given key.
//
// The returned value is valid until the request is released,
// either though ReleaseRequest or your request handler returning.
// Do not store references to returned value. Make copies instead.
func (h *header) Peek(key string) []byte {
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
return h.peek(b2s(k))
}
// Host returns Host header value.
func (h *header) Host() []byte {
if h.disableSpecialHeader {
return peekArg(h.h, HeaderHost)
}
return h.host
}
func getHeaderKeyBytes(kv *argsKV, key string, disableNormalizing bool) []byte {
kv.key = append(kv.key[:0], key...)
normalizeHeaderKey(kv.key, disableNormalizing)
return kv.key
}
func peekArg(h []argsKV, k string) []byte {
for i, n := 0, len(h); i < n; i++ {
kv := &h[i]
if b2s(kv.key) == k {
return kv.value
}
}
return nil
}
func (h *header) peek(key string) []byte {
switch key {
case HeaderHost:
return h.Host()
case HeaderContentType:
return h.ContentType()
case HeaderUserAgent:
return h.UserAgent()
case HeaderConnection:
if h.ConnectionClose() {
return []byte(strClose)
}
return peekArg(h.h, key)
case HeaderContentLength:
return h.contentLengthBytes
case HeaderCookie:
if h.cookiesCollected {
return appendRequestCookieBytes(nil, h.cookies)
}
return peekArg(h.h, key)
case HeaderTrailer:
return appendArgsKey(nil, h.trailer, strCommaSpace)
default:
return peekArg(h.h, key)
}
}
// ConnectionClose returns true if 'Connection: close' header is set.
func (h *header) ConnectionClose() bool {
return h.connectionClose
}
// UserAgent returns User-Agent header value.
func (h *header) UserAgent() []byte {
if h.disableSpecialHeader {
return peekArg(h.h, HeaderUserAgent)
}
return h.userAgent
}
func appendArgsKey(dst []byte, args []argsKV, sep string) []byte {
for i, n := 0, len(args); i < n; i++ {
kv := &args[i]
dst = append(dst, kv.key...)
if i+1 < n {
dst = append(dst, sep...)
}
}
return dst
}
// b2s converts byte slice to a string without memory allocation.
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
func b2s(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
// s2b converts string to a byte slice without memory allocation.
func s2b(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
+622
View File
@@ -0,0 +1,622 @@
package httpx
import (
"errors"
"log/slog"
"net/http"
"strconv"
"strings"
"unsafe"
"github.com/soypat/lneto/internal"
)
type headerBuf struct {
buf []byte
off int // offset into buf for parsing.
// args contains key-value store.
args []argsKV
}
type tokint = uint16
type headerSlice struct {
start tokint
len tokint
}
type argSlice struct {
start tokint
len tokint
}
type argsKV struct {
key headerSlice
value headerSlice // value start >0 means value is present.
}
func (tb headerBuf) musttoken(slice headerSlice) []byte {
return tb.buf[slice.start : slice.start+slice.len]
}
func (tb headerBuf) mustargs(slice argSlice) []argsKV {
return tb.args[slice.start : slice.start+slice.len]
}
func (tb headerBuf) visitArgs(args argSlice, f func(k, v []byte)) {
a := tb.mustargs(args)
for _, arg := range a {
k := tb.musttoken(arg.key)
v := tb.musttoken(arg.value)
f(k, v)
}
}
func (tb headerBuf) visitArgsKey(args argSlice, f func(k []byte)) {
a := tb.mustargs(args)
for _, arg := range a {
f(tb.musttoken(arg.key))
}
}
func (tb headerBuf) slice(b []byte) headerSlice {
base := uintptr(unsafe.Pointer(&tb.buf[0]))
off := uintptr(unsafe.Pointer(&b[0]))
if off < base || off > base+uintptr(len(tb.buf)) {
panic("httpx: argument buffer does not alias header buffer")
}
return headerSlice{
start: tokint(off - base),
len: tokint(len(b)),
}
}
func (kv argsKV) HasValue() bool { return kv.value.start > 0 }
type flags uint8
const (
disableNormalizing flags = 1 << iota
disableSpecialHeader
noDefaultContentType
connectionClose
noHTTP11
cookiesCollected
)
func (f flags) hasAll(checkThese flags) bool {
return f&checkThese == checkThese
}
type header struct {
buf headerBuf
contentLength int
h argSlice
cookies argSlice
trailer argSlice
host headerSlice
contentLengthBytes headerSlice
contentType headerSlice
userAgent headerSlice
method headerSlice
proto headerSlice
requestURI headerSlice
rawHeaders headerSlice
mulHeader headerSlice
flags flags
logger *slog.Logger
}
func (h *header) Set(key, value string) {
h.bufKV.key = append(h.bufKV.key[:0], key...)
normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
h.SetCanonical(b2s(h.bufKV.key), value)
}
func (h *header) Add(key, value string) {
if h.setSpecialHeader(key, value) {
return
}
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
h.h = appendArg(h.h, b2s(k), value, argsHasValue)
}
// ContentEncoding returns Content-Encoding header value.
func (h *header) ContentEncoding() []byte {
return peekArg(h.h, strContentEncoding)
}
// SetContentEncoding sets Content-Encoding header value.
func (h *header) SetContentEncoding(contentEncoding string) {
h.Set(strContentEncoding, contentEncoding)
}
// SetContentType sets Content-Type header value.
func (h *header) SetContentType(contentType string) {
h.contentType = append(h.contentType[:0], contentType...)
}
// ContentType returns Content-Type header value.
func (h *header) ContentType() []byte {
contentType := h.contentType
if !h.noDefaultContentType && len(h.contentType) == 0 {
contentType = append(contentType, defaultContentType...)
}
return contentType
}
// SetCanonical sets the given 'key: value' header assuming that
// key is in canonical form.
//
// If the header is set as a Trailer (forbidden trailers will not be set, see SetTrailer for more details),
// it will be sent after the chunked request body.
func (h *header) SetCanonical(key, value string) {
if h.setSpecialHeader(key, value) {
return
}
h.setNonSpecial(key, value)
}
// setSpecialHeader handles special headers and return true when a header is processed.
func (h *header) setSpecialHeader(key, value string) bool {
if len(key) == 0 || h.disableSpecialHeader {
return false
}
h.trace("setSpecialHeader", slog.String("key", key), slog.String("value", value))
switch key[0] | 0x20 {
case 'c':
switch {
case caseInsensitiveCompare(strContentType, key):
h.SetContentType(value)
return true
case caseInsensitiveCompare(strContentLength, key):
if contentLength, err := parseContentLength(value); err == nil {
h.contentLength = contentLength
h.contentLengthBytes = append(h.contentLengthBytes[:0], value...)
}
return true
case caseInsensitiveCompare(strConnection, key):
if strClose == value {
h.SetConnectionClose()
} else {
h.ResetConnectionClose()
h.setNonSpecial(key, value)
}
return true
case caseInsensitiveCompare(strCookie, key):
h.collectCookies()
h.cookies = parseRequestCookies(h.cookies, value)
return true
}
case 't': // OK
if caseInsensitiveCompare(strTransferEncoding, key) {
// Transfer-Encoding is managed automatically.
return true
} else if caseInsensitiveCompare(strTrailer, key) {
_ = h.SetTrailer(value)
return true
}
case 'h':
if caseInsensitiveCompare(strHost, key) {
h.SetHost(value)
return true
}
case 'u':
if caseInsensitiveCompare(strUserAgent, key) {
h.SetUserAgent(value)
return true
}
}
return false
}
// SetHost sets Host header value.
func (h *header) SetHost(host string) {
h.host = append(h.host[:0], host...)
}
// SetUserAgent sets User-Agent header value.
func (h *header) SetUserAgent(userAgent string) {
h.userAgent = append(h.userAgent[:0], userAgent...)
}
// SetConnectionClose sets 'Connection: close' header.
func (h *header) SetConnectionClose() {
h.connectionClose = true
}
// ResetConnectionClose clears 'Connection: close' header if it exists.
func (h *header) ResetConnectionClose() {
if h.connectionClose {
h.connectionClose = false
h.h = delAllArgs(h.h, strConnection)
}
}
func (h *header) SetContentRange(startPos, endPos, contentLength int) {
b := h.bufKV.value[:0]
b = append(b, strBytes...)
b = append(b, ' ')
b = appendUint(b, startPos)
b = append(b, '-')
b = appendUint(b, endPos)
b = append(b, '/')
b = appendUint(b, contentLength)
h.bufKV.value = b
h.setNonSpecial(strContentRange, b2s(h.bufKV.value))
}
// setNonSpecial directly put into map i.e. not a basic header.
func (h *header) setNonSpecial(key string, value string) {
h.trace("httpx:setNonSpecial", slog.String("key", key), slog.String("value", value))
h.h = setArg(h.h, key, value, argsHasValue)
}
func appendUint(b []byte, v int) []byte {
if v < 0 {
panic("negative uint")
}
return strconv.AppendUint(b, uint64(v), 10)
}
// ContentLength returns Content-Length header value.
//
// It may be negative:
// -1 means Transfer-Encoding: chunked.
// -2 means Transfer-Encoding: identity.
func (h *header) ContentLength() int {
return h.contentLength
}
// SetContentLength sets Content-Length header value.
//
// Content-Length may be negative:
// -1 means Transfer-Encoding: chunked.
// -2 means Transfer-Encoding: identity.
func (h *header) SetContentLength(contentLength int) {
h.contentLength = contentLength
if contentLength >= 0 {
h.contentLengthBytes = appendUint(h.contentLengthBytes[:0], contentLength)
h.h = delAllArgs(h.h, strTransferEncoding)
} else {
h.contentLengthBytes = h.contentLengthBytes[:0]
h.h = setArg(h.h, strTransferEncoding, strChunked, argsHasValue)
}
}
var ErrBadTrailer = errors.New("contain forbidden trailer")
// SetTrailer sets Trailer header value for chunked request
// to indicate which headers will be sent after the body.
//
// Use Set to set the trailer header later.
//
// Trailers are only supported with chunked transfer.
// Trailers allow the sender to include additional headers at the end of chunked messages.
//
// The following trailers are forbidden:
// 1. necessary for message framing (e.g., Transfer-Encoding and Content-Length),
// 2. routing (e.g., Host),
// 3. request modifiers (e.g., controls and conditionals in Section 5 of [RFC7231]),
// 4. authentication (e.g., see [RFC7235] and [RFC6265]),
// 5. response control data (e.g., see Section 7.1 of [RFC7231]),
// 6. determining how to process the payload (e.g., Content-Encoding, Content-Type, Content-Range, and Trailer)
//
// Return ErrBadTrailer if contain any forbidden trailers.
func (h *header) SetTrailer(trailer string) error {
h.trailer = h.trailer[:0]
return h.AddTrailer(trailer)
}
// AddTrailerBytes add Trailer header value for chunked response
// to indicate which headers will be sent after the body.
//
// Use Set to set the trailer header later.
//
// Trailers are only supported with chunked transfer.
// Trailers allow the sender to include additional headers at the end of chunked messages.
//
// The following trailers are forbidden:
// 1. necessary for message framing (e.g., Transfer-Encoding and Content-Length),
// 2. routing (e.g., Host),
// 3. request modifiers (e.g., controls and conditionals in Section 5 of [RFC7231]),
// 4. authentication (e.g., see [RFC7235] and [RFC6265]),
// 5. response control data (e.g., see Section 7.1 of [RFC7231]),
// 6. determining how to process the payload (e.g., Content-Encoding, Content-Type, Content-Range, and Trailer)
//
// Return ErrBadTrailer if contain any forbidden trailers.
func (h *header) AddTrailer(trailer string) error {
h.trace("httpx:AddTrailer", slog.String("trailer", trailer))
var err error
for i := -1; i+1 < len(trailer); {
trailer = trailer[i+1:]
i = strings.IndexByte(trailer, ',')
if i < 0 {
i = len(trailer)
}
key := stripSpace(trailer[:i])
// Forbidden by RFC 7230, section 4.1.2
if isBadTrailer(key) {
err = ErrBadTrailer
continue
}
h.bufKV.key = append(h.bufKV.key[:0], key...)
normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
h.trailer = appendArg(h.trailer, b2s(h.bufKV.key), "", argsNoValue)
}
return err
}
func isBadTrailer(key string) bool {
if len(key) == 0 {
return true
}
switch key[0] | 0x20 {
case 'a':
return caseInsensitiveCompare(key, strAuthorization)
case 'c':
if len(key) > len(HeaderContentType) && caseInsensitiveCompare(key[:8], strContentType[:8]) {
// skip compare prefix 'Content-'
return caseInsensitiveCompare(key[8:], strContentEncoding[8:]) ||
caseInsensitiveCompare(key[8:], strContentLength[8:]) ||
caseInsensitiveCompare(key[8:], strContentType[8:]) ||
caseInsensitiveCompare(key[8:], strContentRange[8:])
}
return caseInsensitiveCompare(key, strConnection)
case 'e':
return caseInsensitiveCompare(key, strExpect)
case 'h':
return caseInsensitiveCompare(key, strHost)
case 'k':
return caseInsensitiveCompare(key, strKeepAlive)
case 'm':
return caseInsensitiveCompare(key, strMaxForwards)
case 'p':
if len(key) > len(HeaderProxyConnection) && caseInsensitiveCompare(key[:6], strProxyConnection[:6]) {
// skip compare prefix 'Proxy-'
return caseInsensitiveCompare(key[6:], strProxyConnection[6:]) ||
caseInsensitiveCompare(key[6:], strProxyAuthenticate[6:]) ||
caseInsensitiveCompare(key[6:], strProxyAuthorization[6:])
}
case 'r':
return caseInsensitiveCompare(key, strRange)
case 't':
return caseInsensitiveCompare(key, strTE) ||
caseInsensitiveCompare(key, strTrailer) ||
caseInsensitiveCompare(key, strTransferEncoding)
case 'w':
return caseInsensitiveCompare(key, strWWWAuthenticate)
}
return false
}
// RawHeaders returns raw header key/value bytes.
//
// Depending on server configuration, header keys may be normalized to
// capital-case in place.
//
// This copy is set aside during parsing, so empty slice is returned for all
// cases where parsing did not happen. Similarly, request line is not stored
// during parsing and can not be returned.
//
// The slice is not safe to use after the handler returns.
func (h *header) RawHeaders() []byte {
return h.rawHeaders
}
// DisableNormalizing disables header names' normalization.
//
// By default all the header names are normalized by uppercasing
// the first letter and all the first letters following dashes,
// while lowercasing all the other letters.
// Examples:
//
// - CONNECTION -> Connection
// - conteNT-tYPE -> Content-Type
// - foo-bar-baz -> Foo-Bar-Baz
//
// Disable header names' normalization only if know what are you doing.
func (h *header) DisableNormalizing() {
h.disableNormalizing = true
}
// DisableSpecialHeader disables special header processing.
// fasthttp will not set any special headers for you, such as Host, Content-Type, User-Agent, etc.
// You must set everything yourself.
// If RequestHeader.Read() is called, special headers will be ignored.
// This can be used to control case and order of special headers.
// This is generally not recommended.
func (h *header) DisableSpecialHeader() {
h.disableSpecialHeader = true
}
// Method returns HTTP request method.
func (h *header) Method() []byte {
if len(h.method) == 0 {
h.method = append(h.method, http.MethodGet...)
}
return h.method
}
func (h *header) SetMethod(method string) {
h.method = append(h.method[:0], method...)
}
// SetRequestURI sets RequestURI for the first HTTP request line.
func (h *header) SetRequestURI(requestURI string) {
h.requestURI = append(h.requestURI[:0], requestURI...)
}
// RequestURI returns RequestURI from the first HTTP request line.
func (h *header) RequestURI() []byte {
requestURI := h.requestURI
if len(requestURI) == 0 {
requestURI = append(requestURI, '/')
}
return requestURI
}
// Protocol returns HTTP protocol.
func (h *header) Protocol() []byte {
if len(h.proto) == 0 {
h.proto = append(h.proto, strHTTP11...)
}
return h.proto
}
func (h *header) SetProtocol(protocol string) {
h.proto = append(h.proto[:0], protocol...)
}
// AppendReqRespCommon appends request/response common header representation to dst and returns the extended buffer.
func (h *header) AppendReqRespCommon(dst []byte) []byte {
for i, n := 0, len(h.h); i < n; i++ {
kv := &h.h[i]
// Exclude trailer from header
exclude := false
for _, t := range h.trailer {
if b2s(kv.key) == b2s(t.key) {
exclude = true
break
}
}
if !exclude {
dst = appendHeaderLine(dst, b2s(kv.key), b2s(kv.value))
}
}
if len(h.trailer) > 0 {
aux := appendArgsKey(nil, h.trailer, strCommaSpace)
dst = appendHeaderLine(dst, strTrailer, b2s(aux))
}
// there is no need in h.collectCookies() here, since if cookies aren't collected yet,
// they all are located in h.h.
n := len(h.cookies)
if n > 0 && !h.disableSpecialHeader {
dst = append(dst, strCookie...)
dst = append(dst, strColonSpace...)
dst = appendRequestCookieBytes(dst, h.cookies)
dst = append(dst, strCRLF...)
}
if h.ConnectionClose() && !h.disableSpecialHeader {
dst = appendHeaderLine(dst, strConnection, strClose)
}
return append(dst, strCRLF...)
}
func appendHeaderLine(dst []byte, key, value string) []byte {
dst = append(dst, key...)
dst = append(dst, strColonSpace...)
dst = append(dst, value...)
return append(dst, strCRLF...)
}
func (h *header) ignoreBody() bool {
return h.IsGet() || h.IsHead()
}
func (h *header) collectCookies() {
if h.cookiesCollected {
return
}
for i, n := 0, len(h.h); i < n; i++ {
kv := &h.h[i]
if caseInsensitiveCompare(b2s(kv.key), strCookie) {
h.cookies = parseRequestCookies(h.cookies, b2s(kv.value))
tmp := *kv
copy(h.h[i:], h.h[i+1:])
n--
i--
h.h[n] = tmp
h.h = h.h[:n]
}
}
h.cookiesCollected = true
}
func (h *header) MethodIs(method string) bool {
return b2s(h.method) == method
}
// IsGet returns true if request method is GET.
func (h *header) IsGet() bool { return len(h.method) == 0 || h.MethodIs(http.MethodGet) }
// IsHead returns true if request method is HEAD.
func (h *header) IsHead() bool { return h.MethodIs(http.MethodHead) }
// IsPost returns true if request method is POST.
func (h *header) IsPost() bool { return h.MethodIs(http.MethodPost) }
// IsPut returns true if request method is PUT.
func (h *header) IsPut() bool { return h.MethodIs(http.MethodPut) }
// IsDelete returns true if request method is DELETE.
func (h *header) IsDelete() bool { return h.MethodIs(http.MethodDelete) }
// IsConnect returns true if request method is CONNECT.
func (h *header) IsConnect() bool { return h.MethodIs(http.MethodConnect) }
// IsOptions returns true if request method is OPTIONS.
func (h *header) IsOptions() bool { return h.MethodIs(http.MethodOptions) }
// IsTrace returns true if request method is TRACE.
func (h *header) IsTrace() bool { return h.MethodIs(http.MethodTrace) }
// IsPatch returns true if request method is PATCH.
func (h *header) IsPatch() bool { return h.MethodIs(http.MethodPatch) }
// IsHTTP11 returns true if the request is HTTP/1.1.
func (h *header) IsHTTP11() bool { return !h.noHTTP11 }
// Embed this type into a struct, which mustn't be copied,
// so `go vet` gives a warning if this struct is copied.
//
// See https://github.com/golang/go/issues/8005#issuecomment-190753527 for details.
// and also: https://stackoverflow.com/questions/52494458/nocopy-minimal-example
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
func (h *header) trace(msg string, attrs ...slog.Attr) {
internal.LogAttrs(h.logger, internal.LevelTrace, msg, attrs...)
}
func (h *header) debug(msg string, attrs ...slog.Attr) {
internal.LogAttrs(h.logger, slog.LevelDebug, msg, attrs...)
}
func (h *header) info(msg string, attrs ...slog.Attr) {
internal.LogAttrs(h.logger, slog.LevelInfo, msg, attrs...)
}
func normalizeHeaderKey(b []byte, disableNormalizing bool) {
if disableNormalizing {
return
}
n := len(b)
if n == 0 {
return
}
b[0] = toUpperTable[b[0]]
for i := 1; i < n; i++ {
p := &b[i]
if *p == '-' {
i++
if i < n {
b[i] = toUpperTable[b[i]]
}
continue
}
*p = toLowerTable[*p]
}
}