diff --git a/httpx/definitions.go b/httpx/definitions.go new file mode 100644 index 0000000..ce73fca --- /dev/null +++ b/httpx/definitions.go @@ -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" +) diff --git a/httpx/header_parse.go b/httpx/header_parse.go new file mode 100644 index 0000000..915541c --- /dev/null +++ b/httpx/header_parse.go @@ -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)) +} diff --git a/httpx/tokenizer.go b/httpx/tokenizer.go new file mode 100644 index 0000000..667f53c --- /dev/null +++ b/httpx/tokenizer.go @@ -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] + } +}