2024-09-17 01:01:42 +00:00
|
|
|
package fasthttp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"path/filepath"
|
|
|
|
"strconv"
|
|
|
|
"sync"
|
|
|
|
)
|
|
|
|
|
|
|
|
// AcquireURI returns an empty URI instance from the pool.
|
|
|
|
//
|
|
|
|
// Release the URI with ReleaseURI after the URI is no longer needed.
|
|
|
|
// This allows reducing GC load.
|
|
|
|
func AcquireURI() *URI {
|
|
|
|
return uriPool.Get().(*URI)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReleaseURI releases the URI acquired via AcquireURI.
|
|
|
|
//
|
|
|
|
// The released URI mustn't be used after releasing it, otherwise data races
|
|
|
|
// may occur.
|
|
|
|
func ReleaseURI(u *URI) {
|
|
|
|
u.Reset()
|
|
|
|
uriPool.Put(u)
|
|
|
|
}
|
|
|
|
|
|
|
|
var uriPool = &sync.Pool{
|
2024-09-26 01:59:44 +00:00
|
|
|
New: func() any {
|
2024-09-17 01:01:42 +00:00
|
|
|
return &URI{}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// URI represents URI :) .
|
|
|
|
//
|
|
|
|
// It is forbidden copying URI instances. Create new instance and use CopyTo
|
|
|
|
// instead.
|
|
|
|
//
|
|
|
|
// URI instance MUST NOT be used from concurrently running goroutines.
|
|
|
|
type URI struct {
|
|
|
|
noCopy noCopy
|
|
|
|
|
2024-09-26 01:59:44 +00:00
|
|
|
queryArgs Args
|
|
|
|
|
2024-09-17 01:01:42 +00:00
|
|
|
pathOriginal []byte
|
|
|
|
scheme []byte
|
|
|
|
path []byte
|
|
|
|
queryString []byte
|
|
|
|
hash []byte
|
|
|
|
host []byte
|
|
|
|
|
2024-09-26 01:59:44 +00:00
|
|
|
fullURI []byte
|
|
|
|
requestURI []byte
|
|
|
|
|
|
|
|
username []byte
|
|
|
|
password []byte
|
2024-09-17 01:01:42 +00:00
|
|
|
parsedQueryArgs bool
|
|
|
|
|
2024-09-26 01:59:44 +00:00
|
|
|
// Path values are sent as-is without normalization.
|
2024-09-17 01:01:42 +00:00
|
|
|
//
|
|
|
|
// Disabled path normalization may be useful for proxying incoming requests
|
|
|
|
// to servers that are expecting paths to be forwarded as-is.
|
|
|
|
//
|
|
|
|
// By default path values are normalized, i.e.
|
|
|
|
// extra slashes are removed, special characters are encoded.
|
|
|
|
DisablePathNormalizing bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// CopyTo copies uri contents to dst.
|
|
|
|
func (u *URI) CopyTo(dst *URI) {
|
|
|
|
dst.Reset()
|
|
|
|
dst.pathOriginal = append(dst.pathOriginal, u.pathOriginal...)
|
|
|
|
dst.scheme = append(dst.scheme, u.scheme...)
|
|
|
|
dst.path = append(dst.path, u.path...)
|
|
|
|
dst.queryString = append(dst.queryString, u.queryString...)
|
|
|
|
dst.hash = append(dst.hash, u.hash...)
|
|
|
|
dst.host = append(dst.host, u.host...)
|
|
|
|
dst.username = append(dst.username, u.username...)
|
|
|
|
dst.password = append(dst.password, u.password...)
|
|
|
|
|
|
|
|
u.queryArgs.CopyTo(&dst.queryArgs)
|
|
|
|
dst.parsedQueryArgs = u.parsedQueryArgs
|
|
|
|
dst.DisablePathNormalizing = u.DisablePathNormalizing
|
|
|
|
|
|
|
|
// fullURI and requestURI shouldn't be copied, since they are created
|
|
|
|
// from scratch on each FullURI() and RequestURI() call.
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hash returns URI hash, i.e. qwe of http://aaa.com/foo/bar?baz=123#qwe .
|
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) Hash() []byte {
|
|
|
|
return u.hash
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetHash sets URI hash.
|
|
|
|
func (u *URI) SetHash(hash string) {
|
|
|
|
u.hash = append(u.hash[:0], hash...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetHashBytes sets URI hash.
|
|
|
|
func (u *URI) SetHashBytes(hash []byte) {
|
|
|
|
u.hash = append(u.hash[:0], hash...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Username returns URI username
|
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) Username() []byte {
|
|
|
|
return u.username
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetUsername sets URI username.
|
|
|
|
func (u *URI) SetUsername(username string) {
|
|
|
|
u.username = append(u.username[:0], username...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetUsernameBytes sets URI username.
|
|
|
|
func (u *URI) SetUsernameBytes(username []byte) {
|
|
|
|
u.username = append(u.username[:0], username...)
|
|
|
|
}
|
|
|
|
|
2024-09-26 01:59:44 +00:00
|
|
|
// Password returns URI password.
|
2024-09-17 01:01:42 +00:00
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) Password() []byte {
|
|
|
|
return u.password
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPassword sets URI password.
|
|
|
|
func (u *URI) SetPassword(password string) {
|
|
|
|
u.password = append(u.password[:0], password...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPasswordBytes sets URI password.
|
|
|
|
func (u *URI) SetPasswordBytes(password []byte) {
|
|
|
|
u.password = append(u.password[:0], password...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// QueryString returns URI query string,
|
|
|
|
// i.e. baz=123 of http://aaa.com/foo/bar?baz=123#qwe .
|
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) QueryString() []byte {
|
|
|
|
return u.queryString
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetQueryString sets URI query string.
|
|
|
|
func (u *URI) SetQueryString(queryString string) {
|
|
|
|
u.queryString = append(u.queryString[:0], queryString...)
|
|
|
|
u.parsedQueryArgs = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetQueryStringBytes sets URI query string.
|
|
|
|
func (u *URI) SetQueryStringBytes(queryString []byte) {
|
|
|
|
u.queryString = append(u.queryString[:0], queryString...)
|
|
|
|
u.parsedQueryArgs = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Path returns URI path, i.e. /foo/bar of http://aaa.com/foo/bar?baz=123#qwe .
|
|
|
|
//
|
|
|
|
// The returned path is always urldecoded and normalized,
|
|
|
|
// i.e. '//f%20obar/baz/../zzz' becomes '/f obar/zzz'.
|
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) Path() []byte {
|
|
|
|
path := u.path
|
|
|
|
if len(path) == 0 {
|
|
|
|
path = strSlash
|
|
|
|
}
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPath sets URI path.
|
|
|
|
func (u *URI) SetPath(path string) {
|
|
|
|
u.pathOriginal = append(u.pathOriginal[:0], path...)
|
|
|
|
u.path = normalizePath(u.path, u.pathOriginal)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPathBytes sets URI path.
|
|
|
|
func (u *URI) SetPathBytes(path []byte) {
|
|
|
|
u.pathOriginal = append(u.pathOriginal[:0], path...)
|
|
|
|
u.path = normalizePath(u.path, u.pathOriginal)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PathOriginal returns the original path from requestURI passed to URI.Parse().
|
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) PathOriginal() []byte {
|
|
|
|
return u.pathOriginal
|
|
|
|
}
|
|
|
|
|
|
|
|
// Scheme returns URI scheme, i.e. http of http://aaa.com/foo/bar?baz=123#qwe .
|
|
|
|
//
|
|
|
|
// Returned scheme is always lowercased.
|
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) Scheme() []byte {
|
|
|
|
scheme := u.scheme
|
|
|
|
if len(scheme) == 0 {
|
|
|
|
scheme = strHTTP
|
|
|
|
}
|
|
|
|
return scheme
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetScheme sets URI scheme, i.e. http, https, ftp, etc.
|
|
|
|
func (u *URI) SetScheme(scheme string) {
|
|
|
|
u.scheme = append(u.scheme[:0], scheme...)
|
|
|
|
lowercaseBytes(u.scheme)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetSchemeBytes sets URI scheme, i.e. http, https, ftp, etc.
|
|
|
|
func (u *URI) SetSchemeBytes(scheme []byte) {
|
|
|
|
u.scheme = append(u.scheme[:0], scheme...)
|
|
|
|
lowercaseBytes(u.scheme)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *URI) isHTTPS() bool {
|
|
|
|
return bytes.Equal(u.scheme, strHTTPS)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *URI) isHTTP() bool {
|
|
|
|
return len(u.scheme) == 0 || bytes.Equal(u.scheme, strHTTP)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset clears uri.
|
|
|
|
func (u *URI) Reset() {
|
|
|
|
u.pathOriginal = u.pathOriginal[:0]
|
|
|
|
u.scheme = u.scheme[:0]
|
|
|
|
u.path = u.path[:0]
|
|
|
|
u.queryString = u.queryString[:0]
|
|
|
|
u.hash = u.hash[:0]
|
|
|
|
u.username = u.username[:0]
|
|
|
|
u.password = u.password[:0]
|
|
|
|
|
|
|
|
u.host = u.host[:0]
|
|
|
|
u.queryArgs.Reset()
|
|
|
|
u.parsedQueryArgs = false
|
|
|
|
u.DisablePathNormalizing = false
|
|
|
|
|
|
|
|
// There is no need in u.fullURI = u.fullURI[:0], since full uri
|
|
|
|
// is calculated on each call to FullURI().
|
|
|
|
|
|
|
|
// There is no need in u.requestURI = u.requestURI[:0], since requestURI
|
|
|
|
// is calculated on each call to RequestURI().
|
|
|
|
}
|
|
|
|
|
|
|
|
// Host returns host part, i.e. aaa.com of http://aaa.com/foo/bar?baz=123#qwe .
|
|
|
|
//
|
|
|
|
// Host is always lowercased.
|
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) Host() []byte {
|
|
|
|
return u.host
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetHost sets host for the uri.
|
|
|
|
func (u *URI) SetHost(host string) {
|
|
|
|
u.host = append(u.host[:0], host...)
|
|
|
|
lowercaseBytes(u.host)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetHostBytes sets host for the uri.
|
|
|
|
func (u *URI) SetHostBytes(host []byte) {
|
|
|
|
u.host = append(u.host[:0], host...)
|
|
|
|
lowercaseBytes(u.host)
|
|
|
|
}
|
|
|
|
|
|
|
|
var ErrorInvalidURI = errors.New("invalid uri")
|
|
|
|
|
|
|
|
// Parse initializes URI from the given host and uri.
|
|
|
|
//
|
|
|
|
// host may be nil. In this case uri must contain fully qualified uri,
|
|
|
|
// i.e. with scheme and host. http is assumed if scheme is omitted.
|
|
|
|
//
|
|
|
|
// uri may contain e.g. RequestURI without scheme and host if host is non-empty.
|
|
|
|
func (u *URI) Parse(host, uri []byte) error {
|
|
|
|
return u.parse(host, uri, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *URI) parse(host, uri []byte, isTLS bool) error {
|
|
|
|
u.Reset()
|
|
|
|
|
|
|
|
if stringContainsCTLByte(uri) {
|
|
|
|
return ErrorInvalidURI
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(host) == 0 || bytes.Contains(uri, strColonSlashSlash) {
|
|
|
|
scheme, newHost, newURI := splitHostURI(host, uri)
|
|
|
|
u.SetSchemeBytes(scheme)
|
|
|
|
host = newHost
|
|
|
|
uri = newURI
|
|
|
|
}
|
|
|
|
|
|
|
|
if isTLS {
|
|
|
|
u.SetSchemeBytes(strHTTPS)
|
|
|
|
}
|
|
|
|
|
|
|
|
if n := bytes.IndexByte(host, '@'); n >= 0 {
|
|
|
|
auth := host[:n]
|
|
|
|
host = host[n+1:]
|
|
|
|
|
|
|
|
if n := bytes.IndexByte(auth, ':'); n >= 0 {
|
|
|
|
u.username = append(u.username[:0], auth[:n]...)
|
|
|
|
u.password = append(u.password[:0], auth[n+1:]...)
|
|
|
|
} else {
|
|
|
|
u.username = append(u.username[:0], auth...)
|
|
|
|
u.password = u.password[:0]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
u.host = append(u.host, host...)
|
|
|
|
if parsedHost, err := parseHost(u.host); err != nil {
|
|
|
|
return err
|
|
|
|
} else {
|
|
|
|
u.host = parsedHost
|
|
|
|
}
|
|
|
|
lowercaseBytes(u.host)
|
|
|
|
|
|
|
|
b := uri
|
|
|
|
queryIndex := bytes.IndexByte(b, '?')
|
|
|
|
fragmentIndex := bytes.IndexByte(b, '#')
|
|
|
|
// Ignore query in fragment part
|
|
|
|
if fragmentIndex >= 0 && queryIndex > fragmentIndex {
|
|
|
|
queryIndex = -1
|
|
|
|
}
|
|
|
|
|
|
|
|
if queryIndex < 0 && fragmentIndex < 0 {
|
|
|
|
u.pathOriginal = append(u.pathOriginal, b...)
|
|
|
|
u.path = normalizePath(u.path, u.pathOriginal)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if queryIndex >= 0 {
|
|
|
|
// Path is everything up to the start of the query
|
|
|
|
u.pathOriginal = append(u.pathOriginal, b[:queryIndex]...)
|
|
|
|
u.path = normalizePath(u.path, u.pathOriginal)
|
|
|
|
|
|
|
|
if fragmentIndex < 0 {
|
|
|
|
u.queryString = append(u.queryString, b[queryIndex+1:]...)
|
|
|
|
} else {
|
|
|
|
u.queryString = append(u.queryString, b[queryIndex+1:fragmentIndex]...)
|
|
|
|
u.hash = append(u.hash, b[fragmentIndex+1:]...)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// fragmentIndex >= 0 && queryIndex < 0
|
|
|
|
// Path is up to the start of fragment
|
|
|
|
u.pathOriginal = append(u.pathOriginal, b[:fragmentIndex]...)
|
|
|
|
u.path = normalizePath(u.path, u.pathOriginal)
|
|
|
|
u.hash = append(u.hash, b[fragmentIndex+1:]...)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseHost parses host as an authority without user
|
|
|
|
// information. That is, as host[:port].
|
|
|
|
//
|
|
|
|
// Based on https://github.com/golang/go/blob/8ac5cbe05d61df0a7a7c9a38ff33305d4dcfea32/src/net/url/url.go#L619
|
|
|
|
//
|
|
|
|
// The host is parsed and unescaped in place overwriting the contents of the host parameter.
|
|
|
|
func parseHost(host []byte) ([]byte, error) {
|
|
|
|
if len(host) > 0 && host[0] == '[' {
|
|
|
|
// Parse an IP-Literal in RFC 3986 and RFC 6874.
|
|
|
|
// E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80".
|
|
|
|
i := bytes.LastIndexByte(host, ']')
|
|
|
|
if i < 0 {
|
|
|
|
return nil, errors.New("missing ']' in host")
|
|
|
|
}
|
|
|
|
colonPort := host[i+1:]
|
|
|
|
if !validOptionalPort(colonPort) {
|
|
|
|
return nil, fmt.Errorf("invalid port %q after host", colonPort)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RFC 6874 defines that %25 (%-encoded percent) introduces
|
|
|
|
// the zone identifier, and the zone identifier can use basically
|
|
|
|
// any %-encoding it likes. That's different from the host, which
|
|
|
|
// can only %-encode non-ASCII bytes.
|
|
|
|
// We do impose some restrictions on the zone, to avoid stupidity
|
|
|
|
// like newlines.
|
|
|
|
zone := bytes.Index(host[:i], []byte("%25"))
|
|
|
|
if zone >= 0 {
|
|
|
|
host1, err := unescape(host[:zone], encodeHost)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
host2, err := unescape(host[zone:i], encodeZone)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
host3, err := unescape(host[i:], encodeHost)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return append(host1, append(host2, host3...)...), nil
|
|
|
|
}
|
|
|
|
} else if i := bytes.LastIndexByte(host, ':'); i != -1 {
|
|
|
|
colonPort := host[i:]
|
|
|
|
if !validOptionalPort(colonPort) {
|
|
|
|
return nil, fmt.Errorf("invalid port %q after host", colonPort)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
if host, err = unescape(host, encodeHost); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return host, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type encoding int
|
|
|
|
|
|
|
|
const (
|
|
|
|
encodeHost encoding = 1 + iota
|
|
|
|
encodeZone
|
|
|
|
)
|
|
|
|
|
|
|
|
type EscapeError string
|
|
|
|
|
|
|
|
func (e EscapeError) Error() string {
|
|
|
|
return "invalid URL escape " + strconv.Quote(string(e))
|
|
|
|
}
|
|
|
|
|
|
|
|
type InvalidHostError string
|
|
|
|
|
|
|
|
func (e InvalidHostError) Error() string {
|
|
|
|
return "invalid character " + strconv.Quote(string(e)) + " in host name"
|
|
|
|
}
|
|
|
|
|
|
|
|
// unescape unescapes a string; the mode specifies
|
|
|
|
// which section of the URL string is being unescaped.
|
|
|
|
//
|
|
|
|
// Based on https://github.com/golang/go/blob/8ac5cbe05d61df0a7a7c9a38ff33305d4dcfea32/src/net/url/url.go#L199
|
|
|
|
//
|
|
|
|
// Unescapes in place overwriting the contents of s and returning it.
|
|
|
|
func unescape(s []byte, mode encoding) ([]byte, error) {
|
|
|
|
// Count %, check that they're well-formed.
|
|
|
|
n := 0
|
|
|
|
for i := 0; i < len(s); {
|
|
|
|
switch s[i] {
|
|
|
|
case '%':
|
|
|
|
n++
|
|
|
|
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
|
|
|
|
s = s[i:]
|
|
|
|
if len(s) > 3 {
|
|
|
|
s = s[:3]
|
|
|
|
}
|
|
|
|
return nil, EscapeError(s)
|
|
|
|
}
|
|
|
|
// Per https://tools.ietf.org/html/rfc3986#page-21
|
|
|
|
// in the host component %-encoding can only be used
|
|
|
|
// for non-ASCII bytes.
|
|
|
|
// But https://tools.ietf.org/html/rfc6874#section-2
|
|
|
|
// introduces %25 being allowed to escape a percent sign
|
|
|
|
// in IPv6 scoped-address literals. Yay.
|
|
|
|
if mode == encodeHost && unhex(s[i+1]) < 8 && !bytes.Equal(s[i:i+3], []byte("%25")) {
|
|
|
|
return nil, EscapeError(s[i : i+3])
|
|
|
|
}
|
|
|
|
if mode == encodeZone {
|
|
|
|
// RFC 6874 says basically "anything goes" for zone identifiers
|
|
|
|
// and that even non-ASCII can be redundantly escaped,
|
|
|
|
// but it seems prudent to restrict %-escaped bytes here to those
|
|
|
|
// that are valid host name bytes in their unescaped form.
|
|
|
|
// That is, you can use escaping in the zone identifier but not
|
|
|
|
// to introduce bytes you couldn't just write directly.
|
|
|
|
// But Windows puts spaces here! Yay.
|
|
|
|
v := unhex(s[i+1])<<4 | unhex(s[i+2])
|
|
|
|
if !bytes.Equal(s[i:i+3], []byte("%25")) && v != ' ' && shouldEscape(v, encodeHost) {
|
|
|
|
return nil, EscapeError(s[i : i+3])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
i += 3
|
|
|
|
default:
|
|
|
|
if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) {
|
|
|
|
return nil, InvalidHostError(s[i : i+1])
|
|
|
|
}
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if n == 0 {
|
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
t := s[:0]
|
|
|
|
for i := 0; i < len(s); i++ {
|
|
|
|
switch s[i] {
|
|
|
|
case '%':
|
|
|
|
t = append(t, unhex(s[i+1])<<4|unhex(s[i+2]))
|
|
|
|
i += 2
|
|
|
|
default:
|
|
|
|
t = append(t, s[i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return t, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return true if the specified character should be escaped when
|
|
|
|
// appearing in a URL string, according to RFC 3986.
|
|
|
|
//
|
|
|
|
// Please be informed that for now shouldEscape does not check all
|
|
|
|
// reserved characters correctly. See https://github.com/golang/go/issues/5684.
|
|
|
|
//
|
|
|
|
// Based on https://github.com/golang/go/blob/8ac5cbe05d61df0a7a7c9a38ff33305d4dcfea32/src/net/url/url.go#L100
|
|
|
|
func shouldEscape(c byte, mode encoding) bool {
|
|
|
|
// §2.3 Unreserved characters (alphanum)
|
|
|
|
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if mode == encodeHost || mode == encodeZone {
|
|
|
|
// §3.2.2 Host allows
|
|
|
|
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
|
|
|
|
// as part of reg-name.
|
|
|
|
// We add : because we include :port as part of host.
|
|
|
|
// We add [ ] because we include [ipv6]:port as part of host.
|
|
|
|
// We add < > because they're the only characters left that
|
|
|
|
// we could possibly allow, and Parse will reject them if we
|
|
|
|
// escape them (because hosts can't use %-encoding for
|
|
|
|
// ASCII bytes).
|
|
|
|
switch c {
|
|
|
|
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"':
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if c == '-' || c == '_' || c == '.' || c == '~' { // §2.3 Unreserved characters (mark)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Everything else must be escaped.
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func ishex(c byte) bool {
|
|
|
|
return ('0' <= c && c <= '9') ||
|
|
|
|
('a' <= c && c <= 'f') ||
|
|
|
|
('A' <= c && c <= 'F')
|
|
|
|
}
|
|
|
|
|
|
|
|
func unhex(c byte) byte {
|
|
|
|
switch {
|
|
|
|
case '0' <= c && c <= '9':
|
|
|
|
return c - '0'
|
|
|
|
case 'a' <= c && c <= 'f':
|
|
|
|
return c - 'a' + 10
|
|
|
|
case 'A' <= c && c <= 'F':
|
|
|
|
return c - 'A' + 10
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// validOptionalPort reports whether port is either an empty string
|
2024-09-26 01:59:44 +00:00
|
|
|
// or matches /^:\d*$/.
|
2024-09-17 01:01:42 +00:00
|
|
|
func validOptionalPort(port []byte) bool {
|
|
|
|
if len(port) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if port[0] != ':' {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, b := range port[1:] {
|
|
|
|
if b < '0' || b > '9' {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func normalizePath(dst, src []byte) []byte {
|
|
|
|
dst = dst[:0]
|
|
|
|
dst = addLeadingSlash(dst, src)
|
|
|
|
dst = decodeArgAppendNoPlus(dst, src)
|
|
|
|
|
|
|
|
// remove duplicate slashes
|
|
|
|
b := dst
|
|
|
|
bSize := len(b)
|
|
|
|
for {
|
|
|
|
n := bytes.Index(b, strSlashSlash)
|
|
|
|
if n < 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
b = b[n:]
|
|
|
|
copy(b, b[1:])
|
|
|
|
b = b[:len(b)-1]
|
|
|
|
bSize--
|
|
|
|
}
|
|
|
|
dst = dst[:bSize]
|
|
|
|
|
|
|
|
// remove /./ parts
|
|
|
|
b = dst
|
|
|
|
for {
|
|
|
|
n := bytes.Index(b, strSlashDotSlash)
|
|
|
|
if n < 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
nn := n + len(strSlashDotSlash) - 1
|
|
|
|
copy(b[n:], b[nn:])
|
|
|
|
b = b[:len(b)-nn+n]
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove /foo/../ parts
|
|
|
|
for {
|
|
|
|
n := bytes.Index(b, strSlashDotDotSlash)
|
|
|
|
if n < 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
nn := bytes.LastIndexByte(b[:n], '/')
|
|
|
|
if nn < 0 {
|
|
|
|
nn = 0
|
|
|
|
}
|
|
|
|
n += len(strSlashDotDotSlash) - 1
|
|
|
|
copy(b[nn:], b[n:])
|
|
|
|
b = b[:len(b)-n+nn]
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove trailing /foo/..
|
|
|
|
n := bytes.LastIndex(b, strSlashDotDot)
|
|
|
|
if n >= 0 && n+len(strSlashDotDot) == len(b) {
|
|
|
|
nn := bytes.LastIndexByte(b[:n], '/')
|
|
|
|
if nn < 0 {
|
|
|
|
return append(dst[:0], strSlash...)
|
|
|
|
}
|
|
|
|
b = b[:nn+1]
|
|
|
|
}
|
|
|
|
|
|
|
|
if filepath.Separator == '\\' {
|
|
|
|
// remove \.\ parts
|
|
|
|
for {
|
|
|
|
n := bytes.Index(b, strBackSlashDotBackSlash)
|
|
|
|
if n < 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
nn := n + len(strSlashDotSlash) - 1
|
|
|
|
copy(b[n:], b[nn:])
|
|
|
|
b = b[:len(b)-nn+n]
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove /foo/..\ parts
|
|
|
|
for {
|
|
|
|
n := bytes.Index(b, strSlashDotDotBackSlash)
|
|
|
|
if n < 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
nn := bytes.LastIndexByte(b[:n], '/')
|
|
|
|
if nn < 0 {
|
|
|
|
nn = 0
|
|
|
|
}
|
|
|
|
nn++
|
|
|
|
n += len(strSlashDotDotBackSlash)
|
|
|
|
copy(b[nn:], b[n:])
|
|
|
|
b = b[:len(b)-n+nn]
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove /foo\..\ parts
|
|
|
|
for {
|
|
|
|
n := bytes.Index(b, strBackSlashDotDotBackSlash)
|
|
|
|
if n < 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
nn := bytes.LastIndexByte(b[:n], '/')
|
|
|
|
if nn < 0 {
|
|
|
|
nn = 0
|
|
|
|
}
|
|
|
|
n += len(strBackSlashDotDotBackSlash) - 1
|
|
|
|
copy(b[nn:], b[n:])
|
|
|
|
b = b[:len(b)-n+nn]
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove trailing \foo\..
|
|
|
|
n := bytes.LastIndex(b, strBackSlashDotDot)
|
|
|
|
if n >= 0 && n+len(strSlashDotDot) == len(b) {
|
|
|
|
nn := bytes.LastIndexByte(b[:n], '/')
|
|
|
|
if nn < 0 {
|
|
|
|
return append(dst[:0], strSlash...)
|
|
|
|
}
|
|
|
|
b = b[:nn+1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
// RequestURI returns RequestURI - i.e. URI without Scheme and Host.
|
|
|
|
func (u *URI) RequestURI() []byte {
|
|
|
|
var dst []byte
|
|
|
|
if u.DisablePathNormalizing {
|
|
|
|
dst = u.requestURI[:0]
|
|
|
|
dst = append(dst, u.PathOriginal()...)
|
|
|
|
} else {
|
|
|
|
dst = appendQuotedPath(u.requestURI[:0], u.Path())
|
|
|
|
}
|
|
|
|
if u.parsedQueryArgs && u.queryArgs.Len() > 0 {
|
|
|
|
dst = append(dst, '?')
|
|
|
|
dst = u.queryArgs.AppendBytes(dst)
|
|
|
|
} else if len(u.queryString) > 0 {
|
|
|
|
dst = append(dst, '?')
|
|
|
|
dst = append(dst, u.queryString...)
|
|
|
|
}
|
|
|
|
u.requestURI = dst
|
|
|
|
return u.requestURI
|
|
|
|
}
|
|
|
|
|
|
|
|
// LastPathSegment returns the last part of uri path after '/'.
|
|
|
|
//
|
|
|
|
// Examples:
|
|
|
|
//
|
|
|
|
// - For /foo/bar/baz.html path returns baz.html.
|
|
|
|
// - For /foo/bar/ returns empty byte slice.
|
|
|
|
// - For /foobar.js returns foobar.js.
|
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) LastPathSegment() []byte {
|
|
|
|
path := u.Path()
|
|
|
|
n := bytes.LastIndexByte(path, '/')
|
|
|
|
if n < 0 {
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
return path[n+1:]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update updates uri.
|
|
|
|
//
|
|
|
|
// The following newURI types are accepted:
|
|
|
|
//
|
|
|
|
// - Absolute, i.e. http://foobar.com/aaa/bb?cc . In this case the original
|
|
|
|
// uri is replaced by newURI.
|
|
|
|
// - Absolute without scheme, i.e. //foobar.com/aaa/bb?cc. In this case
|
|
|
|
// the original scheme is preserved.
|
|
|
|
// - Missing host, i.e. /aaa/bb?cc . In this case only RequestURI part
|
|
|
|
// of the original uri is replaced.
|
|
|
|
// - Relative path, i.e. xx?yy=abc . In this case the original RequestURI
|
|
|
|
// is updated according to the new relative path.
|
|
|
|
func (u *URI) Update(newURI string) {
|
|
|
|
u.UpdateBytes(s2b(newURI))
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateBytes updates uri.
|
|
|
|
//
|
|
|
|
// The following newURI types are accepted:
|
|
|
|
//
|
|
|
|
// - Absolute, i.e. http://foobar.com/aaa/bb?cc . In this case the original
|
|
|
|
// uri is replaced by newURI.
|
|
|
|
// - Absolute without scheme, i.e. //foobar.com/aaa/bb?cc. In this case
|
|
|
|
// the original scheme is preserved.
|
|
|
|
// - Missing host, i.e. /aaa/bb?cc . In this case only RequestURI part
|
|
|
|
// of the original uri is replaced.
|
|
|
|
// - Relative path, i.e. xx?yy=abc . In this case the original RequestURI
|
|
|
|
// is updated according to the new relative path.
|
|
|
|
func (u *URI) UpdateBytes(newURI []byte) {
|
|
|
|
u.requestURI = u.updateBytes(newURI, u.requestURI)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *URI) updateBytes(newURI, buf []byte) []byte {
|
|
|
|
if len(newURI) == 0 {
|
|
|
|
return buf
|
|
|
|
}
|
|
|
|
|
|
|
|
n := bytes.Index(newURI, strSlashSlash)
|
|
|
|
if n >= 0 {
|
|
|
|
// absolute uri
|
|
|
|
var b [32]byte
|
|
|
|
schemeOriginal := b[:0]
|
|
|
|
if len(u.scheme) > 0 {
|
|
|
|
schemeOriginal = append([]byte(nil), u.scheme...)
|
|
|
|
}
|
|
|
|
if err := u.Parse(nil, newURI); err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if len(schemeOriginal) > 0 && len(u.scheme) == 0 {
|
|
|
|
u.scheme = append(u.scheme[:0], schemeOriginal...)
|
|
|
|
}
|
|
|
|
return buf
|
|
|
|
}
|
|
|
|
|
|
|
|
if newURI[0] == '/' {
|
|
|
|
// uri without host
|
|
|
|
buf = u.appendSchemeHost(buf[:0])
|
|
|
|
buf = append(buf, newURI...)
|
|
|
|
if err := u.Parse(nil, buf); err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return buf
|
|
|
|
}
|
|
|
|
|
|
|
|
// relative path
|
|
|
|
switch newURI[0] {
|
|
|
|
case '?':
|
|
|
|
// query string only update
|
|
|
|
u.SetQueryStringBytes(newURI[1:])
|
|
|
|
return append(buf[:0], u.FullURI()...)
|
|
|
|
case '#':
|
|
|
|
// update only hash
|
|
|
|
u.SetHashBytes(newURI[1:])
|
|
|
|
return append(buf[:0], u.FullURI()...)
|
|
|
|
default:
|
|
|
|
// update the last path part after the slash
|
|
|
|
path := u.Path()
|
|
|
|
n = bytes.LastIndexByte(path, '/')
|
|
|
|
if n < 0 {
|
|
|
|
panic(fmt.Sprintf("BUG: path must contain at least one slash: %q %q", u.Path(), newURI))
|
|
|
|
}
|
|
|
|
buf = u.appendSchemeHost(buf[:0])
|
|
|
|
buf = appendQuotedPath(buf, path[:n+1])
|
|
|
|
buf = append(buf, newURI...)
|
|
|
|
if err := u.Parse(nil, buf); err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return buf
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FullURI returns full uri in the form {Scheme}://{Host}{RequestURI}#{Hash}.
|
|
|
|
//
|
|
|
|
// The returned bytes are valid until the next URI method call.
|
|
|
|
func (u *URI) FullURI() []byte {
|
|
|
|
u.fullURI = u.AppendBytes(u.fullURI[:0])
|
|
|
|
return u.fullURI
|
|
|
|
}
|
|
|
|
|
|
|
|
// AppendBytes appends full uri to dst and returns the extended dst.
|
|
|
|
func (u *URI) AppendBytes(dst []byte) []byte {
|
|
|
|
dst = u.appendSchemeHost(dst)
|
|
|
|
dst = append(dst, u.RequestURI()...)
|
|
|
|
if len(u.hash) > 0 {
|
|
|
|
dst = append(dst, '#')
|
|
|
|
dst = append(dst, u.hash...)
|
|
|
|
}
|
|
|
|
return dst
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *URI) appendSchemeHost(dst []byte) []byte {
|
|
|
|
dst = append(dst, u.Scheme()...)
|
|
|
|
dst = append(dst, strColonSlashSlash...)
|
|
|
|
return append(dst, u.Host()...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteTo writes full uri to w.
|
|
|
|
//
|
|
|
|
// WriteTo implements io.WriterTo interface.
|
|
|
|
func (u *URI) WriteTo(w io.Writer) (int64, error) {
|
|
|
|
n, err := w.Write(u.FullURI())
|
|
|
|
return int64(n), err
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns full uri.
|
|
|
|
func (u *URI) String() string {
|
|
|
|
return string(u.FullURI())
|
|
|
|
}
|
|
|
|
|
|
|
|
func splitHostURI(host, uri []byte) ([]byte, []byte, []byte) {
|
|
|
|
n := bytes.Index(uri, strSlashSlash)
|
|
|
|
if n < 0 {
|
|
|
|
return strHTTP, host, uri
|
|
|
|
}
|
|
|
|
scheme := uri[:n]
|
|
|
|
if bytes.IndexByte(scheme, '/') >= 0 {
|
|
|
|
return strHTTP, host, uri
|
|
|
|
}
|
|
|
|
if len(scheme) > 0 && scheme[len(scheme)-1] == ':' {
|
|
|
|
scheme = scheme[:len(scheme)-1]
|
|
|
|
}
|
|
|
|
n += len(strSlashSlash)
|
|
|
|
uri = uri[n:]
|
|
|
|
n = bytes.IndexByte(uri, '/')
|
|
|
|
nq := bytes.IndexByte(uri, '?')
|
|
|
|
if nq >= 0 && nq < n {
|
|
|
|
// A hack for urls like foobar.com?a=b/xyz
|
|
|
|
n = nq
|
|
|
|
} else if n < 0 {
|
|
|
|
// A hack for bogus urls like foobar.com?a=b without
|
|
|
|
// slash after host.
|
|
|
|
if nq >= 0 {
|
|
|
|
return scheme, uri[:nq], uri[nq:]
|
|
|
|
}
|
|
|
|
return scheme, uri, strSlash
|
|
|
|
}
|
|
|
|
return scheme, uri[:n], uri[n:]
|
|
|
|
}
|
|
|
|
|
|
|
|
// QueryArgs returns query args.
|
|
|
|
//
|
|
|
|
// The returned args are valid until the next URI method call.
|
|
|
|
func (u *URI) QueryArgs() *Args {
|
|
|
|
u.parseQueryArgs()
|
|
|
|
return &u.queryArgs
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *URI) parseQueryArgs() {
|
|
|
|
if u.parsedQueryArgs {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
u.queryArgs.ParseBytes(u.queryString)
|
|
|
|
u.parsedQueryArgs = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// stringContainsCTLByte reports whether s contains any ASCII control character.
|
|
|
|
func stringContainsCTLByte(s []byte) bool {
|
|
|
|
for i := 0; i < len(s); i++ {
|
|
|
|
b := s[i]
|
|
|
|
if b < ' ' || b == 0x7f {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|