Files

1174 lines
37 KiB
Go

package fiber
import (
"bytes"
"errors"
"fmt"
"math"
"mime/multipart"
"net"
"strconv"
"strings"
"github.com/gofiber/utils/v2"
utilsbytes "github.com/gofiber/utils/v2/bytes"
utilsstrings "github.com/gofiber/utils/v2/strings"
"github.com/valyala/bytebufferpool"
"github.com/valyala/fasthttp"
"golang.org/x/net/idna"
)
// Pre-allocated byte slices for common header comparisons to avoid allocations
var (
xForwardedPrefix = []byte("X-Forwarded-")
xForwardedProtoBytes = []byte(HeaderXForwardedProto)
xForwardedProtocolBytes = []byte(HeaderXForwardedProtocol)
xForwardedSslBytes = []byte(HeaderXForwardedSsl)
xURLSchemeBytes = []byte(HeaderXUrlScheme)
onBytes = []byte("on")
)
// Range represents the parsed HTTP Range header extracted by DefaultReq.Range.
type Range struct {
Type string
Ranges []RangeSet
}
// RangeSet represents a single content range from a request.
type RangeSet struct {
Start int64
End int64
}
// DefaultReq is the default implementation of Req used by DefaultCtx.
//
//go:generate ifacemaker --file req.go --struct DefaultReq --iface Req --pkg fiber --output req_interface_gen.go --not-exported true --iface-comment "Req is an interface for request-related Ctx methods."
type DefaultReq struct {
c *DefaultCtx
}
// Accepts checks if the specified extensions or content types are acceptable.
func (r *DefaultReq) Accepts(offers ...string) string {
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAccept))
return getOffer(header, acceptsOfferType, offers...)
}
// AcceptsCharsets checks if the specified charset is acceptable.
func (r *DefaultReq) AcceptsCharsets(offers ...string) string {
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptCharset))
return getOffer(header, acceptsOffer, offers...)
}
// AcceptsEncodings checks if the specified encoding is acceptable.
func (r *DefaultReq) AcceptsEncodings(offers ...string) string {
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptEncoding))
return getOffer(header, acceptsOffer, offers...)
}
// AcceptsLanguages checks if the specified language is acceptable using
// RFC 4647 Basic Filtering.
func (r *DefaultReq) AcceptsLanguages(offers ...string) string {
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptLanguage))
return getOffer(header, acceptsLanguageOfferBasic, offers...)
}
// AcceptsLanguagesExtended checks if the specified language is acceptable using
// RFC 4647 Extended Filtering.
func (r *DefaultReq) AcceptsLanguagesExtended(offers ...string) string {
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptLanguage))
return getOffer(header, acceptsLanguageOfferExtended, offers...)
}
// App returns the *App reference to the instance of the Fiber application
func (r *DefaultReq) App() *App {
return r.c.app
}
// BaseURL returns (protocol + host + base path).
func (r *DefaultReq) BaseURL() string {
return r.c.BaseURL()
}
// BodyRaw contains the raw body submitted in a POST request.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
func (r *DefaultReq) BodyRaw() []byte {
return r.getBody()
}
//nolint:nonamedreturns // gocritic unnamedResult prefers naming decoded body, decode count, and error
func (r *DefaultReq) tryDecodeBodyInOrder(
originalBody *[]byte,
encodings []string,
) (body []byte, decodesRealized uint8, err error) {
request := &r.c.fasthttp.Request
maxBodySize := r.c.app.config.BodyLimit
for idx := range encodings {
i := len(encodings) - 1 - idx
encoding := encodings[i]
decodesRealized++
var decodeErr error
switch encoding {
case StrGzip, "x-gzip":
body, decodeErr = request.BodyGunzipWithLimit(maxBodySize)
case StrBr, StrBrotli:
body, decodeErr = request.BodyUnbrotliWithLimit(maxBodySize)
case StrDeflate:
body, decodeErr = request.BodyInflateWithLimit(maxBodySize)
case StrZstd:
body, decodeErr = request.BodyUnzstdWithLimit(maxBodySize)
case StrIdentity:
body = request.Body()
case StrCompress, "x-compress":
return nil, decodesRealized - 1, ErrNotImplemented
default:
return nil, decodesRealized - 1, ErrUnsupportedMediaType
}
if decodeErr != nil {
return nil, decodesRealized, decodeErr
}
if i > 0 && decodesRealized > 0 {
if i == len(encodings)-1 {
tempBody := request.Body()
*originalBody = make([]byte, len(tempBody))
copy(*originalBody, tempBody)
}
request.SetBodyRaw(body)
}
}
return body, decodesRealized, nil
}
// Body contains the raw body submitted in a POST request.
// This method will decompress the body if the 'Content-Encoding' header is provided.
// It returns the original (or decompressed) body data which is valid only within the handler.
// Don't store direct references to the returned data.
// If you need to keep the body's data later, make a copy or use the Immutable option.
func (r *DefaultReq) Body() []byte {
var (
err error
body, originalBody []byte
headerEncoding string
encodingOrder = []string{"", "", ""}
)
request := &r.c.fasthttp.Request
// Get Content-Encoding header
headerEncoding = utils.UnsafeString(utilsbytes.UnsafeToLower(request.Header.ContentEncoding()))
// If no encoding is provided, return the original body
if headerEncoding == "" {
return r.getBody()
}
// Split and get the encodings list, in order to attend the
// rule defined at: https://www.rfc-editor.org/rfc/rfc9110#section-8.4-5
encodingOrder = getSplicedStrList(headerEncoding, encodingOrder)
for i := range encodingOrder {
encodingOrder[i] = utilsstrings.UnsafeToLower(encodingOrder[i])
}
if len(encodingOrder) == 0 {
return r.getBody()
}
var decodesRealized uint8
body, decodesRealized, err = r.tryDecodeBodyInOrder(&originalBody, encodingOrder)
// Ensure that the body will be the original
if originalBody != nil && decodesRealized > 0 {
request.SetBodyRaw(originalBody)
}
if err != nil {
switch {
case errors.Is(err, ErrUnsupportedMediaType):
_ = r.c.DefaultRes.SendStatus(StatusUnsupportedMediaType) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check
case errors.Is(err, ErrNotImplemented):
_ = r.c.DefaultRes.SendStatus(StatusNotImplemented) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check
case errors.Is(err, fasthttp.ErrBodyTooLarge):
_ = r.c.DefaultRes.SendStatus(StatusRequestEntityTooLarge) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check
default:
// do nothing
}
return []byte(err.Error())
}
return r.c.app.GetBytes(body)
}
// RequestCtx returns *fasthttp.RequestCtx that carries a deadline
// a cancellation signal, and other values across API boundaries.
func (r *DefaultReq) RequestCtx() *fasthttp.RequestCtx {
return r.c.fasthttp
}
// FullURL returns the full request URL (protocol + host + original URL).
func (c *DefaultCtx) FullURL() string {
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
buf.WriteString(c.Scheme())
buf.WriteString("://")
buf.WriteString(c.Host())
buf.WriteString(c.OriginalURL())
return c.app.toString(buf.Bytes())
}
// UserAgent returns the User-Agent request header.
func (c *DefaultCtx) UserAgent() string {
return c.app.toString(c.fasthttp.Request.Header.UserAgent())
}
// Referer returns the Referer request header.
func (c *DefaultCtx) Referer() string {
return c.app.toString(c.fasthttp.Request.Header.Referer())
}
// AcceptLanguage returns the Accept-Language request header.
func (c *DefaultCtx) AcceptLanguage() string {
return c.app.toString(c.fasthttp.Request.Header.Peek(HeaderAcceptLanguage))
}
// AcceptEncoding returns the Accept-Encoding request header.
func (c *DefaultCtx) AcceptEncoding() string {
return c.app.toString(c.fasthttp.Request.Header.Peek(HeaderAcceptEncoding))
}
// HasHeader reports whether the request includes a header with the given key.
func (c *DefaultCtx) HasHeader(key string) bool {
return len(c.fasthttp.Request.Header.Peek(key)) > 0
}
// MediaType returns the MIME type from the Content-Type header without parameters.
func (c *DefaultCtx) MediaType() string {
contentType := utils.TrimSpace(c.fasthttp.Request.Header.ContentType())
if len(contentType) == 0 {
return ""
}
if idx := bytes.IndexByte(contentType, ';'); idx != -1 {
contentType = contentType[:idx]
}
contentType = utils.TrimSpace(contentType)
return c.app.toString(contentType)
}
// Charset returns the charset parameter from the Content-Type header.
func (c *DefaultCtx) Charset() string {
contentType := c.fasthttp.Request.Header.ContentType()
if len(contentType) == 0 {
return ""
}
_, after, ok := bytes.Cut(contentType, []byte{';'})
if !ok {
return ""
}
params := after
for len(params) > 0 {
params = utils.TrimSpace(params)
if len(params) == 0 {
return ""
}
param := params
if idx := bytes.IndexByte(params, ';'); idx != -1 {
param = params[:idx]
params = params[idx+1:]
} else {
params = nil
}
param = utils.TrimSpace(param)
if len(param) == 0 {
continue
}
before, after, ok := bytes.Cut(param, []byte{'='})
if !ok {
continue
}
name := utils.TrimSpace(before)
if !bytes.EqualFold(name, []byte("charset")) {
continue
}
value := utils.TrimSpace(after)
if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
value = value[1 : len(value)-1]
}
return c.app.toString(value)
}
return ""
}
// IsJSON reports whether the Content-Type header is JSON.
func (c *DefaultCtx) IsJSON() bool {
return utils.EqualFold(c.MediaType(), MIMEApplicationJSON)
}
// IsForm reports whether the Content-Type header is form-encoded.
func (c *DefaultCtx) IsForm() bool {
return utils.EqualFold(c.MediaType(), MIMEApplicationForm)
}
// IsMultipart reports whether the Content-Type header is multipart form data.
func (c *DefaultCtx) IsMultipart() bool {
return utils.EqualFold(c.MediaType(), MIMEMultipartForm)
}
// AcceptsJSON reports whether the Accept header allows JSON.
func (c *DefaultCtx) AcceptsJSON() bool {
return c.Accepts(MIMEApplicationJSON) != ""
}
// AcceptsHTML reports whether the Accept header allows HTML.
func (c *DefaultCtx) AcceptsHTML() bool {
return c.Accepts(MIMETextHTML) != ""
}
// AcceptsXML reports whether the Accept header allows XML.
func (c *DefaultCtx) AcceptsXML() bool {
return c.Accepts(MIMEApplicationXML, MIMETextXML) != ""
}
// AcceptsEventStream reports whether the Accept header allows text/event-stream.
func (c *DefaultCtx) AcceptsEventStream() bool {
return c.Accepts("text/event-stream") != ""
}
// Cookies are used for getting a cookie value by key.
// Defaults to the empty string "" if the cookie doesn't exist.
// If a default value is given, it will return that value if the cookie doesn't exist.
// The returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting to use the value outside the Handler.
func (r *DefaultReq) Cookies(key string, defaultValue ...string) string {
return defaultString(r.c.app.toString(r.c.fasthttp.Request.Header.Cookie(key)), defaultValue)
}
// Request return the *fasthttp.Request object
// This allows you to use all fasthttp request methods
// https://godoc.org/github.com/valyala/fasthttp#Request
func (r *DefaultReq) Request() *fasthttp.Request {
return &r.c.fasthttp.Request
}
// FormFile returns the first file by key from a MultipartForm.
// The multipart form is parsed using the application's BodyLimit to prevent
// unbounded memory usage.
func (r *DefaultReq) FormFile(key string) (*multipart.FileHeader, error) {
if _, err := r.MultipartForm(); err != nil {
return nil, err
}
return r.c.fasthttp.FormFile(key)
}
// FormValue returns the first value by key from a MultipartForm.
// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.
// Defaults to the empty string "" if the form value doesn't exist.
// If a default value is given, it will return that value if the form value does not exist.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
// When the request is a multipart form, it is parsed using the application's
// BodyLimit so the configured limit is consistently enforced.
func (r *DefaultReq) FormValue(key string, defaultValue ...string) string {
if r.c.IsMultipart() {
// For multipart requests, parse the form using the application's BodyLimit.
// fasthttp's FormValue would otherwise re-parse with its default 8 MiB limit,
// effectively bypassing the configured BodyLimit.
//
// Preserve the original search order: QueryArgs → PostArgs → MultipartForm.
if v := r.c.fasthttp.QueryArgs().Peek(key); len(v) > 0 {
return r.c.app.toString(v)
}
if v := r.c.fasthttp.PostArgs().Peek(key); len(v) > 0 {
return r.c.app.toString(v)
}
mf, err := r.MultipartForm()
if err != nil {
return defaultString("", defaultValue)
}
if vals := mf.Value[key]; len(vals) > 0 {
return vals[0]
}
return defaultString("", defaultValue)
}
return defaultString(r.c.app.toString(r.c.fasthttp.FormValue(key)), defaultValue)
}
// Fresh returns true when the response is still “fresh” in the client's cache,
// otherwise false is returned to indicate that the client cache is now stale
// and the full response should be sent.
// When a client sends the Cache-Control: no-cache request header to indicate an end-to-end
// reload request, this module will return false to make handling these requests transparent.
// https://github.com/jshttp/fresh/blob/master/index.js#L33
func (r *DefaultReq) Fresh() bool {
header := &r.c.fasthttp.Request.Header
// fields
modifiedSince := header.Peek(HeaderIfModifiedSince)
noneMatch := header.Peek(HeaderIfNoneMatch)
// unconditional request
if len(modifiedSince) == 0 && len(noneMatch) == 0 {
return false
}
// Always return stale when Cache-Control: no-cache
// to support end-to-end reload requests
// https://www.rfc-editor.org/rfc/rfc9111#section-5.2.1.4
cacheControl := header.Peek(HeaderCacheControl)
if len(cacheControl) > 0 && isNoCache(utils.UnsafeString(cacheControl)) {
return false
}
// if-none-match
if len(noneMatch) > 0 && (len(noneMatch) != 1 || noneMatch[0] != '*') {
app := r.c.app
response := &r.c.fasthttp.Response
etag := app.toString(response.Header.Peek(HeaderETag))
if etag == "" {
return false
}
if app.isEtagStale(etag, noneMatch) {
return false
}
if len(modifiedSince) > 0 {
lastModified := response.Header.Peek(HeaderLastModified)
if len(lastModified) > 0 {
lastModifiedTime, err := fasthttp.ParseHTTPDate(lastModified)
if err != nil {
return false
}
modifiedSinceTime, err := fasthttp.ParseHTTPDate(modifiedSince)
if err != nil {
return false
}
return lastModifiedTime.Compare(modifiedSinceTime) != 1
}
}
}
return true
}
// Get returns the HTTP request header specified by field.
// Field names are case-insensitive
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
func (r *DefaultReq) Get(key string, defaultValue ...string) string {
return GetReqHeader(r.c, key, defaultValue...)
}
// GetReqHeader returns the HTTP request header specified by filed.
// This function is generic and can handle different headers type values.
// If the generic type cannot be matched to a supported type, the function
// returns the default value (if provided) or the zero value of type V.
func GetReqHeader[V GenericType](c Ctx, key string, defaultValue ...V) V {
v, err := genericParseType[V](c.App().toString(c.Request().Header.Peek(key)))
if err != nil && len(defaultValue) > 0 {
return defaultValue[0]
}
return v
}
// GetHeaders (a.k.a GetReqHeaders) returns the HTTP request headers.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
func (r *DefaultReq) GetHeaders() map[string][]string {
app := r.c.app
reqHeader := &r.c.fasthttp.Request.Header
// Pre-allocate map with known header count to avoid reallocations
headers := make(map[string][]string, reqHeader.Len())
for k, v := range reqHeader.All() {
key := app.toString(k)
headers[key] = append(headers[key], app.toString(v))
}
return headers
}
// Host contains the host derived from the X-Forwarded-Host or Host HTTP header.
// Returned value is only valid within the handler. Do not store any references.
// In a network context, `Host` refers to the combination of a hostname and potentially a port number used for connecting,
// while `Hostname` refers specifically to the name assigned to a device on a network, excluding any port information.
// Example: URL: https://example.com:8080 -> Host: example.com:8080
// Make copies or use the Immutable setting instead.
// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.
func (r *DefaultReq) Host() string {
if r.IsProxyTrusted() {
if host := r.Get(HeaderXForwardedHost); host != "" {
if before, _, found := strings.Cut(host, ","); found {
return utils.TrimSpace(before)
}
return utils.TrimSpace(host)
}
}
return r.c.app.toString(r.c.fasthttp.Request.URI().Host())
}
// Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header using the c.Host() method.
// Returned value is only valid within the handler. Do not store any references.
// Example: URL: https://example.com:8080 -> Hostname: example.com
// Make copies or use the Immutable setting instead.
// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.
func (r *DefaultReq) Hostname() string {
addr, _ := parseAddr(r.Host())
return addr
}
// Port returns the remote port of the request.
func (r *DefaultReq) Port() string {
addr := r.c.fasthttp.RemoteAddr()
if addr == nil {
return "0"
}
switch typedAddr := addr.(type) {
case *net.TCPAddr:
return strconv.Itoa(typedAddr.Port)
case *net.UnixAddr:
return ""
}
_, port, err := net.SplitHostPort(addr.String())
if err != nil {
return ""
}
return port
}
// IP returns the remote IP address of the request.
// If ProxyHeader and IP Validation is configured, it will parse that header and return the first valid IP address.
// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.
func (r *DefaultReq) IP() string {
app := r.c.app
if r.IsProxyTrusted() && app.config.ProxyHeader != "" {
return r.extractIPFromHeader(app.config.ProxyHeader)
}
if ip := r.c.fasthttp.RemoteIP(); ip != nil {
return ip.String()
}
return ""
}
// extractIPsFromHeader will return a slice of IPs it found given a header name in the order they appear.
// When IP validation is enabled, any invalid IPs will be omitted.
func (r *DefaultReq) extractIPsFromHeader(header string) []string {
// TODO: Reuse the c.extractIPFromHeader func somehow in here
headerValue := r.Get(header)
// We can't know how many IPs we will return, but we will try to guess with this constant division.
// Counting ',' makes function slower for about 50ns in general case.
const maxEstimatedCount = 8
estimatedCount := min(len(headerValue)/maxEstimatedCount,
// Avoid big allocation on big header
maxEstimatedCount)
ipsFound := make([]string, 0, estimatedCount)
i := 0
j := -1
for {
var v4, v6 bool
// Manually splitting string without allocating slice, working with parts directly
i, j = j+1, j+2
if j > len(headerValue) {
break
}
for j < len(headerValue) && headerValue[j] != ',' {
switch headerValue[j] {
case ':':
v6 = true
case '.':
v4 = true
default:
// do nothing
}
j++
}
for i < j && (headerValue[i] == ' ' || headerValue[i] == ',') {
i++
}
s := utils.TrimRight(headerValue[i:j], ' ')
if r.c.app.config.EnableIPValidation {
// Skip validation if IP is clearly not IPv4/IPv6; otherwise, validate without allocations
if (!v6 && !v4) || (v6 && !utils.IsIPv6(s)) || (v4 && !utils.IsIPv4(s)) {
continue
}
}
ipsFound = append(ipsFound, s)
}
return ipsFound
}
// extractIPFromHeader will attempt to pull the real client IP from the given header when IP validation is enabled.
// currently, it will return the first valid IP address in header.
// when IP validation is disabled, it will simply return the value of the header without any inspection.
// Implementation is almost the same as in extractIPsFromHeader, but without allocation of []string.
func (r *DefaultReq) extractIPFromHeader(header string) string {
app := r.c.app
if app.config.EnableIPValidation {
headerValue := r.Get(header)
i := 0
j := -1
for {
var v4, v6 bool
// Manually splitting string without allocating slice, working with parts directly
i, j = j+1, j+2
if j > len(headerValue) {
break
}
for j < len(headerValue) && headerValue[j] != ',' {
switch headerValue[j] {
case ':':
v6 = true
case '.':
v4 = true
default:
// do nothing
}
j++
}
for i < j && headerValue[i] == ' ' {
i++
}
s := utils.TrimRight(headerValue[i:j], ' ')
if app.config.EnableIPValidation {
if (!v6 && !v4) || (v6 && !utils.IsIPv6(s)) || (v4 && !utils.IsIPv4(s)) {
continue
}
}
return s
}
if ip := r.c.fasthttp.RemoteIP(); ip != nil {
return ip.String()
}
return ""
}
// default behavior if IP validation is not enabled is just to return whatever value is
// in the proxy header. Even if it is empty or invalid
return r.Get(app.config.ProxyHeader)
}
// IPs returns a string slice of IP addresses specified in the X-Forwarded-For request header.
// When IP validation is enabled, only valid IPs are returned.
func (r *DefaultReq) IPs() []string {
return r.extractIPsFromHeader(HeaderXForwardedFor)
}
// Is returns the matching content type,
// if the incoming request's Content-Type HTTP header field matches the MIME type specified by the type parameter
func (r *DefaultReq) Is(extension string) bool {
extensionHeader := utils.GetMIME(extension)
if extensionHeader == "" {
return false
}
ct := r.c.app.toString(r.c.fasthttp.Request.Header.ContentType())
if i := strings.IndexByte(ct, ';'); i != -1 {
ct = ct[:i]
}
ct = utils.TrimSpace(ct)
return utils.EqualFold(ct, extensionHeader)
}
// Locals makes it possible to pass any values under keys scoped to the request
// and therefore available to all following routes that match the request.
//
// All the values are removed from ctx after returning from the top
// RequestHandler. Additionally, Close method is called on each value
// implementing io.Closer before removing the value from ctx.
func (r *DefaultReq) Locals(key any, value ...any) any {
if len(value) == 0 {
return r.c.fasthttp.UserValue(key)
}
r.c.fasthttp.SetUserValue(key, value[0])
return value[0]
}
// Locals function utilizing Go's generics feature.
// This function allows for manipulating and retrieving local values within a
// request context with a more specific data type.
//
// All the values are removed from ctx after returning from the top
// RequestHandler. Additionally, Close method is called on each value
// implementing io.Closer before removing the value from ctx.
func Locals[V any](c Ctx, key any, value ...V) V {
var v V
var ok bool
if len(value) == 0 {
v, ok = c.Locals(key).(V)
} else {
v, ok = c.Locals(key, value[0]).(V)
}
if !ok {
return v // return zero of type V
}
return v
}
// Method returns the HTTP request method for the context, optionally overridden by the provided argument.
// If no override is given or if the provided override is not a valid HTTP method, it returns the current method from the context.
// Otherwise, it updates the context's method and returns the overridden method as a string.
func (r *DefaultReq) Method(override ...string) string {
app := r.c.app
if len(override) == 0 {
// Nothing to override, just return current method from context
return app.method(r.c.methodInt)
}
method := utilsstrings.ToUpper(override[0])
methodInt := app.methodInt(method)
if methodInt == -1 {
// Provided override does not valid HTTP method, no override, return current method
return app.method(r.c.methodInt)
}
r.c.methodInt = methodInt
return method
}
// MultipartForm parse form entries from binary.
// This returns a map[string][]string, so given a key, the value will be a string slice.
func (r *DefaultReq) MultipartForm() (*multipart.Form, error) {
return r.c.fasthttp.MultipartFormWithLimit(r.c.app.config.BodyLimit)
}
// OriginalURL contains the original request URL.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting to use the value outside the Handler.
func (r *DefaultReq) OriginalURL() string {
return r.c.app.toString(r.c.fasthttp.Request.Header.RequestURI())
}
// Params is used to get the route parameters.
// Defaults to empty string "" if the param doesn't exist.
// If a default value is given, it will return that value if the param doesn't exist.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting to use the value outside the Handler.
func (r *DefaultReq) Params(key string, defaultValue ...string) string {
if key == "*" || key == "+" {
key += "1"
}
app := r.c.app
route := r.c.Route()
values := &r.c.values
for i := range route.Params {
if len(key) != len(route.Params[i]) {
continue
}
if route.Params[i] == key || (!app.config.CaseSensitive && utils.EqualFold(route.Params[i], key)) {
// if there is no value for the key
if len(values) <= i || values[i] == "" {
break
}
val := values[i]
return r.c.app.GetString(val)
}
}
return defaultString("", defaultValue)
}
// Params is used to get the route parameters.
// This function is generic and can handle different route parameters type values.
// If the generic type cannot be matched to a supported type, the function
// returns the default value (if provided) or the zero value of type V.
//
// Example:
//
// http://example.com/user/:user -> http://example.com/user/john
// Params[string](c, "user") -> returns john
//
// http://example.com/id/:id -> http://example.com/user/114
// Params[int](c, "id") -> returns 114 as integer.
//
// http://example.com/id/:number -> http://example.com/id/john
// Params[int](c, "number", 0) -> returns 0 because can't parse 'john' as integer.
func Params[V GenericType](c Ctx, key string, defaultValue ...V) V {
v, err := genericParseType[V](c.Params(key))
if err != nil && len(defaultValue) > 0 {
return defaultValue[0]
}
return v
}
// Scheme contains the request protocol string: http or https for TLS requests.
// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.
func (r *DefaultReq) Scheme() string {
ctx := r.c.fasthttp
if ctx.IsTLS() {
return schemeHTTPS
}
if !r.IsProxyTrusted() {
return schemeHTTP
}
app := r.c.app
scheme := schemeHTTP
const lenXHeaderName = 12
for key, val := range ctx.Request.Header.All() {
if len(key) < lenXHeaderName {
continue // Neither "X-Forwarded-" nor "X-Url-Scheme"
}
switch {
case utils.EqualFold(key[:len(xForwardedPrefix)], xForwardedPrefix):
if utils.EqualFold(key, xForwardedProtoBytes) ||
utils.EqualFold(key, xForwardedProtocolBytes) {
v := app.toString(val)
if before, _, found := strings.Cut(v, ","); found {
scheme = utils.TrimSpace(before)
} else {
scheme = utils.TrimSpace(v)
}
} else if utils.EqualFold(key, xForwardedSslBytes) && utils.EqualFold(val, onBytes) {
scheme = schemeHTTPS
}
case utils.EqualFold(key, xURLSchemeBytes):
scheme = utils.TrimSpace(app.toString(val))
default:
continue
}
}
return utilsstrings.ToLower(utils.TrimSpace(scheme))
}
// Protocol returns the HTTP protocol of request: HTTP/1.1 and HTTP/2.
func (r *DefaultReq) Protocol() string {
return r.c.app.toString(r.c.fasthttp.Request.Header.Protocol())
}
// Query returns the query string parameter in the url.
// Defaults to empty string "" if the query doesn't exist.
// If a default value is given, it will return that value if the query doesn't exist.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting to use the value outside the Handler.
func (r *DefaultReq) Query(key string, defaultValue ...string) string {
return Query(r.c, key, defaultValue...)
}
// Queries returns a map of query parameters and their values.
//
// GET /?name=alex&wanna_cake=2&id=
// Queries()["name"] == "alex"
// Queries()["wanna_cake"] == "2"
// Queries()["id"] == ""
//
// GET /?field1=value1&field1=value2&field2=value3
// Queries()["field1"] == "value2"
// Queries()["field2"] == "value3"
//
// GET /?list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3
// Queries()["list_a"] == "3"
// Queries()["list_b[]"] == "3"
// Queries()["list_c"] == "1,2,3"
//
// GET /api/search?filters.author.name=John&filters.category.name=Technology&filters[customer][name]=Alice&filters[status]=pending
// Queries()["filters.author.name"] == "John"
// Queries()["filters.category.name"] == "Technology"
// Queries()["filters[customer][name]"] == "Alice"
// Queries()["filters[status]"] == "pending"
func (r *DefaultReq) Queries() map[string]string {
app := r.c.app
queryArgs := r.c.fasthttp.QueryArgs()
m := make(map[string]string, queryArgs.Len())
for key, value := range queryArgs.All() {
m[app.toString(key)] = app.toString(value)
}
return m
}
// Query Retrieves the value of a query parameter from the request's URI.
// The function is generic and can handle query parameter values of different types.
// It takes the following parameters:
// - c: The context object representing the current request.
// - key: The name of the query parameter.
// - defaultValue: (Optional) The default value to return if the query parameter is not found or cannot be parsed.
// The function performs the following steps:
// 1. Type-asserts the context object to *DefaultCtx.
// 2. Retrieves the raw query parameter value from the request's URI.
// 3. Parses the raw value into the appropriate type based on the generic type parameter V.
// If parsing fails, the function checks if a default value is provided. If so, it returns the default value.
// 4. Returns the parsed value.
//
// If the generic type cannot be matched to a supported type, the function returns the default value (if provided) or the zero value of type V.
//
// Example usage:
//
// GET /?search=john&age=8
// name := Query[string](c, "search") // Returns "john"
// age := Query[int](c, "age") // Returns 8
// unknown := Query[string](c, "unknown", "default") // Returns "default" since the query parameter "unknown" is not found
func Query[V GenericType](c Ctx, key string, defaultValue ...V) V {
q := c.App().toString(c.RequestCtx().QueryArgs().Peek(key))
v, err := genericParseType[V](q)
if err != nil && len(defaultValue) > 0 {
return defaultValue[0]
}
return v
}
// Range returns a struct containing the type and a slice of ranges.
func (r *DefaultReq) Range(size int64) (Range, error) {
var (
rangeData Range
ranges string
)
rangeStr := utils.TrimSpace(r.Get(HeaderRange))
maxRanges := r.c.app.config.MaxRanges
const maxRangePrealloc = 8
prealloc := min(maxRanges, maxRangePrealloc)
if prealloc > 0 {
rangeData.Ranges = make([]RangeSet, 0, prealloc)
}
parseBound := func(value string) (int64, error) {
parsed, err := utils.ParseUint(value)
if err != nil {
return 0, fmt.Errorf("parse range bound %q: %w", value, err)
}
if parsed > (math.MaxUint64 >> 1) {
return 0, ErrRangeMalformed
}
return int64(parsed), nil
}
before, after, found := strings.Cut(rangeStr, "=")
if !found || strings.IndexByte(after, '=') >= 0 {
return rangeData, ErrRangeMalformed
}
rangeData.Type = utilsstrings.ToLower(utils.TrimSpace(before))
if rangeData.Type != "bytes" {
return rangeData, ErrRangeMalformed
}
ranges = utils.TrimSpace(after)
var (
singleRange string
moreRanges = ranges
rangeCount int
)
for moreRanges != "" {
rangeCount++
if rangeCount > maxRanges {
r.c.DefaultRes.Status(StatusRequestedRangeNotSatisfiable)
r.c.DefaultRes.Set(HeaderContentRange, "bytes */"+utils.FormatInt(size)) //nolint:staticcheck // It is fine to ignore the static check
return rangeData, ErrRangeTooLarge
}
singleRange = moreRanges
if i := strings.IndexByte(moreRanges, ','); i >= 0 {
singleRange = moreRanges[:i]
moreRanges = utils.TrimSpace(moreRanges[i+1:])
} else {
moreRanges = ""
}
singleRange = utils.TrimSpace(singleRange)
var (
startStr, endStr string
i int
)
if i = strings.IndexByte(singleRange, '-'); i == -1 {
return rangeData, ErrRangeMalformed
}
startStr = utils.TrimSpace(singleRange[:i])
endStr = utils.TrimSpace(singleRange[i+1:])
start, startErr := parseBound(startStr)
end, endErr := parseBound(endStr)
if errors.Is(startErr, ErrRangeMalformed) || errors.Is(endErr, ErrRangeMalformed) {
return rangeData, ErrRangeMalformed
}
if startErr != nil { // -nnn
start = max(size-end, 0)
end = size - 1
} else if endErr != nil { // nnn-
end = size - 1
}
if end > size-1 { // limit last-byte-pos to current length
end = size - 1
}
if start > end || start < 0 {
continue
}
rangeData.Ranges = append(rangeData.Ranges, RangeSet{
Start: start,
End: end,
})
}
if len(rangeData.Ranges) < 1 {
r.c.DefaultRes.Status(StatusRequestedRangeNotSatisfiable)
r.c.DefaultRes.Set(HeaderContentRange, "bytes */"+utils.FormatInt(size)) //nolint:staticcheck // It is fine to ignore the static check
return rangeData, ErrRequestedRangeNotSatisfiable
}
return rangeData, nil
}
// Route returns the matched Route struct.
func (r *DefaultReq) Route() *Route {
return r.c.Route()
}
// Subdomains returns a slice of subdomains from the host, excluding the last `offset` components.
// If the offset is negative or exceeds the number of subdomains, an empty slice is returned.
// If the offset is zero every label (no trimming) is returned.
func (r *DefaultReq) Subdomains(offset ...int) []string {
o := 2
if len(offset) > 0 {
o = offset[0]
}
// Negative offset, return nothing.
if o < 0 {
return []string{}
}
// Normalize host according to RFC 3986
host := r.Hostname()
// Trim the trailing dot of a fully-qualified domain
if strings.HasSuffix(host, ".") {
host = utils.TrimRight(host, '.')
}
host = utilsstrings.ToLower(host)
// Decode punycode labels only when necessary
if strings.Contains(host, "xn--") {
if u, err := idna.Lookup.ToUnicode(host); err == nil {
host = utilsstrings.ToLower(u)
}
}
// Return nothing for IP addresses
ip := host
if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
ip = ip[1 : len(ip)-1]
}
if utils.IsIPv4(ip) || utils.IsIPv6(ip) {
return []string{}
}
// Use stack-allocated array for typical domain names (up to 8 labels)
// This avoids heap allocation for most common cases
var partsBuf [8]string
parts := partsBuf[:0]
for part := range strings.SplitSeq(host, ".") {
parts = append(parts, part)
}
// offset == 0, caller wants everything.
if o == 0 {
// Need to return a copy since partsBuf is on the stack
result := make([]string, len(parts))
copy(result, parts)
return result
}
// If we trim away the whole slice (or more), nothing remains.
if o >= len(parts) {
return []string{}
}
// Return a heap-allocated copy of the relevant portion
result := make([]string, len(parts)-o)
copy(result, parts[:len(parts)-o])
return result
}
// Stale returns the inverse of Fresh, indicating if the client's cached response is considered stale.
func (r *DefaultReq) Stale() bool {
return !r.Fresh()
}
// IsProxyTrusted checks trustworthiness of remote ip.
// If Config.TrustProxy false, it returns false.
// IsProxyTrusted can check remote ip by proxy ranges and ip map.
func (r *DefaultReq) IsProxyTrusted() bool {
config := r.c.app.config
if !config.TrustProxy {
return false
}
remoteAddr := r.c.fasthttp.RemoteAddr()
switch remoteAddr.(type) {
case *net.UnixAddr:
return config.TrustProxyConfig.UnixSocket
case *net.TCPAddr, *net.UDPAddr:
// Keep existing RemoteIP/IP-map/CIDR checks for TCP/UDP paths as-is.
default:
// Unknown address type: do not trust by default.
return false
}
ip := r.c.fasthttp.RemoteIP()
if ip == nil {
return false
}
if (config.TrustProxyConfig.Loopback && ip.IsLoopback()) ||
(config.TrustProxyConfig.Private && ip.IsPrivate()) ||
(config.TrustProxyConfig.LinkLocal && ip.IsLinkLocalUnicast()) {
return true
}
if _, trusted := config.TrustProxyConfig.ips[ip.String()]; trusted {
return true
}
for _, ipNet := range config.TrustProxyConfig.ranges {
if ipNet.Contains(ip) {
return true
}
}
return false
}
// IsFromLocal will return true if request came from local.
func (r *DefaultReq) IsFromLocal() bool {
// Unix sockets are inherently local - only processes on the same host can connect.
remoteAddr := r.c.fasthttp.RemoteAddr()
if _, ok := remoteAddr.(*net.UnixAddr); ok {
return true
}
if ip := r.c.fasthttp.RemoteIP(); ip != nil {
return ip.IsLoopback()
}
return false
}
// Release is a method to reset Req fields when to use ReleaseCtx()
func (r *DefaultReq) release() {
r.c = nil
}
func (r *DefaultReq) getBody() []byte {
return r.c.app.GetBytes(r.c.fasthttp.Request.Body())
}