From 23685f6632de82405249d9b873786bc8fbec1297 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Wed, 28 Aug 2024 14:51:57 -0700 Subject: [PATCH 1/4] add net/url with https://go-review.googlesource.com/c/go/+/450375 --- internal/net/url/README | 1 + internal/net/url/url.go | 1276 ++++++++++++++++++++ internal/net/url/url_test.go | 2204 ++++++++++++++++++++++++++++++++++ 3 files changed, 3481 insertions(+) create mode 100644 internal/net/url/README create mode 100644 internal/net/url/url.go create mode 100644 internal/net/url/url_test.go diff --git a/internal/net/url/README b/internal/net/url/README new file mode 100644 index 0000000..bad607e --- /dev/null +++ b/internal/net/url/README @@ -0,0 +1 @@ +net/url from commit e6ebbefaf848604c8df3e2a58e146948b03e608b diff --git a/internal/net/url/url.go b/internal/net/url/url.go new file mode 100644 index 0000000..e959ed4 --- /dev/null +++ b/internal/net/url/url.go @@ -0,0 +1,1276 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package url parses URLs and implements query escaping. +package url + +// See RFC 3986. This package generally follows RFC 3986, except where +// it deviates for compatibility reasons. When sending changes, first +// search old issues for history on decisions. Unit tests should also +// contain references to issue numbers with details. + +import ( + "errors" + "fmt" + "path" + "sort" + "strconv" + "strings" +) + +// Error reports an error and the operation and URL that caused it. +type Error struct { + Op string + URL string + Err error +} + +func (e *Error) Unwrap() error { return e.Err } +func (e *Error) Error() string { return fmt.Sprintf("%s %q: %s", e.Op, e.URL, e.Err) } + +func (e *Error) Timeout() bool { + t, ok := e.Err.(interface { + Timeout() bool + }) + return ok && t.Timeout() +} + +func (e *Error) Temporary() bool { + t, ok := e.Err.(interface { + Temporary() bool + }) + return ok && t.Temporary() +} + +const upperhex = "0123456789ABCDEF" + +func ishex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +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 +} + +type encoding int + +const ( + encodePath encoding = 1 + iota + encodePathSegment + encodeHost + encodeZone + encodeUserPassword + encodeQueryComponent + encodeFragment +) + +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" +} + +// 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 golang.org/issue/5684. +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 + } + } + + switch c { + case '-', '_', '.', '~': // §2.3 Unreserved characters (mark) + return false + + case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved) + // Different sections of the URL allow a few of + // the reserved characters to appear unescaped. + switch mode { + case encodePath: // §3.3 + // The RFC allows : @ & = + $ but saves / ; , for assigning + // meaning to individual path segments. This package + // only manipulates the path as a whole, so we allow those + // last three as well. That leaves only ? to escape. + return c == '?' + + case encodePathSegment: // §3.3 + // The RFC allows : @ & = + $ but saves / ; , for assigning + // meaning to individual path segments. + return c == '/' || c == ';' || c == ',' || c == '?' + + case encodeUserPassword: // §3.2.1 + // The RFC allows ';', ':', '&', '=', '+', '$', and ',' in + // userinfo, so we must escape only '@', '/', and '?'. + // The parsing of userinfo treats ':' as special so we must escape + // that too. + return c == '@' || c == '/' || c == '?' || c == ':' + + case encodeQueryComponent: // §3.4 + // The RFC reserves (so we must escape) everything. + return true + + case encodeFragment: // §4.1 + // The RFC text is silent but the grammar allows + // everything, so escape nothing. + return false + } + } + + if mode == encodeFragment { + // RFC 3986 §2.2 allows not escaping sub-delims. A subset of sub-delims are + // included in reserved from RFC 2396 §2.2. The remaining sub-delims do not + // need to be escaped. To minimize potential breakage, we apply two restrictions: + // (1) we always escape sub-delims outside of the fragment, and (2) we always + // escape single quote to avoid breaking callers that had previously assumed that + // single quotes would be escaped. See issue #19917. + switch c { + case '!', '(', ')', '*': + return false + } + } + + // Everything else must be escaped. + return true +} + +// QueryUnescape does the inverse transformation of QueryEscape, +// converting each 3-byte encoded substring of the form "%AB" into the +// hex-decoded byte 0xAB. +// It returns an error if any % is not followed by two hexadecimal +// digits. +func QueryUnescape(s string) (string, error) { + return unescape(s, encodeQueryComponent) +} + +// PathUnescape does the inverse transformation of PathEscape, +// converting each 3-byte encoded substring of the form "%AB" into the +// hex-decoded byte 0xAB. It returns an error if any % is not followed +// by two hexadecimal digits. +// +// PathUnescape is identical to QueryUnescape except that it does not +// unescape '+' to ' ' (space). +func PathUnescape(s string) (string, error) { + return unescape(s, encodePathSegment) +} + +// unescape unescapes a string; the mode specifies +// which section of the URL string is being unescaped. +func unescape(s string, mode encoding) (string, error) { + isPercentEscape := func(s string, i int) bool { + return i+2 < len(s) && ishex(s[i+1]) && ishex(s[i+2]) + } + + // Count %, check that they're well-formed. + n := 0 + hasPlus := false + for i := 0; i < len(s); { + switch s[i] { + case '%': + if !isPercentEscape(s, i) { + // https://url.spec.whatwg.org/#percent-encoded-bytes + // says that % followed by non-hex characters + // should be accepted with no error. + i++ + continue + } + n++ + // 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 && s[i:i+3] != "%25" { + return "", 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 s[i:i+3] != "%25" && v != ' ' && shouldEscape(v, encodeHost) { + return "", EscapeError(s[i : i+3]) + } + } + i += 3 + case '+': + hasPlus = mode == encodeQueryComponent + i++ + default: + if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) { + return "", InvalidHostError(s[i : i+1]) + } + i++ + } + } + + if n == 0 && !hasPlus { + return s, nil + } + + var t strings.Builder + t.Grow(len(s) - 2*n) + for i := 0; i < len(s); i++ { + switch s[i] { + case '%': + if !isPercentEscape(s, i) { + t.WriteByte('%') + } else { + t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2])) + i += 2 + } + case '+': + if mode == encodeQueryComponent { + t.WriteByte(' ') + } else { + t.WriteByte('+') + } + default: + t.WriteByte(s[i]) + } + } + return t.String(), nil +} + +// QueryEscape escapes the string so it can be safely placed +// inside a URL query. +func QueryEscape(s string) string { + return escape(s, encodeQueryComponent) +} + +// PathEscape escapes the string so it can be safely placed inside a URL path segment, +// replacing special characters (including /) with %XX sequences as needed. +func PathEscape(s string) string { + return escape(s, encodePathSegment) +} + +func escape(s string, mode encoding) string { + spaceCount, hexCount := 0, 0 + for i := 0; i < len(s); i++ { + c := s[i] + if shouldEscape(c, mode) { + if c == ' ' && mode == encodeQueryComponent { + spaceCount++ + } else { + hexCount++ + } + } + } + + if spaceCount == 0 && hexCount == 0 { + return s + } + + var buf [64]byte + var t []byte + + required := len(s) + 2*hexCount + if required <= len(buf) { + t = buf[:required] + } else { + t = make([]byte, required) + } + + if hexCount == 0 { + copy(t, s) + for i := 0; i < len(s); i++ { + if s[i] == ' ' { + t[i] = '+' + } + } + return string(t) + } + + j := 0 + for i := 0; i < len(s); i++ { + switch c := s[i]; { + case c == ' ' && mode == encodeQueryComponent: + t[j] = '+' + j++ + case shouldEscape(c, mode): + t[j] = '%' + t[j+1] = upperhex[c>>4] + t[j+2] = upperhex[c&15] + j += 3 + default: + t[j] = s[i] + j++ + } + } + return string(t) +} + +// A URL represents a parsed URL (technically, a URI reference). +// +// The general form represented is: +// +// [scheme:][//[userinfo@]host][/]path[?query][#fragment] +// +// URLs that do not start with a slash after the scheme are interpreted as: +// +// scheme:opaque[?query][#fragment] +// +// Note that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/. +// A consequence is that it is impossible to tell which slashes in the Path were +// slashes in the raw URL and which were %2f. This distinction is rarely important, +// but when it is, the code should use the EscapedPath method, which preserves +// the original encoding of Path. +// +// The RawPath field is an optional field which is only set when the default +// encoding of Path is different from the escaped path. See the EscapedPath method +// for more details. +// +// URL's String method uses the EscapedPath method to obtain the path. +type URL struct { + Scheme string + Opaque string // encoded opaque data + User *Userinfo // username and password information + Host string // host or host:port + Path string // path (relative paths may omit leading slash) + RawPath string // encoded path hint (see EscapedPath method) + OmitHost bool // do not emit empty host (authority) + ForceQuery bool // append a query ('?') even if RawQuery is empty + RawQuery string // encoded query values, without '?' + Fragment string // fragment for references, without '#' + RawFragment string // encoded fragment hint (see EscapedFragment method) +} + +// User returns a Userinfo containing the provided username +// and no password set. +func User(username string) *Userinfo { + return &Userinfo{username, "", false} +} + +// UserPassword returns a Userinfo containing the provided username +// and password. +// +// This functionality should only be used with legacy web sites. +// RFC 2396 warns that interpreting Userinfo this way +// “is NOT RECOMMENDED, because the passing of authentication +// information in clear text (such as URI) has proven to be a +// security risk in almost every case where it has been used.” +func UserPassword(username, password string) *Userinfo { + return &Userinfo{username, password, true} +} + +// The Userinfo type is an immutable encapsulation of username and +// password details for a URL. An existing Userinfo value is guaranteed +// to have a username set (potentially empty, as allowed by RFC 2396), +// and optionally a password. +type Userinfo struct { + username string + password string + passwordSet bool +} + +// Username returns the username. +func (u *Userinfo) Username() string { + if u == nil { + return "" + } + return u.username +} + +// Password returns the password in case it is set, and whether it is set. +func (u *Userinfo) Password() (string, bool) { + if u == nil { + return "", false + } + return u.password, u.passwordSet +} + +// String returns the encoded userinfo information in the standard form +// of "username[:password]". +func (u *Userinfo) String() string { + if u == nil { + return "" + } + s := escape(u.username, encodeUserPassword) + if u.passwordSet { + s += ":" + escape(u.password, encodeUserPassword) + } + return s +} + +// Maybe rawURL is of the form scheme:path. +// (Scheme must be [a-zA-Z][a-zA-Z0-9+.-]*) +// If so, return scheme, path; else return "", rawURL. +func getScheme(rawURL string) (scheme, path string, err error) { + for i := 0; i < len(rawURL); i++ { + c := rawURL[i] + switch { + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': + // do nothing + case '0' <= c && c <= '9' || c == '+' || c == '-' || c == '.': + if i == 0 { + return "", rawURL, nil + } + case c == ':': + if i == 0 { + return "", "", errors.New("missing protocol scheme") + } + return rawURL[:i], rawURL[i+1:], nil + default: + // we have encountered an invalid character, + // so there is no valid scheme + return "", rawURL, nil + } + } + return "", rawURL, nil +} + +// Parse parses a raw url into a URL structure. +// +// The url may be relative (a path, without a host) or absolute +// (starting with a scheme). Trying to parse a hostname and path +// without a scheme is invalid but may not necessarily return an +// error, due to parsing ambiguities. +func Parse(rawURL string) (*URL, error) { + // Cut off #frag + u, frag, _ := strings.Cut(rawURL, "#") + url, err := parse(u, false) + if err != nil { + return nil, &Error{"parse", u, err} + } + if frag == "" { + return url, nil + } + if err = url.setFragment(frag); err != nil { + return nil, &Error{"parse", rawURL, err} + } + return url, nil +} + +// ParseRequestURI parses a raw url into a URL structure. It assumes that +// url was received in an HTTP request, so the url is interpreted +// only as an absolute URI or an absolute path. +// The string url is assumed not to have a #fragment suffix. +// (Web browsers strip #fragment before sending the URL to a web server.) +func ParseRequestURI(rawURL string) (*URL, error) { + url, err := parse(rawURL, true) + if err != nil { + return nil, &Error{"parse", rawURL, err} + } + return url, nil +} + +// parse parses a URL from a string in one of two contexts. If +// viaRequest is true, the URL is assumed to have arrived via an HTTP request, +// in which case only absolute URLs or path-absolute relative URLs are allowed. +// If viaRequest is false, all forms of relative URLs are allowed. +func parse(rawURL string, viaRequest bool) (*URL, error) { + var rest string + var err error + + if stringContainsCTLByte(rawURL) { + return nil, errors.New("net/url: invalid control character in URL") + } + + if rawURL == "" && viaRequest { + return nil, errors.New("empty url") + } + url := new(URL) + + if rawURL == "*" { + url.Path = "*" + return url, nil + } + + // Split off possible leading "http:", "mailto:", etc. + // Cannot contain escaped characters. + if url.Scheme, rest, err = getScheme(rawURL); err != nil { + return nil, err + } + url.Scheme = strings.ToLower(url.Scheme) + + if strings.HasSuffix(rest, "?") && strings.Count(rest, "?") == 1 { + url.ForceQuery = true + rest = rest[:len(rest)-1] + } else { + rest, url.RawQuery, _ = strings.Cut(rest, "?") + } + + if !strings.HasPrefix(rest, "/") { + if url.Scheme != "" { + // We consider rootless paths per RFC 3986 as opaque. + url.Opaque = rest + return url, nil + } + if viaRequest { + return nil, errors.New("invalid URI for request") + } + + // Avoid confusion with malformed schemes, like cache_object:foo/bar. + // See golang.org/issue/16822. + // + // RFC 3986, §3.3: + // In addition, a URI reference (Section 4.1) may be a relative-path reference, + // in which case the first path segment cannot contain a colon (":") character. + if segment, _, _ := strings.Cut(rest, "/"); strings.Contains(segment, ":") { + // First path segment has colon. Not allowed in relative URL. + return nil, errors.New("first path segment in URL cannot contain colon") + } + } + + if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") { + var authority string + authority, rest = rest[2:], "" + if i := strings.Index(authority, "/"); i >= 0 { + authority, rest = authority[:i], authority[i:] + } + url.User, url.Host, err = parseAuthority(authority) + if err != nil { + return nil, err + } + } else if url.Scheme != "" && strings.HasPrefix(rest, "/") { + // OmitHost is set to true when rawURL has an empty host (authority). + // See golang.org/issue/46059. + url.OmitHost = true + } + + // Set Path and, optionally, RawPath. + // RawPath is a hint of the encoding of Path. We don't want to set it if + // the default escaping of Path is equivalent, to help make sure that people + // don't rely on it in general. + if err := url.setPath(rest); err != nil { + return nil, err + } + return url, nil +} + +func parseAuthority(authority string) (user *Userinfo, host string, err error) { + i := strings.LastIndex(authority, "@") + if i < 0 { + host, err = parseHost(authority) + } else { + host, err = parseHost(authority[i+1:]) + } + if err != nil { + return nil, "", err + } + if i < 0 { + return nil, host, nil + } + userinfo := authority[:i] + if !validUserinfo(userinfo) { + return nil, "", errors.New("net/url: invalid userinfo") + } + if !strings.Contains(userinfo, ":") { + if userinfo, err = unescape(userinfo, encodeUserPassword); err != nil { + return nil, "", err + } + user = User(userinfo) + } else { + username, password, _ := strings.Cut(userinfo, ":") + if username, err = unescape(username, encodeUserPassword); err != nil { + return nil, "", err + } + if password, err = unescape(password, encodeUserPassword); err != nil { + return nil, "", err + } + user = UserPassword(username, password) + } + return user, host, nil +} + +// parseHost parses host as an authority without user +// information. That is, as host[:port]. +func parseHost(host string) (string, error) { + if strings.HasPrefix(host, "[") { + // Parse an IP-Literal in RFC 3986 and RFC 6874. + // E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80". + i := strings.LastIndex(host, "]") + if i < 0 { + return "", errors.New("missing ']' in host") + } + colonPort := host[i+1:] + if !validOptionalPort(colonPort) { + return "", 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 := strings.Index(host[:i], "%25") + if zone >= 0 { + host1, err := unescape(host[:zone], encodeHost) + if err != nil { + return "", err + } + host2, err := unescape(host[zone:i], encodeZone) + if err != nil { + return "", err + } + host3, err := unescape(host[i:], encodeHost) + if err != nil { + return "", err + } + return host1 + host2 + host3, nil + } + } else if i := strings.LastIndex(host, ":"); i != -1 { + colonPort := host[i:] + if !validOptionalPort(colonPort) { + return "", fmt.Errorf("invalid port %q after host", colonPort) + } + } + + var err error + if host, err = unescape(host, encodeHost); err != nil { + return "", err + } + return host, nil +} + +// setPath sets the Path and RawPath fields of the URL based on the provided +// escaped path p. It maintains the invariant that RawPath is only specified +// when it differs from the default encoding of the path. +// For example: +// - setPath("/foo/bar") will set Path="/foo/bar" and RawPath="" +// - setPath("/foo%2fbar") will set Path="/foo/bar" and RawPath="/foo%2fbar" +// setPath will return an error only if the provided path contains an invalid +// escaping. +func (u *URL) setPath(p string) error { + path, err := unescape(p, encodePath) + if err != nil { + return err + } + u.Path = path + if escp := escape(path, encodePath); p == escp { + // Default encoding is fine. + u.RawPath = "" + } else { + u.RawPath = p + } + return nil +} + +// EscapedPath returns the escaped form of u.Path. +// In general there are multiple possible escaped forms of any path. +// EscapedPath returns u.RawPath when it is a valid escaping of u.Path. +// Otherwise EscapedPath ignores u.RawPath and computes an escaped +// form on its own. +// The String and RequestURI methods use EscapedPath to construct +// their results. +// In general, code should call EscapedPath instead of +// reading u.RawPath directly. +func (u *URL) EscapedPath() string { + if u.RawPath != "" && validEncoded(u.RawPath, encodePath) { + p, err := unescape(u.RawPath, encodePath) + if err == nil && p == u.Path { + return u.RawPath + } + } + if u.Path == "*" { + return "*" // don't escape (Issue 11202) + } + return escape(u.Path, encodePath) +} + +// validEncoded reports whether s is a valid encoded path or fragment, +// according to mode. +// It must not contain any bytes that require escaping during encoding. +func validEncoded(s string, mode encoding) bool { + for i := 0; i < len(s); i++ { + // RFC 3986, Appendix A. + // pchar = unreserved / pct-encoded / sub-delims / ":" / "@". + // shouldEscape is not quite compliant with the RFC, + // so we check the sub-delims ourselves and let + // shouldEscape handle the others. + switch s[i] { + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@': + // ok + case '[', ']': + // ok - not specified in RFC 3986 but left alone by modern browsers + case '%': + // ok - percent encoded, will decode + default: + if shouldEscape(s[i], mode) { + return false + } + } + } + return true +} + +// setFragment is like setPath but for Fragment/RawFragment. +func (u *URL) setFragment(f string) error { + frag, err := unescape(f, encodeFragment) + if err != nil { + return err + } + u.Fragment = frag + if escf := escape(frag, encodeFragment); f == escf { + // Default encoding is fine. + u.RawFragment = "" + } else { + u.RawFragment = f + } + return nil +} + +// EscapedFragment returns the escaped form of u.Fragment. +// In general there are multiple possible escaped forms of any fragment. +// EscapedFragment returns u.RawFragment when it is a valid escaping of u.Fragment. +// Otherwise EscapedFragment ignores u.RawFragment and computes an escaped +// form on its own. +// The String method uses EscapedFragment to construct its result. +// In general, code should call EscapedFragment instead of +// reading u.RawFragment directly. +func (u *URL) EscapedFragment() string { + if u.RawFragment != "" && validEncoded(u.RawFragment, encodeFragment) { + f, err := unescape(u.RawFragment, encodeFragment) + if err == nil && f == u.Fragment { + return u.RawFragment + } + } + return escape(u.Fragment, encodeFragment) +} + +// validOptionalPort reports whether port is either an empty string +// or matches /^:\d*$/ +func validOptionalPort(port string) bool { + if port == "" { + return true + } + if port[0] != ':' { + return false + } + for _, b := range port[1:] { + if b < '0' || b > '9' { + return false + } + } + return true +} + +// String reassembles the URL into a valid URL string. +// The general form of the result is one of: +// +// scheme:opaque?query#fragment +// scheme://userinfo@host/path?query#fragment +// +// If u.Opaque is non-empty, String uses the first form; +// otherwise it uses the second form. +// Any non-ASCII characters in host are escaped. +// To obtain the path, String uses u.EscapedPath(). +// +// In the second form, the following rules apply: +// - if u.Scheme is empty, scheme: is omitted. +// - if u.User is nil, userinfo@ is omitted. +// - if u.Host is empty, host/ is omitted. +// - if u.Scheme and u.Host are empty and u.User is nil, +// the entire scheme://userinfo@host/ is omitted. +// - if u.Host is non-empty and u.Path begins with a /, +// the form host/path does not add its own /. +// - if u.RawQuery is empty, ?query is omitted. +// - if u.Fragment is empty, #fragment is omitted. +func (u *URL) String() string { + var buf strings.Builder + if u.Scheme != "" { + buf.WriteString(u.Scheme) + buf.WriteByte(':') + } + if u.Opaque != "" { + buf.WriteString(u.Opaque) + } else { + if u.Scheme != "" || u.Host != "" || u.User != nil { + if u.OmitHost && u.Host == "" && u.User == nil { + // omit empty host + } else { + if u.Host != "" || u.Path != "" || u.User != nil { + buf.WriteString("//") + } + if ui := u.User; ui != nil { + buf.WriteString(ui.String()) + buf.WriteByte('@') + } + if h := u.Host; h != "" { + buf.WriteString(escape(h, encodeHost)) + } + } + } + path := u.EscapedPath() + if path != "" && path[0] != '/' && u.Host != "" { + buf.WriteByte('/') + } + if buf.Len() == 0 { + // RFC 3986 §4.2 + // A path segment that contains a colon character (e.g., "this:that") + // cannot be used as the first segment of a relative-path reference, as + // it would be mistaken for a scheme name. Such a segment must be + // preceded by a dot-segment (e.g., "./this:that") to make a relative- + // path reference. + if segment, _, _ := strings.Cut(path, "/"); strings.Contains(segment, ":") { + buf.WriteString("./") + } + } + buf.WriteString(path) + } + if u.ForceQuery || u.RawQuery != "" { + buf.WriteByte('?') + buf.WriteString(u.RawQuery) + } + if u.Fragment != "" { + buf.WriteByte('#') + buf.WriteString(u.EscapedFragment()) + } + return buf.String() +} + +// Redacted is like String but replaces any password with "xxxxx". +// Only the password in u.URL is redacted. +func (u *URL) Redacted() string { + if u == nil { + return "" + } + + ru := *u + if _, has := ru.User.Password(); has { + ru.User = UserPassword(ru.User.Username(), "xxxxx") + } + return ru.String() +} + +// Values maps a string key to a list of values. +// It is typically used for query parameters and form values. +// Unlike in the http.Header map, the keys in a Values map +// are case-sensitive. +type Values map[string][]string + +// Get gets the first value associated with the given key. +// If there are no values associated with the key, Get returns +// the empty string. To access multiple values, use the map +// directly. +func (v Values) Get(key string) string { + if v == nil { + return "" + } + vs := v[key] + if len(vs) == 0 { + return "" + } + return vs[0] +} + +// Set sets the key to value. It replaces any existing +// values. +func (v Values) Set(key, value string) { + v[key] = []string{value} +} + +// Add adds the value to key. It appends to any existing +// values associated with key. +func (v Values) Add(key, value string) { + v[key] = append(v[key], value) +} + +// Del deletes the values associated with key. +func (v Values) Del(key string) { + delete(v, key) +} + +// Has checks whether a given key is set. +func (v Values) Has(key string) bool { + _, ok := v[key] + return ok +} + +// ParseQuery parses the URL-encoded query string and returns +// a map listing the values specified for each key. +// ParseQuery always returns a non-nil map containing all the +// valid query parameters found; err describes the first decoding error +// encountered, if any. +// +// Query is expected to be a list of key=value settings separated by ampersands. +// A setting without an equals sign is interpreted as a key set to an empty +// value. +// Settings containing a non-URL-encoded semicolon are considered invalid. +func ParseQuery(query string) (Values, error) { + m := make(Values) + err := parseQuery(m, query) + return m, err +} + +func parseQuery(m Values, query string) (err error) { + for query != "" { + var key string + key, query, _ = strings.Cut(query, "&") + if strings.Contains(key, ";") { + err = fmt.Errorf("invalid semicolon separator in query") + continue + } + if key == "" { + continue + } + key, value, _ := strings.Cut(key, "=") + key, err1 := QueryUnescape(key) + if err1 != nil { + if err == nil { + err = err1 + } + continue + } + value, err1 = QueryUnescape(value) + if err1 != nil { + if err == nil { + err = err1 + } + continue + } + m[key] = append(m[key], value) + } + return err +} + +// Encode encodes the values into “URL encoded” form +// ("bar=baz&foo=quux") sorted by key. +func (v Values) Encode() string { + if v == nil { + return "" + } + var buf strings.Builder + keys := make([]string, 0, len(v)) + for k := range v { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + vs := v[k] + keyEscaped := QueryEscape(k) + for _, v := range vs { + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(keyEscaped) + buf.WriteByte('=') + buf.WriteString(QueryEscape(v)) + } + } + return buf.String() +} + +// resolvePath applies special path segments from refs and applies +// them to base, per RFC 3986. +func resolvePath(base, ref string) string { + var full string + if ref == "" { + full = base + } else if ref[0] != '/' { + i := strings.LastIndex(base, "/") + full = base[:i+1] + ref + } else { + full = ref + } + if full == "" { + return "" + } + + var ( + elem string + dst strings.Builder + ) + first := true + remaining := full + // We want to return a leading '/', so write it now. + dst.WriteByte('/') + found := true + for found { + elem, remaining, found = strings.Cut(remaining, "/") + if elem == "." { + first = false + // drop + continue + } + + if elem == ".." { + // Ignore the leading '/' we already wrote. + str := dst.String()[1:] + index := strings.LastIndexByte(str, '/') + + dst.Reset() + dst.WriteByte('/') + if index == -1 { + first = true + } else { + dst.WriteString(str[:index]) + } + } else { + if !first { + dst.WriteByte('/') + } + dst.WriteString(elem) + first = false + } + } + + if elem == "." || elem == ".." { + dst.WriteByte('/') + } + + // We wrote an initial '/', but we don't want two. + r := dst.String() + if len(r) > 1 && r[1] == '/' { + r = r[1:] + } + return r +} + +// IsAbs reports whether the URL is absolute. +// Absolute means that it has a non-empty scheme. +func (u *URL) IsAbs() bool { + return u.Scheme != "" +} + +// Parse parses a URL in the context of the receiver. The provided URL +// may be relative or absolute. Parse returns nil, err on parse +// failure, otherwise its return value is the same as ResolveReference. +func (u *URL) Parse(ref string) (*URL, error) { + refURL, err := Parse(ref) + if err != nil { + return nil, err + } + return u.ResolveReference(refURL), nil +} + +// ResolveReference resolves a URI reference to an absolute URI from +// an absolute base URI u, per RFC 3986 Section 5.2. The URI reference +// may be relative or absolute. ResolveReference always returns a new +// URL instance, even if the returned URL is identical to either the +// base or reference. If ref is an absolute URL, then ResolveReference +// ignores base and returns a copy of ref. +func (u *URL) ResolveReference(ref *URL) *URL { + url := *ref + if ref.Scheme == "" { + url.Scheme = u.Scheme + } + if ref.Scheme != "" || ref.Host != "" || ref.User != nil { + // The "absoluteURI" or "net_path" cases. + // We can ignore the error from setPath since we know we provided a + // validly-escaped path. + url.setPath(resolvePath(ref.EscapedPath(), "")) + return &url + } + if ref.Opaque != "" { + url.User = nil + url.Host = "" + url.Path = "" + return &url + } + if ref.Path == "" && !ref.ForceQuery && ref.RawQuery == "" { + url.RawQuery = u.RawQuery + if ref.Fragment == "" { + url.Fragment = u.Fragment + url.RawFragment = u.RawFragment + } + } + // The "abs_path" or "rel_path" cases. + url.Host = u.Host + url.User = u.User + url.setPath(resolvePath(u.EscapedPath(), ref.EscapedPath())) + return &url +} + +// Query parses RawQuery and returns the corresponding values. +// It silently discards malformed value pairs. +// To check errors use ParseQuery. +func (u *URL) Query() Values { + v, _ := ParseQuery(u.RawQuery) + return v +} + +// RequestURI returns the encoded path?query or opaque?query +// string that would be used in an HTTP request for u. +func (u *URL) RequestURI() string { + result := u.Opaque + if result == "" { + result = u.EscapedPath() + if result == "" { + result = "/" + } + } else { + if strings.HasPrefix(result, "//") { + result = u.Scheme + ":" + result + } + } + if u.ForceQuery || u.RawQuery != "" { + result += "?" + u.RawQuery + } + return result +} + +// Hostname returns u.Host, stripping any valid port number if present. +// +// If the result is enclosed in square brackets, as literal IPv6 addresses are, +// the square brackets are removed from the result. +func (u *URL) Hostname() string { + host, _ := splitHostPort(u.Host) + return host +} + +// Port returns the port part of u.Host, without the leading colon. +// +// If u.Host doesn't contain a valid numeric port, Port returns an empty string. +func (u *URL) Port() string { + _, port := splitHostPort(u.Host) + return port +} + +// splitHostPort separates host and port. If the port is not valid, it returns +// the entire input as host, and it doesn't check the validity of the host. +// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric. +func splitHostPort(hostPort string) (host, port string) { + host = hostPort + + colon := strings.LastIndexByte(host, ':') + if colon != -1 && validOptionalPort(host[colon:]) { + host, port = host[:colon], host[colon+1:] + } + + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = host[1 : len(host)-1] + } + + return +} + +// Marshaling interface implementations. +// Would like to implement MarshalText/UnmarshalText but that will change the JSON representation of URLs. + +func (u *URL) MarshalBinary() (text []byte, err error) { + return []byte(u.String()), nil +} + +func (u *URL) UnmarshalBinary(text []byte) error { + u1, err := Parse(string(text)) + if err != nil { + return err + } + *u = *u1 + return nil +} + +// JoinPath returns a new URL with the provided path elements joined to +// any existing path and the resulting path cleaned of any ./ or ../ elements. +// Any sequences of multiple / characters will be reduced to a single /. +func (u *URL) JoinPath(elem ...string) *URL { + elem = append([]string{u.EscapedPath()}, elem...) + var p string + if !strings.HasPrefix(elem[0], "/") { + // Return a relative path if u is relative, + // but ensure that it contains no ../ elements. + elem[0] = "/" + elem[0] + p = path.Join(elem...)[1:] + } else { + p = path.Join(elem...) + } + // path.Join will remove any trailing slashes. + // Preserve at least one. + if strings.HasSuffix(elem[len(elem)-1], "/") && !strings.HasSuffix(p, "/") { + p += "/" + } + url := *u + url.setPath(p) + return &url +} + +// validUserinfo reports whether s is a valid userinfo string per RFC 3986 +// Section 3.2.1: +// +// userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +// / "*" / "+" / "," / ";" / "=" +// +// It doesn't validate pct-encoded. The caller does that via func unescape. +func validUserinfo(s string) bool { + for _, r := range s { + if 'A' <= r && r <= 'Z' { + continue + } + if 'a' <= r && r <= 'z' { + continue + } + if '0' <= r && r <= '9' { + continue + } + switch r { + case '-', '.', '_', ':', '~', '!', '$', '&', '\'', + '(', ')', '*', '+', ',', ';', '=', '%', '@': + continue + default: + return false + } + } + return true +} + +// stringContainsCTLByte reports whether s contains any ASCII control character. +func stringContainsCTLByte(s string) bool { + for i := 0; i < len(s); i++ { + b := s[i] + if b < ' ' || b == 0x7f { + return true + } + } + return false +} + +// JoinPath returns a URL string with the provided path elements joined to +// the existing path of base and the resulting path cleaned of any ./ or ../ elements. +func JoinPath(base string, elem ...string) (result string, err error) { + url, err := Parse(base) + if err != nil { + return + } + result = url.JoinPath(elem...).String() + return +} diff --git a/internal/net/url/url_test.go b/internal/net/url/url_test.go new file mode 100644 index 0000000..899ec99 --- /dev/null +++ b/internal/net/url/url_test.go @@ -0,0 +1,2204 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package url + +import ( + "bytes" + encodingPkg "encoding" + "encoding/gob" + "encoding/json" + "fmt" + "io" + "net" + "reflect" + "strings" + "testing" +) + +type URLTest struct { + in string + out *URL // expected parse + roundtrip string // expected result of reserializing the URL; empty means same as "in". +} + +var urltests = []URLTest{ + // no path + { + "http://www.google.com", + &URL{ + Scheme: "http", + Host: "www.google.com", + }, + "", + }, + // path + { + "http://www.google.com/", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + }, + "", + }, + // path with hex escaping + { + "http://www.google.com/file%20one%26two", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/file one&two", + RawPath: "/file%20one%26two", + }, + "", + }, + // fragment with hex escaping + { + "http://www.google.com/#file%20one%26two", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + Fragment: "file one&two", + RawFragment: "file%20one%26two", + }, + "", + }, + // user + { + "ftp://webmaster@www.google.com/", + &URL{ + Scheme: "ftp", + User: User("webmaster"), + Host: "www.google.com", + Path: "/", + }, + "", + }, + // escape sequence in username + { + "ftp://john%20doe@www.google.com/", + &URL{ + Scheme: "ftp", + User: User("john doe"), + Host: "www.google.com", + Path: "/", + }, + "ftp://john%20doe@www.google.com/", + }, + // empty query + { + "http://www.google.com/?", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + ForceQuery: true, + }, + "", + }, + // query ending in question mark (Issue 14573) + { + "http://www.google.com/?foo=bar?", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "foo=bar?", + }, + "", + }, + // query + { + "http://www.google.com/?q=go+language", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + }, + "", + }, + // query with hex escaping: NOT parsed + { + "http://www.google.com/?q=go%20language", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go%20language", + }, + "", + }, + // %20 outside query + { + "http://www.google.com/a%20b?q=c+d", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/a b", + RawQuery: "q=c+d", + }, + "", + }, + // path without leading /, so no parsing + { + "http:www.google.com/?q=go+language", + &URL{ + Scheme: "http", + Opaque: "www.google.com/", + RawQuery: "q=go+language", + }, + "http:www.google.com/?q=go+language", + }, + // path without leading /, so no parsing + { + "http:%2f%2fwww.google.com/?q=go+language", + &URL{ + Scheme: "http", + Opaque: "%2f%2fwww.google.com/", + RawQuery: "q=go+language", + }, + "http:%2f%2fwww.google.com/?q=go+language", + }, + // non-authority with path; see golang.org/issue/46059 + { + "mailto:/webmaster@golang.org", + &URL{ + Scheme: "mailto", + Path: "/webmaster@golang.org", + OmitHost: true, + }, + "", + }, + // non-authority + { + "mailto:webmaster@golang.org", + &URL{ + Scheme: "mailto", + Opaque: "webmaster@golang.org", + }, + "", + }, + // unescaped :// in query should not create a scheme + { + "/foo?query=http://bad", + &URL{ + Path: "/foo", + RawQuery: "query=http://bad", + }, + "", + }, + // leading // without scheme should create an authority + { + "//foo", + &URL{ + Host: "foo", + }, + "", + }, + // leading // without scheme, with userinfo, path, and query + { + "//user@foo/path?a=b", + &URL{ + User: User("user"), + Host: "foo", + Path: "/path", + RawQuery: "a=b", + }, + "", + }, + // Three leading slashes isn't an authority, but doesn't return an error. + // (We can't return an error, as this code is also used via + // ServeHTTP -> ReadRequest -> Parse, which is arguably a + // different URL parsing context, but currently shares the + // same codepath) + { + "///threeslashes", + &URL{ + Path: "///threeslashes", + }, + "", + }, + { + "http://user:password@google.com", + &URL{ + Scheme: "http", + User: UserPassword("user", "password"), + Host: "google.com", + }, + "http://user:password@google.com", + }, + // unescaped @ in username should not confuse host + { + "http://j@ne:password@google.com", + &URL{ + Scheme: "http", + User: UserPassword("j@ne", "password"), + Host: "google.com", + }, + "http://j%40ne:password@google.com", + }, + // unescaped @ in password should not confuse host + { + "http://jane:p@ssword@google.com", + &URL{ + Scheme: "http", + User: UserPassword("jane", "p@ssword"), + Host: "google.com", + }, + "http://jane:p%40ssword@google.com", + }, + { + "http://j@ne:password@google.com/p@th?q=@go", + &URL{ + Scheme: "http", + User: UserPassword("j@ne", "password"), + Host: "google.com", + Path: "/p@th", + RawQuery: "q=@go", + }, + "http://j%40ne:password@google.com/p@th?q=@go", + }, + { + "http://www.google.com/?q=go+language#foo", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + Fragment: "foo", + }, + "", + }, + { + "http://www.google.com/?q=go+language#foo&bar", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + Fragment: "foo&bar", + }, + "http://www.google.com/?q=go+language#foo&bar", + }, + { + "http://www.google.com/?q=go+language#foo%26bar", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + Fragment: "foo&bar", + RawFragment: "foo%26bar", + }, + "http://www.google.com/?q=go+language#foo%26bar", + }, + { + "file:///home/adg/rabbits", + &URL{ + Scheme: "file", + Host: "", + Path: "/home/adg/rabbits", + }, + "file:///home/adg/rabbits", + }, + // "Windows" paths are no exception to the rule. + // See golang.org/issue/6027, especially comment #9. + { + "file:///C:/FooBar/Baz.txt", + &URL{ + Scheme: "file", + Host: "", + Path: "/C:/FooBar/Baz.txt", + }, + "file:///C:/FooBar/Baz.txt", + }, + // case-insensitive scheme + { + "MaIlTo:webmaster@golang.org", + &URL{ + Scheme: "mailto", + Opaque: "webmaster@golang.org", + }, + "mailto:webmaster@golang.org", + }, + // Relative path + { + "a/b/c", + &URL{ + Path: "a/b/c", + }, + "a/b/c", + }, + // escaped '?' in username and password + { + "http://%3Fam:pa%3Fsword@google.com", + &URL{ + Scheme: "http", + User: UserPassword("?am", "pa?sword"), + Host: "google.com", + }, + "", + }, + // host subcomponent; IPv4 address in RFC 3986 + { + "http://192.168.0.1/", + &URL{ + Scheme: "http", + Host: "192.168.0.1", + Path: "/", + }, + "", + }, + // host and port subcomponents; IPv4 address in RFC 3986 + { + "http://192.168.0.1:8080/", + &URL{ + Scheme: "http", + Host: "192.168.0.1:8080", + Path: "/", + }, + "", + }, + // host subcomponent; IPv6 address in RFC 3986 + { + "http://[fe80::1]/", + &URL{ + Scheme: "http", + Host: "[fe80::1]", + Path: "/", + }, + "", + }, + // host and port subcomponents; IPv6 address in RFC 3986 + { + "http://[fe80::1]:8080/", + &URL{ + Scheme: "http", + Host: "[fe80::1]:8080", + Path: "/", + }, + "", + }, + // host subcomponent; IPv6 address with zone identifier in RFC 6874 + { + "http://[fe80::1%25en0]/", // alphanum zone identifier + &URL{ + Scheme: "http", + Host: "[fe80::1%en0]", + Path: "/", + }, + "", + }, + // host and port subcomponents; IPv6 address with zone identifier in RFC 6874 + { + "http://[fe80::1%25en0]:8080/", // alphanum zone identifier + &URL{ + Scheme: "http", + Host: "[fe80::1%en0]:8080", + Path: "/", + }, + "", + }, + // host subcomponent; IPv6 address with zone identifier in RFC 6874 + { + "http://[fe80::1%25%65%6e%301-._~]/", // percent-encoded+unreserved zone identifier + &URL{ + Scheme: "http", + Host: "[fe80::1%en01-._~]", + Path: "/", + }, + "http://[fe80::1%25en01-._~]/", + }, + // host and port subcomponents; IPv6 address with zone identifier in RFC 6874 + { + "http://[fe80::1%25%65%6e%301-._~]:8080/", // percent-encoded+unreserved zone identifier + &URL{ + Scheme: "http", + Host: "[fe80::1%en01-._~]:8080", + Path: "/", + }, + "http://[fe80::1%25en01-._~]:8080/", + }, + // alternate escapings of path survive round trip + { + "http://rest.rsc.io/foo%2fbar/baz%2Fquux?alt=media", + &URL{ + Scheme: "http", + Host: "rest.rsc.io", + Path: "/foo/bar/baz/quux", + RawPath: "/foo%2fbar/baz%2Fquux", + RawQuery: "alt=media", + }, + "", + }, + // issue 12036 + { + "mysql://a,b,c/bar", + &URL{ + Scheme: "mysql", + Host: "a,b,c", + Path: "/bar", + }, + "", + }, + // worst case host, still round trips + { + "scheme://!$&'()*+,;=hello!:1/path", + &URL{ + Scheme: "scheme", + Host: "!$&'()*+,;=hello!:1", + Path: "/path", + }, + "", + }, + // worst case path, still round trips + { + "http://host/!$&'()*+,;=:@[hello]", + &URL{ + Scheme: "http", + Host: "host", + Path: "/!$&'()*+,;=:@[hello]", + RawPath: "/!$&'()*+,;=:@[hello]", + }, + "", + }, + // golang.org/issue/5684 + { + "http://example.com/oid/[order_id]", + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/oid/[order_id]", + RawPath: "/oid/[order_id]", + }, + "", + }, + // golang.org/issue/12200 (colon with empty port) + { + "http://192.168.0.2:8080/foo", + &URL{ + Scheme: "http", + Host: "192.168.0.2:8080", + Path: "/foo", + }, + "", + }, + { + "http://192.168.0.2:/foo", + &URL{ + Scheme: "http", + Host: "192.168.0.2:", + Path: "/foo", + }, + "", + }, + { + // Malformed IPv6 but still accepted. + "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080/foo", + &URL{ + Scheme: "http", + Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080", + Path: "/foo", + }, + "", + }, + { + // Malformed IPv6 but still accepted. + "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:/foo", + &URL{ + Scheme: "http", + Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:", + Path: "/foo", + }, + "", + }, + { + "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080/foo", + &URL{ + Scheme: "http", + Host: "[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080", + Path: "/foo", + }, + "", + }, + { + "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:/foo", + &URL{ + Scheme: "http", + Host: "[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:", + Path: "/foo", + }, + "", + }, + // golang.org/issue/7991 and golang.org/issue/12719 (non-ascii %-encoded in host) + { + "http://hello.世界.com/foo", + &URL{ + Scheme: "http", + Host: "hello.世界.com", + Path: "/foo", + }, + "http://hello.%E4%B8%96%E7%95%8C.com/foo", + }, + { + "http://hello.%e4%b8%96%e7%95%8c.com/foo", + &URL{ + Scheme: "http", + Host: "hello.世界.com", + Path: "/foo", + }, + "http://hello.%E4%B8%96%E7%95%8C.com/foo", + }, + { + "http://hello.%E4%B8%96%E7%95%8C.com/foo", + &URL{ + Scheme: "http", + Host: "hello.世界.com", + Path: "/foo", + }, + "", + }, + // golang.org/issue/10433 (path beginning with //) + { + "http://example.com//foo", + &URL{ + Scheme: "http", + Host: "example.com", + Path: "//foo", + }, + "", + }, + // test that we can reparse the host names we accept. + { + "myscheme://authority<\"hi\">/foo", + &URL{ + Scheme: "myscheme", + Host: "authority<\"hi\">", + Path: "/foo", + }, + "", + }, + // spaces in hosts are disallowed but escaped spaces in IPv6 scope IDs are grudgingly OK. + // This happens on Windows. + // golang.org/issue/14002 + { + "tcp://[2020::2020:20:2020:2020%25Windows%20Loves%20Spaces]:2020", + &URL{ + Scheme: "tcp", + Host: "[2020::2020:20:2020:2020%Windows Loves Spaces]:2020", + }, + "", + }, + // test we can roundtrip magnet url + // fix issue https://golang.org/issue/20054 + { + "magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", + &URL{ + Scheme: "magnet", + Host: "", + Path: "", + RawQuery: "xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", + }, + "magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", + }, + { + "mailto:?subject=hi", + &URL{ + Scheme: "mailto", + Host: "", + Path: "", + RawQuery: "subject=hi", + }, + "mailto:?subject=hi", + }, +} + +// more useful string for debugging than fmt's struct printer +func ufmt(u *URL) string { + var user, pass any + if u.User != nil { + user = u.User.Username() + if p, ok := u.User.Password(); ok { + pass = p + } + } + return fmt.Sprintf("opaque=%q, scheme=%q, user=%#v, pass=%#v, host=%q, path=%q, rawpath=%q, rawq=%q, frag=%q, rawfrag=%q, forcequery=%v, omithost=%t", + u.Opaque, u.Scheme, user, pass, u.Host, u.Path, u.RawPath, u.RawQuery, u.Fragment, u.RawFragment, u.ForceQuery, u.OmitHost) +} + +func BenchmarkString(b *testing.B) { + b.StopTimer() + b.ReportAllocs() + for _, tt := range urltests { + u, err := Parse(tt.in) + if err != nil { + b.Errorf("Parse(%q) returned error %s", tt.in, err) + continue + } + if tt.roundtrip == "" { + continue + } + b.StartTimer() + var g string + for i := 0; i < b.N; i++ { + g = u.String() + } + b.StopTimer() + if w := tt.roundtrip; b.N > 0 && g != w { + b.Errorf("Parse(%q).String() == %q, want %q", tt.in, g, w) + } + } +} + +func TestParse(t *testing.T) { + for _, tt := range urltests { + u, err := Parse(tt.in) + if err != nil { + t.Errorf("Parse(%q) returned error %v", tt.in, err) + continue + } + if !reflect.DeepEqual(u, tt.out) { + t.Errorf("Parse(%q):\n\tgot %v\n\twant %v\n", tt.in, ufmt(u), ufmt(tt.out)) + } + } +} + +const pathThatLooksSchemeRelative = "//not.a.user@not.a.host/just/a/path" + +var parseRequestURLTests = []struct { + url string + expectedValid bool +}{ + {"http://foo.com", true}, + {"http://foo.com/", true}, + {"http://foo.com/path", true}, + {"/", true}, + {pathThatLooksSchemeRelative, true}, + {"//not.a.user@%66%6f%6f.com/just/a/path/also", true}, + {"*", true}, + {"http://192.168.0.1/", true}, + {"http://192.168.0.1:8080/", true}, + {"http://[fe80::1]/", true}, + {"http://[fe80::1]:8080/", true}, + + // Tests exercising RFC 6874 compliance: + {"http://[fe80::1%25en0]/", true}, // with alphanum zone identifier + {"http://[fe80::1%25en0]:8080/", true}, // with alphanum zone identifier + {"http://[fe80::1%25%65%6e%301-._~]/", true}, // with percent-encoded+unreserved zone identifier + {"http://[fe80::1%25%65%6e%301-._~]:8080/", true}, // with percent-encoded+unreserved zone identifier + + {"foo.html", false}, + {"../dir/", false}, + {" http://foo.com", false}, + {"http://192.168.0.%31/", false}, + {"http://192.168.0.%31:8080/", false}, + {"http://[fe80::%31]/", false}, + {"http://[fe80::%31]:8080/", false}, + {"http://[fe80::%31%25en0]/", false}, + {"http://[fe80::%31%25en0]:8080/", false}, + + // These two cases are valid as textual representations as + // described in RFC 4007, but are not valid as address + // literals with IPv6 zone identifiers in URIs as described in + // RFC 6874. However, this seems to be overridden by + // https://url.spec.whatwg.org/#percent-encoded-bytes + // which permits unencoded % characters. + {"http://[fe80::1%en0]/", true}, + {"http://[fe80::1%en0]:8080/", true}, +} + +func TestParseRequestURI(t *testing.T) { + for _, test := range parseRequestURLTests { + _, err := ParseRequestURI(test.url) + if test.expectedValid && err != nil { + t.Errorf("ParseRequestURI(%q) gave err %v; want no error", test.url, err) + } else if !test.expectedValid && err == nil { + t.Errorf("ParseRequestURI(%q) gave nil error; want some error", test.url) + } + } + + url, err := ParseRequestURI(pathThatLooksSchemeRelative) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if url.Path != pathThatLooksSchemeRelative { + t.Errorf("ParseRequestURI path:\ngot %q\nwant %q", url.Path, pathThatLooksSchemeRelative) + } +} + +var stringURLTests = []struct { + url URL + want string +}{ + // No leading slash on path should prepend slash on String() call + { + url: URL{ + Scheme: "http", + Host: "www.google.com", + Path: "search", + }, + want: "http://www.google.com/search", + }, + // Relative path with first element containing ":" should be prepended with "./", golang.org/issue/17184 + { + url: URL{ + Path: "this:that", + }, + want: "./this:that", + }, + // Relative path with second element containing ":" should not be prepended with "./" + { + url: URL{ + Path: "here/this:that", + }, + want: "here/this:that", + }, + // Non-relative path with first element containing ":" should not be prepended with "./" + { + url: URL{ + Scheme: "http", + Host: "www.google.com", + Path: "this:that", + }, + want: "http://www.google.com/this:that", + }, +} + +func TestURLString(t *testing.T) { + for _, tt := range urltests { + u, err := Parse(tt.in) + if err != nil { + t.Errorf("Parse(%q) returned error %s", tt.in, err) + continue + } + expected := tt.in + if tt.roundtrip != "" { + expected = tt.roundtrip + } + s := u.String() + if s != expected { + t.Errorf("Parse(%q).String() == %q (expected %q)", tt.in, s, expected) + } + } + + for _, tt := range stringURLTests { + if got := tt.url.String(); got != tt.want { + t.Errorf("%+v.String() = %q; want %q", tt.url, got, tt.want) + } + } +} + +func TestURLRedacted(t *testing.T) { + cases := []struct { + name string + url *URL + want string + }{ + { + name: "non-blank Password", + url: &URL{ + Scheme: "http", + Host: "host.tld", + Path: "this:that", + User: UserPassword("user", "password"), + }, + want: "http://user:xxxxx@host.tld/this:that", + }, + { + name: "blank Password", + url: &URL{ + Scheme: "http", + Host: "host.tld", + Path: "this:that", + User: User("user"), + }, + want: "http://user@host.tld/this:that", + }, + { + name: "nil User", + url: &URL{ + Scheme: "http", + Host: "host.tld", + Path: "this:that", + User: UserPassword("", "password"), + }, + want: "http://:xxxxx@host.tld/this:that", + }, + { + name: "blank Username, blank Password", + url: &URL{ + Scheme: "http", + Host: "host.tld", + Path: "this:that", + }, + want: "http://host.tld/this:that", + }, + { + name: "empty URL", + url: &URL{}, + want: "", + }, + { + name: "nil URL", + url: nil, + want: "", + }, + } + + for _, tt := range cases { + t := t + t.Run(tt.name, func(t *testing.T) { + if g, w := tt.url.Redacted(), tt.want; g != w { + t.Fatalf("got: %q\nwant: %q", g, w) + } + }) + } +} + +type EscapeTest struct { + in string + out string + err error +} + +var unescapeTests = []EscapeTest{ + { + "", + "", + nil, + }, + { + "abc", + "abc", + nil, + }, + { + "1%41", + "1A", + nil, + }, + { + "1%41%42%43", + "1ABC", + nil, + }, + { + "%4a", + "J", + nil, + }, + { + "%6F", + "o", + nil, + }, + { + "%", // not enough characters after % + "%", + nil, + }, + { + "%a", // not enough characters after % + "%a", + nil, + }, + { + "%1", // not enough characters after % + "%1", + nil, + }, + { + "123%45%6", // not enough characters after % + "123E%6", + nil, + }, + { + "%zzzzz", // invalid hex digits + "%zzzzz", + nil, + }, + { + "a+b", + "a b", + nil, + }, + { + "a%20b", + "a b", + nil, + }, +} + +func TestUnescape(t *testing.T) { + for _, tt := range unescapeTests { + actual, err := QueryUnescape(tt.in) + if actual != tt.out || (err != nil) != (tt.err != nil) { + t.Errorf("QueryUnescape(%q) = %q, %s; want %q, %s", tt.in, actual, err, tt.out, tt.err) + } + + in := tt.in + out := tt.out + if strings.Contains(tt.in, "+") { + in = strings.ReplaceAll(tt.in, "+", "%20") + actual, err := PathUnescape(in) + if actual != tt.out || (err != nil) != (tt.err != nil) { + t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", in, actual, err, tt.out, tt.err) + } + if tt.err == nil { + s, err := QueryUnescape(strings.ReplaceAll(tt.in, "+", "XXX")) + if err != nil { + continue + } + in = tt.in + out = strings.ReplaceAll(s, "XXX", "+") + } + } + + actual, err = PathUnescape(in) + if actual != out || (err != nil) != (tt.err != nil) { + t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", in, actual, err, out, tt.err) + } + } +} + +var queryEscapeTests = []EscapeTest{ + { + "", + "", + nil, + }, + { + "abc", + "abc", + nil, + }, + { + "one two", + "one+two", + nil, + }, + { + "10%", + "10%25", + nil, + }, + { + " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;", + "+%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A%2F%40%24%27%28%29%2A%2C%3B", + nil, + }, +} + +func TestQueryEscape(t *testing.T) { + for _, tt := range queryEscapeTests { + actual := QueryEscape(tt.in) + if tt.out != actual { + t.Errorf("QueryEscape(%q) = %q, want %q", tt.in, actual, tt.out) + } + + // for bonus points, verify that escape:unescape is an identity. + roundtrip, err := QueryUnescape(actual) + if roundtrip != tt.in || err != nil { + t.Errorf("QueryUnescape(%q) = %q, %s; want %q, %s", actual, roundtrip, err, tt.in, "[no error]") + } + } +} + +var pathEscapeTests = []EscapeTest{ + { + "", + "", + nil, + }, + { + "abc", + "abc", + nil, + }, + { + "abc+def", + "abc+def", + nil, + }, + { + "a/b", + "a%2Fb", + nil, + }, + { + "one two", + "one%20two", + nil, + }, + { + "10%", + "10%25", + nil, + }, + { + " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;", + "%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B", + nil, + }, +} + +func TestPathEscape(t *testing.T) { + for _, tt := range pathEscapeTests { + actual := PathEscape(tt.in) + if tt.out != actual { + t.Errorf("PathEscape(%q) = %q, want %q", tt.in, actual, tt.out) + } + + // for bonus points, verify that escape:unescape is an identity. + roundtrip, err := PathUnescape(actual) + if roundtrip != tt.in || err != nil { + t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", actual, roundtrip, err, tt.in, "[no error]") + } + } +} + +//var userinfoTests = []UserinfoTest{ +// {"user", "password", "user:password"}, +// {"foo:bar", "~!@#$%^&*()_+{}|[]\\-=`:;'\"<>?,./", +// "foo%3Abar:~!%40%23$%25%5E&*()_+%7B%7D%7C%5B%5D%5C-=%60%3A;'%22%3C%3E?,.%2F"}, +//} + +type EncodeQueryTest struct { + m Values + expected string +} + +var encodeQueryTests = []EncodeQueryTest{ + {nil, ""}, + {Values{"q": {"puppies"}, "oe": {"utf8"}}, "oe=utf8&q=puppies"}, + {Values{"q": {"dogs", "&", "7"}}, "q=dogs&q=%26&q=7"}, + {Values{ + "a": {"a1", "a2", "a3"}, + "b": {"b1", "b2", "b3"}, + "c": {"c1", "c2", "c3"}, + }, "a=a1&a=a2&a=a3&b=b1&b=b2&b=b3&c=c1&c=c2&c=c3"}, +} + +func TestEncodeQuery(t *testing.T) { + for _, tt := range encodeQueryTests { + if q := tt.m.Encode(); q != tt.expected { + t.Errorf(`EncodeQuery(%+v) = %q, want %q`, tt.m, q, tt.expected) + } + } +} + +var resolvePathTests = []struct { + base, ref, expected string +}{ + {"a/b", ".", "/a/"}, + {"a/b", "c", "/a/c"}, + {"a/b", "..", "/"}, + {"a/", "..", "/"}, + {"a/", "../..", "/"}, + {"a/b/c", "..", "/a/"}, + {"a/b/c", "../d", "/a/d"}, + {"a/b/c", ".././d", "/a/d"}, + {"a/b", "./..", "/"}, + {"a/./b", ".", "/a/"}, + {"a/../", ".", "/"}, + {"a/.././b", "c", "/c"}, +} + +func TestResolvePath(t *testing.T) { + for _, test := range resolvePathTests { + got := resolvePath(test.base, test.ref) + if got != test.expected { + t.Errorf("For %q + %q got %q; expected %q", test.base, test.ref, got, test.expected) + } + } +} + +func BenchmarkResolvePath(b *testing.B) { + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + resolvePath("a/b/c", ".././d") + } +} + +var resolveReferenceTests = []struct { + base, rel, expected string +}{ + // Absolute URL references + {"http://foo.com?a=b", "https://bar.com/", "https://bar.com/"}, + {"http://foo.com/", "https://bar.com/?a=b", "https://bar.com/?a=b"}, + {"http://foo.com/", "https://bar.com/?", "https://bar.com/?"}, + {"http://foo.com/bar", "mailto:foo@example.com", "mailto:foo@example.com"}, + + // Path-absolute references + {"http://foo.com/bar", "/baz", "http://foo.com/baz"}, + {"http://foo.com/bar?a=b#f", "/baz", "http://foo.com/baz"}, + {"http://foo.com/bar?a=b", "/baz?", "http://foo.com/baz?"}, + {"http://foo.com/bar?a=b", "/baz?c=d", "http://foo.com/baz?c=d"}, + + // Multiple slashes + {"http://foo.com/bar", "http://foo.com//baz", "http://foo.com//baz"}, + {"http://foo.com/bar", "http://foo.com///baz/quux", "http://foo.com///baz/quux"}, + + // Scheme-relative + {"https://foo.com/bar?a=b", "//bar.com/quux", "https://bar.com/quux"}, + + // Path-relative references: + + // ... current directory + {"http://foo.com", ".", "http://foo.com/"}, + {"http://foo.com/bar", ".", "http://foo.com/"}, + {"http://foo.com/bar/", ".", "http://foo.com/bar/"}, + + // ... going down + {"http://foo.com", "bar", "http://foo.com/bar"}, + {"http://foo.com/", "bar", "http://foo.com/bar"}, + {"http://foo.com/bar/baz", "quux", "http://foo.com/bar/quux"}, + + // ... going up + {"http://foo.com/bar/baz", "../quux", "http://foo.com/quux"}, + {"http://foo.com/bar/baz", "../../../../../quux", "http://foo.com/quux"}, + {"http://foo.com/bar", "..", "http://foo.com/"}, + {"http://foo.com/bar/baz", "./..", "http://foo.com/"}, + // ".." in the middle (issue 3560) + {"http://foo.com/bar/baz", "quux/dotdot/../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/.././tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/./../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/././../../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/./.././../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/dotdot/./../../.././././tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/../dotdot/../dot/./tail/..", "http://foo.com/bar/quux/dot/"}, + + // Remove any dot-segments prior to forming the target URI. + // https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 + {"http://foo.com/dot/./dotdot/../foo/bar", "../baz", "http://foo.com/dot/baz"}, + + // Triple dot isn't special + {"http://foo.com/bar", "...", "http://foo.com/..."}, + + // Fragment + {"http://foo.com/bar", ".#frag", "http://foo.com/#frag"}, + {"http://example.org/", "#!$&%27()*+,;=", "http://example.org/#!$&%27()*+,;="}, + + // Paths with escaping (issue 16947). + {"http://foo.com/foo%2fbar/", "../baz", "http://foo.com/baz"}, + {"http://foo.com/1/2%2f/3%2f4/5", "../../a/b/c", "http://foo.com/1/a/b/c"}, + {"http://foo.com/1/2/3", "./a%2f../../b/..%2fc", "http://foo.com/1/2/b/..%2fc"}, + {"http://foo.com/1/2%2f/3%2f4/5", "./a%2f../b/../c", "http://foo.com/1/2%2f/3%2f4/a%2f../c"}, + {"http://foo.com/foo%20bar/", "../baz", "http://foo.com/baz"}, + {"http://foo.com/foo", "../bar%2fbaz", "http://foo.com/bar%2fbaz"}, + {"http://foo.com/foo%2dbar/", "./baz-quux", "http://foo.com/foo%2dbar/baz-quux"}, + + // RFC 3986: Normal Examples + // https://datatracker.ietf.org/doc/html/rfc3986#section-5.4.1 + {"http://a/b/c/d;p?q", "g:h", "g:h"}, + {"http://a/b/c/d;p?q", "g", "http://a/b/c/g"}, + {"http://a/b/c/d;p?q", "./g", "http://a/b/c/g"}, + {"http://a/b/c/d;p?q", "g/", "http://a/b/c/g/"}, + {"http://a/b/c/d;p?q", "/g", "http://a/g"}, + {"http://a/b/c/d;p?q", "//g", "http://g"}, + {"http://a/b/c/d;p?q", "?y", "http://a/b/c/d;p?y"}, + {"http://a/b/c/d;p?q", "g?y", "http://a/b/c/g?y"}, + {"http://a/b/c/d;p?q", "#s", "http://a/b/c/d;p?q#s"}, + {"http://a/b/c/d;p?q", "g#s", "http://a/b/c/g#s"}, + {"http://a/b/c/d;p?q", "g?y#s", "http://a/b/c/g?y#s"}, + {"http://a/b/c/d;p?q", ";x", "http://a/b/c/;x"}, + {"http://a/b/c/d;p?q", "g;x", "http://a/b/c/g;x"}, + {"http://a/b/c/d;p?q", "g;x?y#s", "http://a/b/c/g;x?y#s"}, + {"http://a/b/c/d;p?q", "", "http://a/b/c/d;p?q"}, + {"http://a/b/c/d;p?q", ".", "http://a/b/c/"}, + {"http://a/b/c/d;p?q", "./", "http://a/b/c/"}, + {"http://a/b/c/d;p?q", "..", "http://a/b/"}, + {"http://a/b/c/d;p?q", "../", "http://a/b/"}, + {"http://a/b/c/d;p?q", "../g", "http://a/b/g"}, + {"http://a/b/c/d;p?q", "../..", "http://a/"}, + {"http://a/b/c/d;p?q", "../../", "http://a/"}, + {"http://a/b/c/d;p?q", "../../g", "http://a/g"}, + + // RFC 3986: Abnormal Examples + // https://datatracker.ietf.org/doc/html/rfc3986#section-5.4.2 + {"http://a/b/c/d;p?q", "../../../g", "http://a/g"}, + {"http://a/b/c/d;p?q", "../../../../g", "http://a/g"}, + {"http://a/b/c/d;p?q", "/./g", "http://a/g"}, + {"http://a/b/c/d;p?q", "/../g", "http://a/g"}, + {"http://a/b/c/d;p?q", "g.", "http://a/b/c/g."}, + {"http://a/b/c/d;p?q", ".g", "http://a/b/c/.g"}, + {"http://a/b/c/d;p?q", "g..", "http://a/b/c/g.."}, + {"http://a/b/c/d;p?q", "..g", "http://a/b/c/..g"}, + {"http://a/b/c/d;p?q", "./../g", "http://a/b/g"}, + {"http://a/b/c/d;p?q", "./g/.", "http://a/b/c/g/"}, + {"http://a/b/c/d;p?q", "g/./h", "http://a/b/c/g/h"}, + {"http://a/b/c/d;p?q", "g/../h", "http://a/b/c/h"}, + {"http://a/b/c/d;p?q", "g;x=1/./y", "http://a/b/c/g;x=1/y"}, + {"http://a/b/c/d;p?q", "g;x=1/../y", "http://a/b/c/y"}, + {"http://a/b/c/d;p?q", "g?y/./x", "http://a/b/c/g?y/./x"}, + {"http://a/b/c/d;p?q", "g?y/../x", "http://a/b/c/g?y/../x"}, + {"http://a/b/c/d;p?q", "g#s/./x", "http://a/b/c/g#s/./x"}, + {"http://a/b/c/d;p?q", "g#s/../x", "http://a/b/c/g#s/../x"}, + + // Extras. + {"https://a/b/c/d;p?q", "//g?q", "https://g?q"}, + {"https://a/b/c/d;p?q", "//g#s", "https://g#s"}, + {"https://a/b/c/d;p?q", "//g/d/e/f?y#s", "https://g/d/e/f?y#s"}, + {"https://a/b/c/d;p#s", "?y", "https://a/b/c/d;p?y"}, + {"https://a/b/c/d;p?q#s", "?y", "https://a/b/c/d;p?y"}, + + // Empty path and query but with ForceQuery (issue 46033). + {"https://a/b/c/d;p?q#s", "?", "https://a/b/c/d;p?"}, +} + +func TestResolveReference(t *testing.T) { + mustParse := func(url string) *URL { + u, err := Parse(url) + if err != nil { + t.Fatalf("Parse(%q) got err %v", url, err) + } + return u + } + opaque := &URL{Scheme: "scheme", Opaque: "opaque"} + for _, test := range resolveReferenceTests { + base := mustParse(test.base) + rel := mustParse(test.rel) + url := base.ResolveReference(rel) + if got := url.String(); got != test.expected { + t.Errorf("URL(%q).ResolveReference(%q)\ngot %q\nwant %q", test.base, test.rel, got, test.expected) + } + // Ensure that new instances are returned. + if base == url { + t.Errorf("Expected URL.ResolveReference to return new URL instance.") + } + // Test the convenience wrapper too. + url, err := base.Parse(test.rel) + if err != nil { + t.Errorf("URL(%q).Parse(%q) failed: %v", test.base, test.rel, err) + } else if got := url.String(); got != test.expected { + t.Errorf("URL(%q).Parse(%q)\ngot %q\nwant %q", test.base, test.rel, got, test.expected) + } else if base == url { + // Ensure that new instances are returned for the wrapper too. + t.Errorf("Expected URL.Parse to return new URL instance.") + } + // Ensure Opaque resets the URL. + url = base.ResolveReference(opaque) + if *url != *opaque { + t.Errorf("ResolveReference failed to resolve opaque URL:\ngot %#v\nwant %#v", url, opaque) + } + // Test the convenience wrapper with an opaque URL too. + url, err = base.Parse("scheme:opaque") + if err != nil { + t.Errorf(`URL(%q).Parse("scheme:opaque") failed: %v`, test.base, err) + } else if *url != *opaque { + t.Errorf("Parse failed to resolve opaque URL:\ngot %#v\nwant %#v", opaque, url) + } else if base == url { + // Ensure that new instances are returned, again. + t.Errorf("Expected URL.Parse to return new URL instance.") + } + } +} + +func TestQueryValues(t *testing.T) { + u, _ := Parse("http://x.com?foo=bar&bar=1&bar=2&baz") + v := u.Query() + if len(v) != 3 { + t.Errorf("got %d keys in Query values, want 3", len(v)) + } + if g, e := v.Get("foo"), "bar"; g != e { + t.Errorf("Get(foo) = %q, want %q", g, e) + } + // Case sensitive: + if g, e := v.Get("Foo"), ""; g != e { + t.Errorf("Get(Foo) = %q, want %q", g, e) + } + if g, e := v.Get("bar"), "1"; g != e { + t.Errorf("Get(bar) = %q, want %q", g, e) + } + if g, e := v.Get("baz"), ""; g != e { + t.Errorf("Get(baz) = %q, want %q", g, e) + } + if h, e := v.Has("foo"), true; h != e { + t.Errorf("Has(foo) = %t, want %t", h, e) + } + if h, e := v.Has("bar"), true; h != e { + t.Errorf("Has(bar) = %t, want %t", h, e) + } + if h, e := v.Has("baz"), true; h != e { + t.Errorf("Has(baz) = %t, want %t", h, e) + } + if h, e := v.Has("noexist"), false; h != e { + t.Errorf("Has(noexist) = %t, want %t", h, e) + } + v.Del("bar") + if g, e := v.Get("bar"), ""; g != e { + t.Errorf("second Get(bar) = %q, want %q", g, e) + } +} + +type parseTest struct { + query string + out Values + ok bool +} + +var parseTests = []parseTest{ + { + query: "a=1", + out: Values{"a": []string{"1"}}, + ok: true, + }, + { + query: "a=1&b=2", + out: Values{"a": []string{"1"}, "b": []string{"2"}}, + ok: true, + }, + { + query: "a=1&a=2&a=banana", + out: Values{"a": []string{"1", "2", "banana"}}, + ok: true, + }, + { + query: "ascii=%3Ckey%3A+0x90%3E", + out: Values{"ascii": []string{""}}, + ok: true, + }, { + query: "a=1;b=2", + out: Values{}, + ok: false, + }, { + query: "a;b=1", + out: Values{}, + ok: false, + }, { + query: "a=%3B", // hex encoding for semicolon + out: Values{"a": []string{";"}}, + ok: true, + }, + { + query: "a%3Bb=1", + out: Values{"a;b": []string{"1"}}, + ok: true, + }, + { + query: "a=1&a=2;a=banana", + out: Values{"a": []string{"1"}}, + ok: false, + }, + { + query: "a;b&c=1", + out: Values{"c": []string{"1"}}, + ok: false, + }, + { + query: "a=1&b=2;a=3&c=4", + out: Values{"a": []string{"1"}, "c": []string{"4"}}, + ok: false, + }, + { + query: "a=1&b=2;c=3", + out: Values{"a": []string{"1"}}, + ok: false, + }, + { + query: ";", + out: Values{}, + ok: false, + }, + { + query: "a=1;", + out: Values{}, + ok: false, + }, + { + query: "a=1&;", + out: Values{"a": []string{"1"}}, + ok: false, + }, + { + query: ";a=1&b=2", + out: Values{"b": []string{"2"}}, + ok: false, + }, + { + query: "a=1&b=2;", + out: Values{"a": []string{"1"}}, + ok: false, + }, +} + +func TestParseQuery(t *testing.T) { + for _, test := range parseTests { + t.Run(test.query, func(t *testing.T) { + form, err := ParseQuery(test.query) + if test.ok != (err == nil) { + want := "" + if test.ok { + want = "" + } + t.Errorf("Unexpected error: %v, want %v", err, want) + } + if len(form) != len(test.out) { + t.Errorf("len(form) = %d, want %d", len(form), len(test.out)) + } + for k, evs := range test.out { + vs, ok := form[k] + if !ok { + t.Errorf("Missing key %q", k) + continue + } + if len(vs) != len(evs) { + t.Errorf("len(form[%q]) = %d, want %d", k, len(vs), len(evs)) + continue + } + for j, ev := range evs { + if v := vs[j]; v != ev { + t.Errorf("form[%q][%d] = %q, want %q", k, j, v, ev) + } + } + } + }) + } +} + +type RequestURITest struct { + url *URL + out string +} + +var requritests = []RequestURITest{ + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "", + }, + "/", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/a b", + }, + "/a%20b", + }, + // golang.org/issue/4860 variant 1 + { + &URL{ + Scheme: "http", + Host: "example.com", + Opaque: "/%2F/%2F/", + }, + "/%2F/%2F/", + }, + // golang.org/issue/4860 variant 2 + { + &URL{ + Scheme: "http", + Host: "example.com", + Opaque: "//other.example.com/%2F/%2F/", + }, + "http://other.example.com/%2F/%2F/", + }, + // better fix for issue 4860 + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/////", + RawPath: "/%2F/%2F/", + }, + "/%2F/%2F/", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/////", + RawPath: "/WRONG/", // ignored because doesn't match Path + }, + "/////", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/a b", + RawQuery: "q=go+language", + }, + "/a%20b?q=go+language", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/a b", + RawPath: "/a b", // ignored because invalid + RawQuery: "q=go+language", + }, + "/a%20b?q=go+language", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/a?b", + RawPath: "/a?b", // ignored because invalid + RawQuery: "q=go+language", + }, + "/a%3Fb?q=go+language", + }, + { + &URL{ + Scheme: "myschema", + Opaque: "opaque", + }, + "opaque", + }, + { + &URL{ + Scheme: "myschema", + Opaque: "opaque", + RawQuery: "q=go+language", + }, + "opaque?q=go+language", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "//foo", + }, + "//foo", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/foo", + ForceQuery: true, + }, + "/foo?", + }, +} + +func TestRequestURI(t *testing.T) { + for _, tt := range requritests { + s := tt.url.RequestURI() + if s != tt.out { + t.Errorf("%#v.RequestURI() == %q (expected %q)", tt.url, s, tt.out) + } + } +} + +func TestParseErrors(t *testing.T) { + tests := []struct { + in string + wantErr bool + }{ + {"http://[::1]", false}, + {"http://[::1]:80", false}, + {"http://[::1]:namedport", true}, // rfc3986 3.2.3 + {"http://x:namedport", true}, // rfc3986 3.2.3 + {"http://[::1]/", false}, + {"http://[::1]a", true}, + {"http://[::1]%23", true}, + {"http://[::1%25en0]", false}, // valid zone id + {"http://[::1]:", false}, // colon, but no port OK + {"http://x:", false}, // colon, but no port OK + {"http://[::1]:%38%30", true}, // not allowed: % encoding only for non-ASCII + {"http://[::1%25%41]", false}, // RFC 6874 allows over-escaping in zone + {"http://[%10::1]", true}, // no %xx escapes in IP address + {"http://[::1]/%48", false}, // %xx in path is fine + {"http://%41:8080/", true}, // not allowed: % encoding only for non-ASCII + {"mysql://x@y(z:123)/foo", true}, // not well-formed per RFC 3986, golang.org/issue/33646 + {"mysql://x@y(1.2.3.4:123)/foo", true}, + + {" http://foo.com", true}, // invalid character in schema + {"ht tp://foo.com", true}, // invalid character in schema + {"ahttp://foo.com", false}, // valid schema characters + {"1http://foo.com", true}, // invalid character in schema + + {"http://[]%20%48%54%54%50%2f%31%2e%31%0a%4d%79%48%65%61%64%65%72%3a%20%31%32%33%0a%0a/", true}, // golang.org/issue/11208 + {"http://a b.com/", true}, // no space in host name please + {"cache_object://foo", true}, // scheme cannot have _, relative path cannot have : in first segment + {"cache_object:foo", true}, + {"cache_object:foo/bar", true}, + {"cache_object/:foo/bar", false}, + } + for _, tt := range tests { + u, err := Parse(tt.in) + if tt.wantErr { + if err == nil { + t.Errorf("Parse(%q) = %#v; want an error", tt.in, u) + } + continue + } + if err != nil { + t.Errorf("Parse(%q) = %v; want no error", tt.in, err) + } + } +} + +// Issue 11202 +func TestStarRequest(t *testing.T) { + u, err := Parse("*") + if err != nil { + t.Fatal(err) + } + if got, want := u.RequestURI(), "*"; got != want { + t.Errorf("RequestURI = %q; want %q", got, want) + } +} + +type shouldEscapeTest struct { + in byte + mode encoding + escape bool +} + +var shouldEscapeTests = []shouldEscapeTest{ + // Unreserved characters (§2.3) + {'a', encodePath, false}, + {'a', encodeUserPassword, false}, + {'a', encodeQueryComponent, false}, + {'a', encodeFragment, false}, + {'a', encodeHost, false}, + {'z', encodePath, false}, + {'A', encodePath, false}, + {'Z', encodePath, false}, + {'0', encodePath, false}, + {'9', encodePath, false}, + {'-', encodePath, false}, + {'-', encodeUserPassword, false}, + {'-', encodeQueryComponent, false}, + {'-', encodeFragment, false}, + {'.', encodePath, false}, + {'_', encodePath, false}, + {'~', encodePath, false}, + + // User information (§3.2.1) + {':', encodeUserPassword, true}, + {'/', encodeUserPassword, true}, + {'?', encodeUserPassword, true}, + {'@', encodeUserPassword, true}, + {'$', encodeUserPassword, false}, + {'&', encodeUserPassword, false}, + {'+', encodeUserPassword, false}, + {',', encodeUserPassword, false}, + {';', encodeUserPassword, false}, + {'=', encodeUserPassword, false}, + + // Host (IP address, IPv6 address, registered name, port suffix; §3.2.2) + {'!', encodeHost, false}, + {'$', encodeHost, false}, + {'&', encodeHost, false}, + {'\'', encodeHost, false}, + {'(', encodeHost, false}, + {')', encodeHost, false}, + {'*', encodeHost, false}, + {'+', encodeHost, false}, + {',', encodeHost, false}, + {';', encodeHost, false}, + {'=', encodeHost, false}, + {':', encodeHost, false}, + {'[', encodeHost, false}, + {']', encodeHost, false}, + {'0', encodeHost, false}, + {'9', encodeHost, false}, + {'A', encodeHost, false}, + {'z', encodeHost, false}, + {'_', encodeHost, false}, + {'-', encodeHost, false}, + {'.', encodeHost, false}, +} + +func TestShouldEscape(t *testing.T) { + for _, tt := range shouldEscapeTests { + if shouldEscape(tt.in, tt.mode) != tt.escape { + t.Errorf("shouldEscape(%q, %v) returned %v; expected %v", tt.in, tt.mode, !tt.escape, tt.escape) + } + } +} + +type timeoutError struct { + timeout bool +} + +func (e *timeoutError) Error() string { return "timeout error" } +func (e *timeoutError) Timeout() bool { return e.timeout } + +type temporaryError struct { + temporary bool +} + +func (e *temporaryError) Error() string { return "temporary error" } +func (e *temporaryError) Temporary() bool { return e.temporary } + +type timeoutTemporaryError struct { + timeoutError + temporaryError +} + +func (e *timeoutTemporaryError) Error() string { return "timeout/temporary error" } + +var netErrorTests = []struct { + err error + timeout bool + temporary bool +}{{ + err: &Error{"Get", "http://google.com/", &timeoutError{timeout: true}}, + timeout: true, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", &timeoutError{timeout: false}}, + timeout: false, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", &temporaryError{temporary: true}}, + timeout: false, + temporary: true, +}, { + err: &Error{"Get", "http://google.com/", &temporaryError{temporary: false}}, + timeout: false, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: true}, temporaryError{temporary: true}}}, + timeout: true, + temporary: true, +}, { + err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: false}, temporaryError{temporary: true}}}, + timeout: false, + temporary: true, +}, { + err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: true}, temporaryError{temporary: false}}}, + timeout: true, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: false}, temporaryError{temporary: false}}}, + timeout: false, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", io.EOF}, + timeout: false, + temporary: false, +}} + +// Test that url.Error implements net.Error and that it forwards +func TestURLErrorImplementsNetError(t *testing.T) { + for i, tt := range netErrorTests { + err, ok := tt.err.(net.Error) + if !ok { + t.Errorf("%d: %T does not implement net.Error", i+1, tt.err) + continue + } + if err.Timeout() != tt.timeout { + t.Errorf("%d: err.Timeout(): got %v, want %v", i+1, err.Timeout(), tt.timeout) + continue + } + if err.Temporary() != tt.temporary { + t.Errorf("%d: err.Temporary(): got %v, want %v", i+1, err.Temporary(), tt.temporary) + } + } +} + +func TestURLHostnameAndPort(t *testing.T) { + tests := []struct { + in string // URL.Host field + host string + port string + }{ + {"foo.com:80", "foo.com", "80"}, + {"foo.com", "foo.com", ""}, + {"foo.com:", "foo.com", ""}, + {"FOO.COM", "FOO.COM", ""}, // no canonicalization + {"1.2.3.4", "1.2.3.4", ""}, + {"1.2.3.4:80", "1.2.3.4", "80"}, + {"[1:2:3:4]", "1:2:3:4", ""}, + {"[1:2:3:4]:80", "1:2:3:4", "80"}, + {"[::1]:80", "::1", "80"}, + {"[::1]", "::1", ""}, + {"[::1]:", "::1", ""}, + {"localhost", "localhost", ""}, + {"localhost:443", "localhost", "443"}, + {"some.super.long.domain.example.org:8080", "some.super.long.domain.example.org", "8080"}, + {"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:17000", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", "17000"}, + {"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ""}, + + // Ensure that even when not valid, Host is one of "Hostname", + // "Hostname:Port", "[Hostname]" or "[Hostname]:Port". + // See https://golang.org/issue/29098. + {"[google.com]:80", "google.com", "80"}, + {"google.com]:80", "google.com]", "80"}, + {"google.com:80_invalid_port", "google.com:80_invalid_port", ""}, + {"[::1]extra]:80", "::1]extra", "80"}, + {"google.com]extra:extra", "google.com]extra:extra", ""}, + } + for _, tt := range tests { + u := &URL{Host: tt.in} + host, port := u.Hostname(), u.Port() + if host != tt.host { + t.Errorf("Hostname for Host %q = %q; want %q", tt.in, host, tt.host) + } + if port != tt.port { + t.Errorf("Port for Host %q = %q; want %q", tt.in, port, tt.port) + } + } +} + +var _ encodingPkg.BinaryMarshaler = (*URL)(nil) +var _ encodingPkg.BinaryUnmarshaler = (*URL)(nil) + +func TestJSON(t *testing.T) { + u, err := Parse("https://www.google.com/x?y=z") + if err != nil { + t.Fatal(err) + } + js, err := json.Marshal(u) + if err != nil { + t.Fatal(err) + } + + // If only we could implement TextMarshaler/TextUnmarshaler, + // this would work: + // + // if string(js) != strconv.Quote(u.String()) { + // t.Errorf("json encoding: %s\nwant: %s\n", js, strconv.Quote(u.String())) + // } + + u1 := new(URL) + err = json.Unmarshal(js, u1) + if err != nil { + t.Fatal(err) + } + if u1.String() != u.String() { + t.Errorf("json decoded to: %s\nwant: %s\n", u1, u) + } +} + +func TestGob(t *testing.T) { + u, err := Parse("https://www.google.com/x?y=z") + if err != nil { + t.Fatal(err) + } + var w bytes.Buffer + err = gob.NewEncoder(&w).Encode(u) + if err != nil { + t.Fatal(err) + } + + u1 := new(URL) + err = gob.NewDecoder(&w).Decode(u1) + if err != nil { + t.Fatal(err) + } + if u1.String() != u.String() { + t.Errorf("json decoded to: %s\nwant: %s\n", u1, u) + } +} + +func TestNilUser(t *testing.T) { + defer func() { + if v := recover(); v != nil { + t.Fatalf("unexpected panic: %v", v) + } + }() + + u, err := Parse("http://foo.com/") + + if err != nil { + t.Fatalf("parse err: %v", err) + } + + if v := u.User.Username(); v != "" { + t.Fatalf("expected empty username, got %s", v) + } + + if v, ok := u.User.Password(); v != "" || ok { + t.Fatalf("expected empty password, got %s (%v)", v, ok) + } + + if v := u.User.String(); v != "" { + t.Fatalf("expected empty string, got %s", v) + } +} + +func TestInvalidUserPassword(t *testing.T) { + _, err := Parse("http://user^:passwo^rd@foo.com/") + if got, wantsub := fmt.Sprint(err), "net/url: invalid userinfo"; !strings.Contains(got, wantsub) { + t.Errorf("error = %q; want substring %q", got, wantsub) + } +} + +func TestRejectControlCharacters(t *testing.T) { + tests := []string{ + "http://foo.com/?foo\nbar", + "http\r://foo.com/", + "http://foo\x7f.com/", + } + for _, s := range tests { + _, err := Parse(s) + const wantSub = "net/url: invalid control character in URL" + if got := fmt.Sprint(err); !strings.Contains(got, wantSub) { + t.Errorf("Parse(%q) error = %q; want substring %q", s, got, wantSub) + } + } + + // But don't reject non-ASCII CTLs, at least for now: + if _, err := Parse("http://foo.com/ctl\x80"); err != nil { + t.Errorf("error parsing URL with non-ASCII control byte: %v", err) + } + +} + +var escapeBenchmarks = []struct { + unescaped string + query string + path string +}{ + { + unescaped: "one two", + query: "one+two", + path: "one%20two", + }, + { + unescaped: "Фотки собак", + query: "%D0%A4%D0%BE%D1%82%D0%BA%D0%B8+%D1%81%D0%BE%D0%B1%D0%B0%D0%BA", + path: "%D0%A4%D0%BE%D1%82%D0%BA%D0%B8%20%D1%81%D0%BE%D0%B1%D0%B0%D0%BA", + }, + + { + unescaped: "shortrun(break)shortrun", + query: "shortrun%28break%29shortrun", + path: "shortrun%28break%29shortrun", + }, + + { + unescaped: "longerrunofcharacters(break)anotherlongerrunofcharacters", + query: "longerrunofcharacters%28break%29anotherlongerrunofcharacters", + path: "longerrunofcharacters%28break%29anotherlongerrunofcharacters", + }, + + { + unescaped: strings.Repeat("padded/with+various%characters?that=need$some@escaping+paddedsowebreak/256bytes", 4), + query: strings.Repeat("padded%2Fwith%2Bvarious%25characters%3Fthat%3Dneed%24some%40escaping%2Bpaddedsowebreak%2F256bytes", 4), + path: strings.Repeat("padded%2Fwith+various%25characters%3Fthat=need$some@escaping+paddedsowebreak%2F256bytes", 4), + }, +} + +func BenchmarkQueryEscape(b *testing.B) { + for _, tc := range escapeBenchmarks { + b.Run("", func(b *testing.B) { + b.ReportAllocs() + var g string + for i := 0; i < b.N; i++ { + g = QueryEscape(tc.unescaped) + } + b.StopTimer() + if g != tc.query { + b.Errorf("QueryEscape(%q) == %q, want %q", tc.unescaped, g, tc.query) + } + + }) + } +} + +func BenchmarkPathEscape(b *testing.B) { + for _, tc := range escapeBenchmarks { + b.Run("", func(b *testing.B) { + b.ReportAllocs() + var g string + for i := 0; i < b.N; i++ { + g = PathEscape(tc.unescaped) + } + b.StopTimer() + if g != tc.path { + b.Errorf("PathEscape(%q) == %q, want %q", tc.unescaped, g, tc.path) + } + + }) + } +} + +func BenchmarkQueryUnescape(b *testing.B) { + for _, tc := range escapeBenchmarks { + b.Run("", func(b *testing.B) { + b.ReportAllocs() + var g string + for i := 0; i < b.N; i++ { + g, _ = QueryUnescape(tc.query) + } + b.StopTimer() + if g != tc.unescaped { + b.Errorf("QueryUnescape(%q) == %q, want %q", tc.query, g, tc.unescaped) + } + + }) + } +} + +func BenchmarkPathUnescape(b *testing.B) { + for _, tc := range escapeBenchmarks { + b.Run("", func(b *testing.B) { + b.ReportAllocs() + var g string + for i := 0; i < b.N; i++ { + g, _ = PathUnescape(tc.path) + } + b.StopTimer() + if g != tc.unescaped { + b.Errorf("PathUnescape(%q) == %q, want %q", tc.path, g, tc.unescaped) + } + + }) + } +} + +func TestJoinPath(t *testing.T) { + tests := []struct { + base string + elem []string + out string + }{ + { + base: "https://go.googlesource.com", + elem: []string{"go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com/a/b/c", + elem: []string{"../../../go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com/", + elem: []string{"../go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com", + elem: []string{"../go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com", + elem: []string{"../go", "../../go", "../../../go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com/../go", + elem: nil, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com/", + elem: []string{"./go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com//", + elem: []string{"/go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com//", + elem: []string{"/go", "a", "b", "c"}, + out: "https://go.googlesource.com/go/a/b/c", + }, + { + base: "http://[fe80::1%en0]:8080/", + elem: []string{"/go"}, + out: "http://[fe80::1%25en0]:8080/go", + }, + { + base: "https://go.googlesource.com", + elem: []string{"go/"}, + out: "https://go.googlesource.com/go/", + }, + { + base: "https://go.googlesource.com", + elem: []string{"go//"}, + out: "https://go.googlesource.com/go/", + }, + { + base: "https://go.googlesource.com", + elem: nil, + out: "https://go.googlesource.com/", + }, + { + base: "https://go.googlesource.com/", + elem: nil, + out: "https://go.googlesource.com/", + }, + { + base: "https://go.googlesource.com/a%2fb", + elem: []string{"c"}, + out: "https://go.googlesource.com/a%2fb/c", + }, + { + base: "https://go.googlesource.com/a%2fb", + elem: []string{"c%2fd"}, + out: "https://go.googlesource.com/a%2fb/c%2fd", + }, + { + base: "https://go.googlesource.com/a/b", + elem: []string{"/go"}, + out: "https://go.googlesource.com/a/b/go", + }, + { + base: "/", + elem: nil, + out: "/", + }, + { + base: "a", + elem: nil, + out: "a", + }, + { + base: "a", + elem: []string{"b"}, + out: "a/b", + }, + { + base: "a", + elem: []string{"../b"}, + out: "b", + }, + { + base: "a", + elem: []string{"../../b"}, + out: "b", + }, + { + base: "", + elem: []string{"a"}, + out: "a", + }, + { + base: "", + elem: []string{"../a"}, + out: "a", + }, + } + for _, tt := range tests { + wantErr := "nil" + if tt.out == "" { + wantErr = "non-nil error" + } + if out, err := JoinPath(tt.base, tt.elem...); out != tt.out || (err == nil) != (tt.out != "") { + t.Errorf("JoinPath(%q, %q) = %q, %v, want %q, %v", tt.base, tt.elem, out, err, tt.out, wantErr) + } + var out string + u, err := Parse(tt.base) + if err == nil { + u = u.JoinPath(tt.elem...) + out = u.String() + } + if out != tt.out || (err == nil) != (tt.out != "") { + t.Errorf("Parse(%q).JoinPath(%q) = %q, %v, want %q, %v", tt.base, tt.elem, out, err, tt.out, wantErr) + } + } +} From 4ab7035b3490f02d090824ab8eb7ff8146c6377e Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Wed, 28 Aug 2024 15:13:01 -0700 Subject: [PATCH 2/4] internal/net/url: convert Userinfo field to neturl.Userinfo --- internal/net/url/url.go | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/internal/net/url/url.go b/internal/net/url/url.go index e959ed4..0db3a19 100644 --- a/internal/net/url/url.go +++ b/internal/net/url/url.go @@ -17,6 +17,9 @@ import ( "sort" "strconv" "strings" + "unsafe" + + neturl "net/url" ) // Error reports an error and the operation and URL that caused it. @@ -369,16 +372,16 @@ func escape(s string, mode encoding) string { // URL's String method uses the EscapedPath method to obtain the path. type URL struct { Scheme string - Opaque string // encoded opaque data - User *Userinfo // username and password information - Host string // host or host:port - Path string // path (relative paths may omit leading slash) - RawPath string // encoded path hint (see EscapedPath method) - OmitHost bool // do not emit empty host (authority) - ForceQuery bool // append a query ('?') even if RawQuery is empty - RawQuery string // encoded query values, without '?' - Fragment string // fragment for references, without '#' - RawFragment string // encoded fragment hint (see EscapedFragment method) + Opaque string // encoded opaque data + User *neturl.Userinfo // username and password information + Host string // host or host:port + Path string // path (relative paths may omit leading slash) + RawPath string // encoded path hint (see EscapedPath method) + OmitHost bool // do not emit empty host (authority) + ForceQuery bool // append a query ('?') even if RawQuery is empty + RawQuery string // encoded query values, without '?' + Fragment string // fragment for references, without '#' + RawFragment string // encoded fragment hint (see EscapedFragment method) } // User returns a Userinfo containing the provided username @@ -564,10 +567,12 @@ func parse(rawURL string, viaRequest bool) (*URL, error) { if i := strings.Index(authority, "/"); i >= 0 { authority, rest = authority[:i], authority[i:] } - url.User, url.Host, err = parseAuthority(authority) + var urlUser *Userinfo + urlUser, url.Host, err = parseAuthority(authority) if err != nil { return nil, err } + url.User = (*neturl.Userinfo)(unsafe.Pointer(urlUser)) } else if url.Scheme != "" && strings.HasPrefix(rest, "/") { // OmitHost is set to true when rawURL has an empty host (authority). // See golang.org/issue/46059. @@ -875,7 +880,7 @@ func (u *URL) Redacted() string { ru := *u if _, has := ru.User.Password(); has { - ru.User = UserPassword(ru.User.Username(), "xxxxx") + ru.User = neturl.UserPassword(ru.User.Username(), "xxxxx") } return ru.String() } From 2b5208a3b5aa176170be01145bee94e27a9202cd Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Wed, 28 Aug 2024 15:13:39 -0700 Subject: [PATCH 3/4] fsthttp: use less strict net/url parser --- fsthttp/request.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fsthttp/request.go b/fsthttp/request.go index 799e01b..0061639 100644 --- a/fsthttp/request.go +++ b/fsthttp/request.go @@ -13,6 +13,7 @@ import ( "time" "github.com/fastly/compute-sdk-go/internal/abi/fastly" + intneturl "github.com/fastly/compute-sdk-go/internal/net/url" ) // RequestLimits are the limits for the components of an HTTP request. @@ -149,10 +150,12 @@ func newClientRequest() (*Request, error) { return nil, fmt.Errorf("get URI: %w", err) } - u, err := url.ParseRequestURI(uri) + intu, err := intneturl.ParseRequestURI(uri) if err != nil { return nil, fmt.Errorf("parse URI: %w", err) } + uval := url.URL(*intu) + u := &uval proto, major, minor, err := abiReq.GetVersion() if err != nil { From 24831b2db3dedf839b99aa74f37e1451fb94a535 Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Wed, 28 Aug 2024 15:24:33 -0700 Subject: [PATCH 4/4] internal/net/url: remove url tests --- internal/net/url/url_test.go | 2204 ---------------------------------- 1 file changed, 2204 deletions(-) delete mode 100644 internal/net/url/url_test.go diff --git a/internal/net/url/url_test.go b/internal/net/url/url_test.go deleted file mode 100644 index 899ec99..0000000 --- a/internal/net/url/url_test.go +++ /dev/null @@ -1,2204 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package url - -import ( - "bytes" - encodingPkg "encoding" - "encoding/gob" - "encoding/json" - "fmt" - "io" - "net" - "reflect" - "strings" - "testing" -) - -type URLTest struct { - in string - out *URL // expected parse - roundtrip string // expected result of reserializing the URL; empty means same as "in". -} - -var urltests = []URLTest{ - // no path - { - "http://www.google.com", - &URL{ - Scheme: "http", - Host: "www.google.com", - }, - "", - }, - // path - { - "http://www.google.com/", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/", - }, - "", - }, - // path with hex escaping - { - "http://www.google.com/file%20one%26two", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/file one&two", - RawPath: "/file%20one%26two", - }, - "", - }, - // fragment with hex escaping - { - "http://www.google.com/#file%20one%26two", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/", - Fragment: "file one&two", - RawFragment: "file%20one%26two", - }, - "", - }, - // user - { - "ftp://webmaster@www.google.com/", - &URL{ - Scheme: "ftp", - User: User("webmaster"), - Host: "www.google.com", - Path: "/", - }, - "", - }, - // escape sequence in username - { - "ftp://john%20doe@www.google.com/", - &URL{ - Scheme: "ftp", - User: User("john doe"), - Host: "www.google.com", - Path: "/", - }, - "ftp://john%20doe@www.google.com/", - }, - // empty query - { - "http://www.google.com/?", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/", - ForceQuery: true, - }, - "", - }, - // query ending in question mark (Issue 14573) - { - "http://www.google.com/?foo=bar?", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/", - RawQuery: "foo=bar?", - }, - "", - }, - // query - { - "http://www.google.com/?q=go+language", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/", - RawQuery: "q=go+language", - }, - "", - }, - // query with hex escaping: NOT parsed - { - "http://www.google.com/?q=go%20language", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/", - RawQuery: "q=go%20language", - }, - "", - }, - // %20 outside query - { - "http://www.google.com/a%20b?q=c+d", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/a b", - RawQuery: "q=c+d", - }, - "", - }, - // path without leading /, so no parsing - { - "http:www.google.com/?q=go+language", - &URL{ - Scheme: "http", - Opaque: "www.google.com/", - RawQuery: "q=go+language", - }, - "http:www.google.com/?q=go+language", - }, - // path without leading /, so no parsing - { - "http:%2f%2fwww.google.com/?q=go+language", - &URL{ - Scheme: "http", - Opaque: "%2f%2fwww.google.com/", - RawQuery: "q=go+language", - }, - "http:%2f%2fwww.google.com/?q=go+language", - }, - // non-authority with path; see golang.org/issue/46059 - { - "mailto:/webmaster@golang.org", - &URL{ - Scheme: "mailto", - Path: "/webmaster@golang.org", - OmitHost: true, - }, - "", - }, - // non-authority - { - "mailto:webmaster@golang.org", - &URL{ - Scheme: "mailto", - Opaque: "webmaster@golang.org", - }, - "", - }, - // unescaped :// in query should not create a scheme - { - "/foo?query=http://bad", - &URL{ - Path: "/foo", - RawQuery: "query=http://bad", - }, - "", - }, - // leading // without scheme should create an authority - { - "//foo", - &URL{ - Host: "foo", - }, - "", - }, - // leading // without scheme, with userinfo, path, and query - { - "//user@foo/path?a=b", - &URL{ - User: User("user"), - Host: "foo", - Path: "/path", - RawQuery: "a=b", - }, - "", - }, - // Three leading slashes isn't an authority, but doesn't return an error. - // (We can't return an error, as this code is also used via - // ServeHTTP -> ReadRequest -> Parse, which is arguably a - // different URL parsing context, but currently shares the - // same codepath) - { - "///threeslashes", - &URL{ - Path: "///threeslashes", - }, - "", - }, - { - "http://user:password@google.com", - &URL{ - Scheme: "http", - User: UserPassword("user", "password"), - Host: "google.com", - }, - "http://user:password@google.com", - }, - // unescaped @ in username should not confuse host - { - "http://j@ne:password@google.com", - &URL{ - Scheme: "http", - User: UserPassword("j@ne", "password"), - Host: "google.com", - }, - "http://j%40ne:password@google.com", - }, - // unescaped @ in password should not confuse host - { - "http://jane:p@ssword@google.com", - &URL{ - Scheme: "http", - User: UserPassword("jane", "p@ssword"), - Host: "google.com", - }, - "http://jane:p%40ssword@google.com", - }, - { - "http://j@ne:password@google.com/p@th?q=@go", - &URL{ - Scheme: "http", - User: UserPassword("j@ne", "password"), - Host: "google.com", - Path: "/p@th", - RawQuery: "q=@go", - }, - "http://j%40ne:password@google.com/p@th?q=@go", - }, - { - "http://www.google.com/?q=go+language#foo", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/", - RawQuery: "q=go+language", - Fragment: "foo", - }, - "", - }, - { - "http://www.google.com/?q=go+language#foo&bar", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/", - RawQuery: "q=go+language", - Fragment: "foo&bar", - }, - "http://www.google.com/?q=go+language#foo&bar", - }, - { - "http://www.google.com/?q=go+language#foo%26bar", - &URL{ - Scheme: "http", - Host: "www.google.com", - Path: "/", - RawQuery: "q=go+language", - Fragment: "foo&bar", - RawFragment: "foo%26bar", - }, - "http://www.google.com/?q=go+language#foo%26bar", - }, - { - "file:///home/adg/rabbits", - &URL{ - Scheme: "file", - Host: "", - Path: "/home/adg/rabbits", - }, - "file:///home/adg/rabbits", - }, - // "Windows" paths are no exception to the rule. - // See golang.org/issue/6027, especially comment #9. - { - "file:///C:/FooBar/Baz.txt", - &URL{ - Scheme: "file", - Host: "", - Path: "/C:/FooBar/Baz.txt", - }, - "file:///C:/FooBar/Baz.txt", - }, - // case-insensitive scheme - { - "MaIlTo:webmaster@golang.org", - &URL{ - Scheme: "mailto", - Opaque: "webmaster@golang.org", - }, - "mailto:webmaster@golang.org", - }, - // Relative path - { - "a/b/c", - &URL{ - Path: "a/b/c", - }, - "a/b/c", - }, - // escaped '?' in username and password - { - "http://%3Fam:pa%3Fsword@google.com", - &URL{ - Scheme: "http", - User: UserPassword("?am", "pa?sword"), - Host: "google.com", - }, - "", - }, - // host subcomponent; IPv4 address in RFC 3986 - { - "http://192.168.0.1/", - &URL{ - Scheme: "http", - Host: "192.168.0.1", - Path: "/", - }, - "", - }, - // host and port subcomponents; IPv4 address in RFC 3986 - { - "http://192.168.0.1:8080/", - &URL{ - Scheme: "http", - Host: "192.168.0.1:8080", - Path: "/", - }, - "", - }, - // host subcomponent; IPv6 address in RFC 3986 - { - "http://[fe80::1]/", - &URL{ - Scheme: "http", - Host: "[fe80::1]", - Path: "/", - }, - "", - }, - // host and port subcomponents; IPv6 address in RFC 3986 - { - "http://[fe80::1]:8080/", - &URL{ - Scheme: "http", - Host: "[fe80::1]:8080", - Path: "/", - }, - "", - }, - // host subcomponent; IPv6 address with zone identifier in RFC 6874 - { - "http://[fe80::1%25en0]/", // alphanum zone identifier - &URL{ - Scheme: "http", - Host: "[fe80::1%en0]", - Path: "/", - }, - "", - }, - // host and port subcomponents; IPv6 address with zone identifier in RFC 6874 - { - "http://[fe80::1%25en0]:8080/", // alphanum zone identifier - &URL{ - Scheme: "http", - Host: "[fe80::1%en0]:8080", - Path: "/", - }, - "", - }, - // host subcomponent; IPv6 address with zone identifier in RFC 6874 - { - "http://[fe80::1%25%65%6e%301-._~]/", // percent-encoded+unreserved zone identifier - &URL{ - Scheme: "http", - Host: "[fe80::1%en01-._~]", - Path: "/", - }, - "http://[fe80::1%25en01-._~]/", - }, - // host and port subcomponents; IPv6 address with zone identifier in RFC 6874 - { - "http://[fe80::1%25%65%6e%301-._~]:8080/", // percent-encoded+unreserved zone identifier - &URL{ - Scheme: "http", - Host: "[fe80::1%en01-._~]:8080", - Path: "/", - }, - "http://[fe80::1%25en01-._~]:8080/", - }, - // alternate escapings of path survive round trip - { - "http://rest.rsc.io/foo%2fbar/baz%2Fquux?alt=media", - &URL{ - Scheme: "http", - Host: "rest.rsc.io", - Path: "/foo/bar/baz/quux", - RawPath: "/foo%2fbar/baz%2Fquux", - RawQuery: "alt=media", - }, - "", - }, - // issue 12036 - { - "mysql://a,b,c/bar", - &URL{ - Scheme: "mysql", - Host: "a,b,c", - Path: "/bar", - }, - "", - }, - // worst case host, still round trips - { - "scheme://!$&'()*+,;=hello!:1/path", - &URL{ - Scheme: "scheme", - Host: "!$&'()*+,;=hello!:1", - Path: "/path", - }, - "", - }, - // worst case path, still round trips - { - "http://host/!$&'()*+,;=:@[hello]", - &URL{ - Scheme: "http", - Host: "host", - Path: "/!$&'()*+,;=:@[hello]", - RawPath: "/!$&'()*+,;=:@[hello]", - }, - "", - }, - // golang.org/issue/5684 - { - "http://example.com/oid/[order_id]", - &URL{ - Scheme: "http", - Host: "example.com", - Path: "/oid/[order_id]", - RawPath: "/oid/[order_id]", - }, - "", - }, - // golang.org/issue/12200 (colon with empty port) - { - "http://192.168.0.2:8080/foo", - &URL{ - Scheme: "http", - Host: "192.168.0.2:8080", - Path: "/foo", - }, - "", - }, - { - "http://192.168.0.2:/foo", - &URL{ - Scheme: "http", - Host: "192.168.0.2:", - Path: "/foo", - }, - "", - }, - { - // Malformed IPv6 but still accepted. - "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080/foo", - &URL{ - Scheme: "http", - Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080", - Path: "/foo", - }, - "", - }, - { - // Malformed IPv6 but still accepted. - "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:/foo", - &URL{ - Scheme: "http", - Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:", - Path: "/foo", - }, - "", - }, - { - "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080/foo", - &URL{ - Scheme: "http", - Host: "[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080", - Path: "/foo", - }, - "", - }, - { - "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:/foo", - &URL{ - Scheme: "http", - Host: "[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:", - Path: "/foo", - }, - "", - }, - // golang.org/issue/7991 and golang.org/issue/12719 (non-ascii %-encoded in host) - { - "http://hello.世界.com/foo", - &URL{ - Scheme: "http", - Host: "hello.世界.com", - Path: "/foo", - }, - "http://hello.%E4%B8%96%E7%95%8C.com/foo", - }, - { - "http://hello.%e4%b8%96%e7%95%8c.com/foo", - &URL{ - Scheme: "http", - Host: "hello.世界.com", - Path: "/foo", - }, - "http://hello.%E4%B8%96%E7%95%8C.com/foo", - }, - { - "http://hello.%E4%B8%96%E7%95%8C.com/foo", - &URL{ - Scheme: "http", - Host: "hello.世界.com", - Path: "/foo", - }, - "", - }, - // golang.org/issue/10433 (path beginning with //) - { - "http://example.com//foo", - &URL{ - Scheme: "http", - Host: "example.com", - Path: "//foo", - }, - "", - }, - // test that we can reparse the host names we accept. - { - "myscheme://authority<\"hi\">/foo", - &URL{ - Scheme: "myscheme", - Host: "authority<\"hi\">", - Path: "/foo", - }, - "", - }, - // spaces in hosts are disallowed but escaped spaces in IPv6 scope IDs are grudgingly OK. - // This happens on Windows. - // golang.org/issue/14002 - { - "tcp://[2020::2020:20:2020:2020%25Windows%20Loves%20Spaces]:2020", - &URL{ - Scheme: "tcp", - Host: "[2020::2020:20:2020:2020%Windows Loves Spaces]:2020", - }, - "", - }, - // test we can roundtrip magnet url - // fix issue https://golang.org/issue/20054 - { - "magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", - &URL{ - Scheme: "magnet", - Host: "", - Path: "", - RawQuery: "xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", - }, - "magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", - }, - { - "mailto:?subject=hi", - &URL{ - Scheme: "mailto", - Host: "", - Path: "", - RawQuery: "subject=hi", - }, - "mailto:?subject=hi", - }, -} - -// more useful string for debugging than fmt's struct printer -func ufmt(u *URL) string { - var user, pass any - if u.User != nil { - user = u.User.Username() - if p, ok := u.User.Password(); ok { - pass = p - } - } - return fmt.Sprintf("opaque=%q, scheme=%q, user=%#v, pass=%#v, host=%q, path=%q, rawpath=%q, rawq=%q, frag=%q, rawfrag=%q, forcequery=%v, omithost=%t", - u.Opaque, u.Scheme, user, pass, u.Host, u.Path, u.RawPath, u.RawQuery, u.Fragment, u.RawFragment, u.ForceQuery, u.OmitHost) -} - -func BenchmarkString(b *testing.B) { - b.StopTimer() - b.ReportAllocs() - for _, tt := range urltests { - u, err := Parse(tt.in) - if err != nil { - b.Errorf("Parse(%q) returned error %s", tt.in, err) - continue - } - if tt.roundtrip == "" { - continue - } - b.StartTimer() - var g string - for i := 0; i < b.N; i++ { - g = u.String() - } - b.StopTimer() - if w := tt.roundtrip; b.N > 0 && g != w { - b.Errorf("Parse(%q).String() == %q, want %q", tt.in, g, w) - } - } -} - -func TestParse(t *testing.T) { - for _, tt := range urltests { - u, err := Parse(tt.in) - if err != nil { - t.Errorf("Parse(%q) returned error %v", tt.in, err) - continue - } - if !reflect.DeepEqual(u, tt.out) { - t.Errorf("Parse(%q):\n\tgot %v\n\twant %v\n", tt.in, ufmt(u), ufmt(tt.out)) - } - } -} - -const pathThatLooksSchemeRelative = "//not.a.user@not.a.host/just/a/path" - -var parseRequestURLTests = []struct { - url string - expectedValid bool -}{ - {"http://foo.com", true}, - {"http://foo.com/", true}, - {"http://foo.com/path", true}, - {"/", true}, - {pathThatLooksSchemeRelative, true}, - {"//not.a.user@%66%6f%6f.com/just/a/path/also", true}, - {"*", true}, - {"http://192.168.0.1/", true}, - {"http://192.168.0.1:8080/", true}, - {"http://[fe80::1]/", true}, - {"http://[fe80::1]:8080/", true}, - - // Tests exercising RFC 6874 compliance: - {"http://[fe80::1%25en0]/", true}, // with alphanum zone identifier - {"http://[fe80::1%25en0]:8080/", true}, // with alphanum zone identifier - {"http://[fe80::1%25%65%6e%301-._~]/", true}, // with percent-encoded+unreserved zone identifier - {"http://[fe80::1%25%65%6e%301-._~]:8080/", true}, // with percent-encoded+unreserved zone identifier - - {"foo.html", false}, - {"../dir/", false}, - {" http://foo.com", false}, - {"http://192.168.0.%31/", false}, - {"http://192.168.0.%31:8080/", false}, - {"http://[fe80::%31]/", false}, - {"http://[fe80::%31]:8080/", false}, - {"http://[fe80::%31%25en0]/", false}, - {"http://[fe80::%31%25en0]:8080/", false}, - - // These two cases are valid as textual representations as - // described in RFC 4007, but are not valid as address - // literals with IPv6 zone identifiers in URIs as described in - // RFC 6874. However, this seems to be overridden by - // https://url.spec.whatwg.org/#percent-encoded-bytes - // which permits unencoded % characters. - {"http://[fe80::1%en0]/", true}, - {"http://[fe80::1%en0]:8080/", true}, -} - -func TestParseRequestURI(t *testing.T) { - for _, test := range parseRequestURLTests { - _, err := ParseRequestURI(test.url) - if test.expectedValid && err != nil { - t.Errorf("ParseRequestURI(%q) gave err %v; want no error", test.url, err) - } else if !test.expectedValid && err == nil { - t.Errorf("ParseRequestURI(%q) gave nil error; want some error", test.url) - } - } - - url, err := ParseRequestURI(pathThatLooksSchemeRelative) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - if url.Path != pathThatLooksSchemeRelative { - t.Errorf("ParseRequestURI path:\ngot %q\nwant %q", url.Path, pathThatLooksSchemeRelative) - } -} - -var stringURLTests = []struct { - url URL - want string -}{ - // No leading slash on path should prepend slash on String() call - { - url: URL{ - Scheme: "http", - Host: "www.google.com", - Path: "search", - }, - want: "http://www.google.com/search", - }, - // Relative path with first element containing ":" should be prepended with "./", golang.org/issue/17184 - { - url: URL{ - Path: "this:that", - }, - want: "./this:that", - }, - // Relative path with second element containing ":" should not be prepended with "./" - { - url: URL{ - Path: "here/this:that", - }, - want: "here/this:that", - }, - // Non-relative path with first element containing ":" should not be prepended with "./" - { - url: URL{ - Scheme: "http", - Host: "www.google.com", - Path: "this:that", - }, - want: "http://www.google.com/this:that", - }, -} - -func TestURLString(t *testing.T) { - for _, tt := range urltests { - u, err := Parse(tt.in) - if err != nil { - t.Errorf("Parse(%q) returned error %s", tt.in, err) - continue - } - expected := tt.in - if tt.roundtrip != "" { - expected = tt.roundtrip - } - s := u.String() - if s != expected { - t.Errorf("Parse(%q).String() == %q (expected %q)", tt.in, s, expected) - } - } - - for _, tt := range stringURLTests { - if got := tt.url.String(); got != tt.want { - t.Errorf("%+v.String() = %q; want %q", tt.url, got, tt.want) - } - } -} - -func TestURLRedacted(t *testing.T) { - cases := []struct { - name string - url *URL - want string - }{ - { - name: "non-blank Password", - url: &URL{ - Scheme: "http", - Host: "host.tld", - Path: "this:that", - User: UserPassword("user", "password"), - }, - want: "http://user:xxxxx@host.tld/this:that", - }, - { - name: "blank Password", - url: &URL{ - Scheme: "http", - Host: "host.tld", - Path: "this:that", - User: User("user"), - }, - want: "http://user@host.tld/this:that", - }, - { - name: "nil User", - url: &URL{ - Scheme: "http", - Host: "host.tld", - Path: "this:that", - User: UserPassword("", "password"), - }, - want: "http://:xxxxx@host.tld/this:that", - }, - { - name: "blank Username, blank Password", - url: &URL{ - Scheme: "http", - Host: "host.tld", - Path: "this:that", - }, - want: "http://host.tld/this:that", - }, - { - name: "empty URL", - url: &URL{}, - want: "", - }, - { - name: "nil URL", - url: nil, - want: "", - }, - } - - for _, tt := range cases { - t := t - t.Run(tt.name, func(t *testing.T) { - if g, w := tt.url.Redacted(), tt.want; g != w { - t.Fatalf("got: %q\nwant: %q", g, w) - } - }) - } -} - -type EscapeTest struct { - in string - out string - err error -} - -var unescapeTests = []EscapeTest{ - { - "", - "", - nil, - }, - { - "abc", - "abc", - nil, - }, - { - "1%41", - "1A", - nil, - }, - { - "1%41%42%43", - "1ABC", - nil, - }, - { - "%4a", - "J", - nil, - }, - { - "%6F", - "o", - nil, - }, - { - "%", // not enough characters after % - "%", - nil, - }, - { - "%a", // not enough characters after % - "%a", - nil, - }, - { - "%1", // not enough characters after % - "%1", - nil, - }, - { - "123%45%6", // not enough characters after % - "123E%6", - nil, - }, - { - "%zzzzz", // invalid hex digits - "%zzzzz", - nil, - }, - { - "a+b", - "a b", - nil, - }, - { - "a%20b", - "a b", - nil, - }, -} - -func TestUnescape(t *testing.T) { - for _, tt := range unescapeTests { - actual, err := QueryUnescape(tt.in) - if actual != tt.out || (err != nil) != (tt.err != nil) { - t.Errorf("QueryUnescape(%q) = %q, %s; want %q, %s", tt.in, actual, err, tt.out, tt.err) - } - - in := tt.in - out := tt.out - if strings.Contains(tt.in, "+") { - in = strings.ReplaceAll(tt.in, "+", "%20") - actual, err := PathUnescape(in) - if actual != tt.out || (err != nil) != (tt.err != nil) { - t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", in, actual, err, tt.out, tt.err) - } - if tt.err == nil { - s, err := QueryUnescape(strings.ReplaceAll(tt.in, "+", "XXX")) - if err != nil { - continue - } - in = tt.in - out = strings.ReplaceAll(s, "XXX", "+") - } - } - - actual, err = PathUnescape(in) - if actual != out || (err != nil) != (tt.err != nil) { - t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", in, actual, err, out, tt.err) - } - } -} - -var queryEscapeTests = []EscapeTest{ - { - "", - "", - nil, - }, - { - "abc", - "abc", - nil, - }, - { - "one two", - "one+two", - nil, - }, - { - "10%", - "10%25", - nil, - }, - { - " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;", - "+%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A%2F%40%24%27%28%29%2A%2C%3B", - nil, - }, -} - -func TestQueryEscape(t *testing.T) { - for _, tt := range queryEscapeTests { - actual := QueryEscape(tt.in) - if tt.out != actual { - t.Errorf("QueryEscape(%q) = %q, want %q", tt.in, actual, tt.out) - } - - // for bonus points, verify that escape:unescape is an identity. - roundtrip, err := QueryUnescape(actual) - if roundtrip != tt.in || err != nil { - t.Errorf("QueryUnescape(%q) = %q, %s; want %q, %s", actual, roundtrip, err, tt.in, "[no error]") - } - } -} - -var pathEscapeTests = []EscapeTest{ - { - "", - "", - nil, - }, - { - "abc", - "abc", - nil, - }, - { - "abc+def", - "abc+def", - nil, - }, - { - "a/b", - "a%2Fb", - nil, - }, - { - "one two", - "one%20two", - nil, - }, - { - "10%", - "10%25", - nil, - }, - { - " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;", - "%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B", - nil, - }, -} - -func TestPathEscape(t *testing.T) { - for _, tt := range pathEscapeTests { - actual := PathEscape(tt.in) - if tt.out != actual { - t.Errorf("PathEscape(%q) = %q, want %q", tt.in, actual, tt.out) - } - - // for bonus points, verify that escape:unescape is an identity. - roundtrip, err := PathUnescape(actual) - if roundtrip != tt.in || err != nil { - t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", actual, roundtrip, err, tt.in, "[no error]") - } - } -} - -//var userinfoTests = []UserinfoTest{ -// {"user", "password", "user:password"}, -// {"foo:bar", "~!@#$%^&*()_+{}|[]\\-=`:;'\"<>?,./", -// "foo%3Abar:~!%40%23$%25%5E&*()_+%7B%7D%7C%5B%5D%5C-=%60%3A;'%22%3C%3E?,.%2F"}, -//} - -type EncodeQueryTest struct { - m Values - expected string -} - -var encodeQueryTests = []EncodeQueryTest{ - {nil, ""}, - {Values{"q": {"puppies"}, "oe": {"utf8"}}, "oe=utf8&q=puppies"}, - {Values{"q": {"dogs", "&", "7"}}, "q=dogs&q=%26&q=7"}, - {Values{ - "a": {"a1", "a2", "a3"}, - "b": {"b1", "b2", "b3"}, - "c": {"c1", "c2", "c3"}, - }, "a=a1&a=a2&a=a3&b=b1&b=b2&b=b3&c=c1&c=c2&c=c3"}, -} - -func TestEncodeQuery(t *testing.T) { - for _, tt := range encodeQueryTests { - if q := tt.m.Encode(); q != tt.expected { - t.Errorf(`EncodeQuery(%+v) = %q, want %q`, tt.m, q, tt.expected) - } - } -} - -var resolvePathTests = []struct { - base, ref, expected string -}{ - {"a/b", ".", "/a/"}, - {"a/b", "c", "/a/c"}, - {"a/b", "..", "/"}, - {"a/", "..", "/"}, - {"a/", "../..", "/"}, - {"a/b/c", "..", "/a/"}, - {"a/b/c", "../d", "/a/d"}, - {"a/b/c", ".././d", "/a/d"}, - {"a/b", "./..", "/"}, - {"a/./b", ".", "/a/"}, - {"a/../", ".", "/"}, - {"a/.././b", "c", "/c"}, -} - -func TestResolvePath(t *testing.T) { - for _, test := range resolvePathTests { - got := resolvePath(test.base, test.ref) - if got != test.expected { - t.Errorf("For %q + %q got %q; expected %q", test.base, test.ref, got, test.expected) - } - } -} - -func BenchmarkResolvePath(b *testing.B) { - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - resolvePath("a/b/c", ".././d") - } -} - -var resolveReferenceTests = []struct { - base, rel, expected string -}{ - // Absolute URL references - {"http://foo.com?a=b", "https://bar.com/", "https://bar.com/"}, - {"http://foo.com/", "https://bar.com/?a=b", "https://bar.com/?a=b"}, - {"http://foo.com/", "https://bar.com/?", "https://bar.com/?"}, - {"http://foo.com/bar", "mailto:foo@example.com", "mailto:foo@example.com"}, - - // Path-absolute references - {"http://foo.com/bar", "/baz", "http://foo.com/baz"}, - {"http://foo.com/bar?a=b#f", "/baz", "http://foo.com/baz"}, - {"http://foo.com/bar?a=b", "/baz?", "http://foo.com/baz?"}, - {"http://foo.com/bar?a=b", "/baz?c=d", "http://foo.com/baz?c=d"}, - - // Multiple slashes - {"http://foo.com/bar", "http://foo.com//baz", "http://foo.com//baz"}, - {"http://foo.com/bar", "http://foo.com///baz/quux", "http://foo.com///baz/quux"}, - - // Scheme-relative - {"https://foo.com/bar?a=b", "//bar.com/quux", "https://bar.com/quux"}, - - // Path-relative references: - - // ... current directory - {"http://foo.com", ".", "http://foo.com/"}, - {"http://foo.com/bar", ".", "http://foo.com/"}, - {"http://foo.com/bar/", ".", "http://foo.com/bar/"}, - - // ... going down - {"http://foo.com", "bar", "http://foo.com/bar"}, - {"http://foo.com/", "bar", "http://foo.com/bar"}, - {"http://foo.com/bar/baz", "quux", "http://foo.com/bar/quux"}, - - // ... going up - {"http://foo.com/bar/baz", "../quux", "http://foo.com/quux"}, - {"http://foo.com/bar/baz", "../../../../../quux", "http://foo.com/quux"}, - {"http://foo.com/bar", "..", "http://foo.com/"}, - {"http://foo.com/bar/baz", "./..", "http://foo.com/"}, - // ".." in the middle (issue 3560) - {"http://foo.com/bar/baz", "quux/dotdot/../tail", "http://foo.com/bar/quux/tail"}, - {"http://foo.com/bar/baz", "quux/./dotdot/../tail", "http://foo.com/bar/quux/tail"}, - {"http://foo.com/bar/baz", "quux/./dotdot/.././tail", "http://foo.com/bar/quux/tail"}, - {"http://foo.com/bar/baz", "quux/./dotdot/./../tail", "http://foo.com/bar/quux/tail"}, - {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/././../../tail", "http://foo.com/bar/quux/tail"}, - {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/./.././../tail", "http://foo.com/bar/quux/tail"}, - {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/dotdot/./../../.././././tail", "http://foo.com/bar/quux/tail"}, - {"http://foo.com/bar/baz", "quux/./dotdot/../dotdot/../dot/./tail/..", "http://foo.com/bar/quux/dot/"}, - - // Remove any dot-segments prior to forming the target URI. - // https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 - {"http://foo.com/dot/./dotdot/../foo/bar", "../baz", "http://foo.com/dot/baz"}, - - // Triple dot isn't special - {"http://foo.com/bar", "...", "http://foo.com/..."}, - - // Fragment - {"http://foo.com/bar", ".#frag", "http://foo.com/#frag"}, - {"http://example.org/", "#!$&%27()*+,;=", "http://example.org/#!$&%27()*+,;="}, - - // Paths with escaping (issue 16947). - {"http://foo.com/foo%2fbar/", "../baz", "http://foo.com/baz"}, - {"http://foo.com/1/2%2f/3%2f4/5", "../../a/b/c", "http://foo.com/1/a/b/c"}, - {"http://foo.com/1/2/3", "./a%2f../../b/..%2fc", "http://foo.com/1/2/b/..%2fc"}, - {"http://foo.com/1/2%2f/3%2f4/5", "./a%2f../b/../c", "http://foo.com/1/2%2f/3%2f4/a%2f../c"}, - {"http://foo.com/foo%20bar/", "../baz", "http://foo.com/baz"}, - {"http://foo.com/foo", "../bar%2fbaz", "http://foo.com/bar%2fbaz"}, - {"http://foo.com/foo%2dbar/", "./baz-quux", "http://foo.com/foo%2dbar/baz-quux"}, - - // RFC 3986: Normal Examples - // https://datatracker.ietf.org/doc/html/rfc3986#section-5.4.1 - {"http://a/b/c/d;p?q", "g:h", "g:h"}, - {"http://a/b/c/d;p?q", "g", "http://a/b/c/g"}, - {"http://a/b/c/d;p?q", "./g", "http://a/b/c/g"}, - {"http://a/b/c/d;p?q", "g/", "http://a/b/c/g/"}, - {"http://a/b/c/d;p?q", "/g", "http://a/g"}, - {"http://a/b/c/d;p?q", "//g", "http://g"}, - {"http://a/b/c/d;p?q", "?y", "http://a/b/c/d;p?y"}, - {"http://a/b/c/d;p?q", "g?y", "http://a/b/c/g?y"}, - {"http://a/b/c/d;p?q", "#s", "http://a/b/c/d;p?q#s"}, - {"http://a/b/c/d;p?q", "g#s", "http://a/b/c/g#s"}, - {"http://a/b/c/d;p?q", "g?y#s", "http://a/b/c/g?y#s"}, - {"http://a/b/c/d;p?q", ";x", "http://a/b/c/;x"}, - {"http://a/b/c/d;p?q", "g;x", "http://a/b/c/g;x"}, - {"http://a/b/c/d;p?q", "g;x?y#s", "http://a/b/c/g;x?y#s"}, - {"http://a/b/c/d;p?q", "", "http://a/b/c/d;p?q"}, - {"http://a/b/c/d;p?q", ".", "http://a/b/c/"}, - {"http://a/b/c/d;p?q", "./", "http://a/b/c/"}, - {"http://a/b/c/d;p?q", "..", "http://a/b/"}, - {"http://a/b/c/d;p?q", "../", "http://a/b/"}, - {"http://a/b/c/d;p?q", "../g", "http://a/b/g"}, - {"http://a/b/c/d;p?q", "../..", "http://a/"}, - {"http://a/b/c/d;p?q", "../../", "http://a/"}, - {"http://a/b/c/d;p?q", "../../g", "http://a/g"}, - - // RFC 3986: Abnormal Examples - // https://datatracker.ietf.org/doc/html/rfc3986#section-5.4.2 - {"http://a/b/c/d;p?q", "../../../g", "http://a/g"}, - {"http://a/b/c/d;p?q", "../../../../g", "http://a/g"}, - {"http://a/b/c/d;p?q", "/./g", "http://a/g"}, - {"http://a/b/c/d;p?q", "/../g", "http://a/g"}, - {"http://a/b/c/d;p?q", "g.", "http://a/b/c/g."}, - {"http://a/b/c/d;p?q", ".g", "http://a/b/c/.g"}, - {"http://a/b/c/d;p?q", "g..", "http://a/b/c/g.."}, - {"http://a/b/c/d;p?q", "..g", "http://a/b/c/..g"}, - {"http://a/b/c/d;p?q", "./../g", "http://a/b/g"}, - {"http://a/b/c/d;p?q", "./g/.", "http://a/b/c/g/"}, - {"http://a/b/c/d;p?q", "g/./h", "http://a/b/c/g/h"}, - {"http://a/b/c/d;p?q", "g/../h", "http://a/b/c/h"}, - {"http://a/b/c/d;p?q", "g;x=1/./y", "http://a/b/c/g;x=1/y"}, - {"http://a/b/c/d;p?q", "g;x=1/../y", "http://a/b/c/y"}, - {"http://a/b/c/d;p?q", "g?y/./x", "http://a/b/c/g?y/./x"}, - {"http://a/b/c/d;p?q", "g?y/../x", "http://a/b/c/g?y/../x"}, - {"http://a/b/c/d;p?q", "g#s/./x", "http://a/b/c/g#s/./x"}, - {"http://a/b/c/d;p?q", "g#s/../x", "http://a/b/c/g#s/../x"}, - - // Extras. - {"https://a/b/c/d;p?q", "//g?q", "https://g?q"}, - {"https://a/b/c/d;p?q", "//g#s", "https://g#s"}, - {"https://a/b/c/d;p?q", "//g/d/e/f?y#s", "https://g/d/e/f?y#s"}, - {"https://a/b/c/d;p#s", "?y", "https://a/b/c/d;p?y"}, - {"https://a/b/c/d;p?q#s", "?y", "https://a/b/c/d;p?y"}, - - // Empty path and query but with ForceQuery (issue 46033). - {"https://a/b/c/d;p?q#s", "?", "https://a/b/c/d;p?"}, -} - -func TestResolveReference(t *testing.T) { - mustParse := func(url string) *URL { - u, err := Parse(url) - if err != nil { - t.Fatalf("Parse(%q) got err %v", url, err) - } - return u - } - opaque := &URL{Scheme: "scheme", Opaque: "opaque"} - for _, test := range resolveReferenceTests { - base := mustParse(test.base) - rel := mustParse(test.rel) - url := base.ResolveReference(rel) - if got := url.String(); got != test.expected { - t.Errorf("URL(%q).ResolveReference(%q)\ngot %q\nwant %q", test.base, test.rel, got, test.expected) - } - // Ensure that new instances are returned. - if base == url { - t.Errorf("Expected URL.ResolveReference to return new URL instance.") - } - // Test the convenience wrapper too. - url, err := base.Parse(test.rel) - if err != nil { - t.Errorf("URL(%q).Parse(%q) failed: %v", test.base, test.rel, err) - } else if got := url.String(); got != test.expected { - t.Errorf("URL(%q).Parse(%q)\ngot %q\nwant %q", test.base, test.rel, got, test.expected) - } else if base == url { - // Ensure that new instances are returned for the wrapper too. - t.Errorf("Expected URL.Parse to return new URL instance.") - } - // Ensure Opaque resets the URL. - url = base.ResolveReference(opaque) - if *url != *opaque { - t.Errorf("ResolveReference failed to resolve opaque URL:\ngot %#v\nwant %#v", url, opaque) - } - // Test the convenience wrapper with an opaque URL too. - url, err = base.Parse("scheme:opaque") - if err != nil { - t.Errorf(`URL(%q).Parse("scheme:opaque") failed: %v`, test.base, err) - } else if *url != *opaque { - t.Errorf("Parse failed to resolve opaque URL:\ngot %#v\nwant %#v", opaque, url) - } else if base == url { - // Ensure that new instances are returned, again. - t.Errorf("Expected URL.Parse to return new URL instance.") - } - } -} - -func TestQueryValues(t *testing.T) { - u, _ := Parse("http://x.com?foo=bar&bar=1&bar=2&baz") - v := u.Query() - if len(v) != 3 { - t.Errorf("got %d keys in Query values, want 3", len(v)) - } - if g, e := v.Get("foo"), "bar"; g != e { - t.Errorf("Get(foo) = %q, want %q", g, e) - } - // Case sensitive: - if g, e := v.Get("Foo"), ""; g != e { - t.Errorf("Get(Foo) = %q, want %q", g, e) - } - if g, e := v.Get("bar"), "1"; g != e { - t.Errorf("Get(bar) = %q, want %q", g, e) - } - if g, e := v.Get("baz"), ""; g != e { - t.Errorf("Get(baz) = %q, want %q", g, e) - } - if h, e := v.Has("foo"), true; h != e { - t.Errorf("Has(foo) = %t, want %t", h, e) - } - if h, e := v.Has("bar"), true; h != e { - t.Errorf("Has(bar) = %t, want %t", h, e) - } - if h, e := v.Has("baz"), true; h != e { - t.Errorf("Has(baz) = %t, want %t", h, e) - } - if h, e := v.Has("noexist"), false; h != e { - t.Errorf("Has(noexist) = %t, want %t", h, e) - } - v.Del("bar") - if g, e := v.Get("bar"), ""; g != e { - t.Errorf("second Get(bar) = %q, want %q", g, e) - } -} - -type parseTest struct { - query string - out Values - ok bool -} - -var parseTests = []parseTest{ - { - query: "a=1", - out: Values{"a": []string{"1"}}, - ok: true, - }, - { - query: "a=1&b=2", - out: Values{"a": []string{"1"}, "b": []string{"2"}}, - ok: true, - }, - { - query: "a=1&a=2&a=banana", - out: Values{"a": []string{"1", "2", "banana"}}, - ok: true, - }, - { - query: "ascii=%3Ckey%3A+0x90%3E", - out: Values{"ascii": []string{""}}, - ok: true, - }, { - query: "a=1;b=2", - out: Values{}, - ok: false, - }, { - query: "a;b=1", - out: Values{}, - ok: false, - }, { - query: "a=%3B", // hex encoding for semicolon - out: Values{"a": []string{";"}}, - ok: true, - }, - { - query: "a%3Bb=1", - out: Values{"a;b": []string{"1"}}, - ok: true, - }, - { - query: "a=1&a=2;a=banana", - out: Values{"a": []string{"1"}}, - ok: false, - }, - { - query: "a;b&c=1", - out: Values{"c": []string{"1"}}, - ok: false, - }, - { - query: "a=1&b=2;a=3&c=4", - out: Values{"a": []string{"1"}, "c": []string{"4"}}, - ok: false, - }, - { - query: "a=1&b=2;c=3", - out: Values{"a": []string{"1"}}, - ok: false, - }, - { - query: ";", - out: Values{}, - ok: false, - }, - { - query: "a=1;", - out: Values{}, - ok: false, - }, - { - query: "a=1&;", - out: Values{"a": []string{"1"}}, - ok: false, - }, - { - query: ";a=1&b=2", - out: Values{"b": []string{"2"}}, - ok: false, - }, - { - query: "a=1&b=2;", - out: Values{"a": []string{"1"}}, - ok: false, - }, -} - -func TestParseQuery(t *testing.T) { - for _, test := range parseTests { - t.Run(test.query, func(t *testing.T) { - form, err := ParseQuery(test.query) - if test.ok != (err == nil) { - want := "" - if test.ok { - want = "" - } - t.Errorf("Unexpected error: %v, want %v", err, want) - } - if len(form) != len(test.out) { - t.Errorf("len(form) = %d, want %d", len(form), len(test.out)) - } - for k, evs := range test.out { - vs, ok := form[k] - if !ok { - t.Errorf("Missing key %q", k) - continue - } - if len(vs) != len(evs) { - t.Errorf("len(form[%q]) = %d, want %d", k, len(vs), len(evs)) - continue - } - for j, ev := range evs { - if v := vs[j]; v != ev { - t.Errorf("form[%q][%d] = %q, want %q", k, j, v, ev) - } - } - } - }) - } -} - -type RequestURITest struct { - url *URL - out string -} - -var requritests = []RequestURITest{ - { - &URL{ - Scheme: "http", - Host: "example.com", - Path: "", - }, - "/", - }, - { - &URL{ - Scheme: "http", - Host: "example.com", - Path: "/a b", - }, - "/a%20b", - }, - // golang.org/issue/4860 variant 1 - { - &URL{ - Scheme: "http", - Host: "example.com", - Opaque: "/%2F/%2F/", - }, - "/%2F/%2F/", - }, - // golang.org/issue/4860 variant 2 - { - &URL{ - Scheme: "http", - Host: "example.com", - Opaque: "//other.example.com/%2F/%2F/", - }, - "http://other.example.com/%2F/%2F/", - }, - // better fix for issue 4860 - { - &URL{ - Scheme: "http", - Host: "example.com", - Path: "/////", - RawPath: "/%2F/%2F/", - }, - "/%2F/%2F/", - }, - { - &URL{ - Scheme: "http", - Host: "example.com", - Path: "/////", - RawPath: "/WRONG/", // ignored because doesn't match Path - }, - "/////", - }, - { - &URL{ - Scheme: "http", - Host: "example.com", - Path: "/a b", - RawQuery: "q=go+language", - }, - "/a%20b?q=go+language", - }, - { - &URL{ - Scheme: "http", - Host: "example.com", - Path: "/a b", - RawPath: "/a b", // ignored because invalid - RawQuery: "q=go+language", - }, - "/a%20b?q=go+language", - }, - { - &URL{ - Scheme: "http", - Host: "example.com", - Path: "/a?b", - RawPath: "/a?b", // ignored because invalid - RawQuery: "q=go+language", - }, - "/a%3Fb?q=go+language", - }, - { - &URL{ - Scheme: "myschema", - Opaque: "opaque", - }, - "opaque", - }, - { - &URL{ - Scheme: "myschema", - Opaque: "opaque", - RawQuery: "q=go+language", - }, - "opaque?q=go+language", - }, - { - &URL{ - Scheme: "http", - Host: "example.com", - Path: "//foo", - }, - "//foo", - }, - { - &URL{ - Scheme: "http", - Host: "example.com", - Path: "/foo", - ForceQuery: true, - }, - "/foo?", - }, -} - -func TestRequestURI(t *testing.T) { - for _, tt := range requritests { - s := tt.url.RequestURI() - if s != tt.out { - t.Errorf("%#v.RequestURI() == %q (expected %q)", tt.url, s, tt.out) - } - } -} - -func TestParseErrors(t *testing.T) { - tests := []struct { - in string - wantErr bool - }{ - {"http://[::1]", false}, - {"http://[::1]:80", false}, - {"http://[::1]:namedport", true}, // rfc3986 3.2.3 - {"http://x:namedport", true}, // rfc3986 3.2.3 - {"http://[::1]/", false}, - {"http://[::1]a", true}, - {"http://[::1]%23", true}, - {"http://[::1%25en0]", false}, // valid zone id - {"http://[::1]:", false}, // colon, but no port OK - {"http://x:", false}, // colon, but no port OK - {"http://[::1]:%38%30", true}, // not allowed: % encoding only for non-ASCII - {"http://[::1%25%41]", false}, // RFC 6874 allows over-escaping in zone - {"http://[%10::1]", true}, // no %xx escapes in IP address - {"http://[::1]/%48", false}, // %xx in path is fine - {"http://%41:8080/", true}, // not allowed: % encoding only for non-ASCII - {"mysql://x@y(z:123)/foo", true}, // not well-formed per RFC 3986, golang.org/issue/33646 - {"mysql://x@y(1.2.3.4:123)/foo", true}, - - {" http://foo.com", true}, // invalid character in schema - {"ht tp://foo.com", true}, // invalid character in schema - {"ahttp://foo.com", false}, // valid schema characters - {"1http://foo.com", true}, // invalid character in schema - - {"http://[]%20%48%54%54%50%2f%31%2e%31%0a%4d%79%48%65%61%64%65%72%3a%20%31%32%33%0a%0a/", true}, // golang.org/issue/11208 - {"http://a b.com/", true}, // no space in host name please - {"cache_object://foo", true}, // scheme cannot have _, relative path cannot have : in first segment - {"cache_object:foo", true}, - {"cache_object:foo/bar", true}, - {"cache_object/:foo/bar", false}, - } - for _, tt := range tests { - u, err := Parse(tt.in) - if tt.wantErr { - if err == nil { - t.Errorf("Parse(%q) = %#v; want an error", tt.in, u) - } - continue - } - if err != nil { - t.Errorf("Parse(%q) = %v; want no error", tt.in, err) - } - } -} - -// Issue 11202 -func TestStarRequest(t *testing.T) { - u, err := Parse("*") - if err != nil { - t.Fatal(err) - } - if got, want := u.RequestURI(), "*"; got != want { - t.Errorf("RequestURI = %q; want %q", got, want) - } -} - -type shouldEscapeTest struct { - in byte - mode encoding - escape bool -} - -var shouldEscapeTests = []shouldEscapeTest{ - // Unreserved characters (§2.3) - {'a', encodePath, false}, - {'a', encodeUserPassword, false}, - {'a', encodeQueryComponent, false}, - {'a', encodeFragment, false}, - {'a', encodeHost, false}, - {'z', encodePath, false}, - {'A', encodePath, false}, - {'Z', encodePath, false}, - {'0', encodePath, false}, - {'9', encodePath, false}, - {'-', encodePath, false}, - {'-', encodeUserPassword, false}, - {'-', encodeQueryComponent, false}, - {'-', encodeFragment, false}, - {'.', encodePath, false}, - {'_', encodePath, false}, - {'~', encodePath, false}, - - // User information (§3.2.1) - {':', encodeUserPassword, true}, - {'/', encodeUserPassword, true}, - {'?', encodeUserPassword, true}, - {'@', encodeUserPassword, true}, - {'$', encodeUserPassword, false}, - {'&', encodeUserPassword, false}, - {'+', encodeUserPassword, false}, - {',', encodeUserPassword, false}, - {';', encodeUserPassword, false}, - {'=', encodeUserPassword, false}, - - // Host (IP address, IPv6 address, registered name, port suffix; §3.2.2) - {'!', encodeHost, false}, - {'$', encodeHost, false}, - {'&', encodeHost, false}, - {'\'', encodeHost, false}, - {'(', encodeHost, false}, - {')', encodeHost, false}, - {'*', encodeHost, false}, - {'+', encodeHost, false}, - {',', encodeHost, false}, - {';', encodeHost, false}, - {'=', encodeHost, false}, - {':', encodeHost, false}, - {'[', encodeHost, false}, - {']', encodeHost, false}, - {'0', encodeHost, false}, - {'9', encodeHost, false}, - {'A', encodeHost, false}, - {'z', encodeHost, false}, - {'_', encodeHost, false}, - {'-', encodeHost, false}, - {'.', encodeHost, false}, -} - -func TestShouldEscape(t *testing.T) { - for _, tt := range shouldEscapeTests { - if shouldEscape(tt.in, tt.mode) != tt.escape { - t.Errorf("shouldEscape(%q, %v) returned %v; expected %v", tt.in, tt.mode, !tt.escape, tt.escape) - } - } -} - -type timeoutError struct { - timeout bool -} - -func (e *timeoutError) Error() string { return "timeout error" } -func (e *timeoutError) Timeout() bool { return e.timeout } - -type temporaryError struct { - temporary bool -} - -func (e *temporaryError) Error() string { return "temporary error" } -func (e *temporaryError) Temporary() bool { return e.temporary } - -type timeoutTemporaryError struct { - timeoutError - temporaryError -} - -func (e *timeoutTemporaryError) Error() string { return "timeout/temporary error" } - -var netErrorTests = []struct { - err error - timeout bool - temporary bool -}{{ - err: &Error{"Get", "http://google.com/", &timeoutError{timeout: true}}, - timeout: true, - temporary: false, -}, { - err: &Error{"Get", "http://google.com/", &timeoutError{timeout: false}}, - timeout: false, - temporary: false, -}, { - err: &Error{"Get", "http://google.com/", &temporaryError{temporary: true}}, - timeout: false, - temporary: true, -}, { - err: &Error{"Get", "http://google.com/", &temporaryError{temporary: false}}, - timeout: false, - temporary: false, -}, { - err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: true}, temporaryError{temporary: true}}}, - timeout: true, - temporary: true, -}, { - err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: false}, temporaryError{temporary: true}}}, - timeout: false, - temporary: true, -}, { - err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: true}, temporaryError{temporary: false}}}, - timeout: true, - temporary: false, -}, { - err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: false}, temporaryError{temporary: false}}}, - timeout: false, - temporary: false, -}, { - err: &Error{"Get", "http://google.com/", io.EOF}, - timeout: false, - temporary: false, -}} - -// Test that url.Error implements net.Error and that it forwards -func TestURLErrorImplementsNetError(t *testing.T) { - for i, tt := range netErrorTests { - err, ok := tt.err.(net.Error) - if !ok { - t.Errorf("%d: %T does not implement net.Error", i+1, tt.err) - continue - } - if err.Timeout() != tt.timeout { - t.Errorf("%d: err.Timeout(): got %v, want %v", i+1, err.Timeout(), tt.timeout) - continue - } - if err.Temporary() != tt.temporary { - t.Errorf("%d: err.Temporary(): got %v, want %v", i+1, err.Temporary(), tt.temporary) - } - } -} - -func TestURLHostnameAndPort(t *testing.T) { - tests := []struct { - in string // URL.Host field - host string - port string - }{ - {"foo.com:80", "foo.com", "80"}, - {"foo.com", "foo.com", ""}, - {"foo.com:", "foo.com", ""}, - {"FOO.COM", "FOO.COM", ""}, // no canonicalization - {"1.2.3.4", "1.2.3.4", ""}, - {"1.2.3.4:80", "1.2.3.4", "80"}, - {"[1:2:3:4]", "1:2:3:4", ""}, - {"[1:2:3:4]:80", "1:2:3:4", "80"}, - {"[::1]:80", "::1", "80"}, - {"[::1]", "::1", ""}, - {"[::1]:", "::1", ""}, - {"localhost", "localhost", ""}, - {"localhost:443", "localhost", "443"}, - {"some.super.long.domain.example.org:8080", "some.super.long.domain.example.org", "8080"}, - {"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:17000", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", "17000"}, - {"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ""}, - - // Ensure that even when not valid, Host is one of "Hostname", - // "Hostname:Port", "[Hostname]" or "[Hostname]:Port". - // See https://golang.org/issue/29098. - {"[google.com]:80", "google.com", "80"}, - {"google.com]:80", "google.com]", "80"}, - {"google.com:80_invalid_port", "google.com:80_invalid_port", ""}, - {"[::1]extra]:80", "::1]extra", "80"}, - {"google.com]extra:extra", "google.com]extra:extra", ""}, - } - for _, tt := range tests { - u := &URL{Host: tt.in} - host, port := u.Hostname(), u.Port() - if host != tt.host { - t.Errorf("Hostname for Host %q = %q; want %q", tt.in, host, tt.host) - } - if port != tt.port { - t.Errorf("Port for Host %q = %q; want %q", tt.in, port, tt.port) - } - } -} - -var _ encodingPkg.BinaryMarshaler = (*URL)(nil) -var _ encodingPkg.BinaryUnmarshaler = (*URL)(nil) - -func TestJSON(t *testing.T) { - u, err := Parse("https://www.google.com/x?y=z") - if err != nil { - t.Fatal(err) - } - js, err := json.Marshal(u) - if err != nil { - t.Fatal(err) - } - - // If only we could implement TextMarshaler/TextUnmarshaler, - // this would work: - // - // if string(js) != strconv.Quote(u.String()) { - // t.Errorf("json encoding: %s\nwant: %s\n", js, strconv.Quote(u.String())) - // } - - u1 := new(URL) - err = json.Unmarshal(js, u1) - if err != nil { - t.Fatal(err) - } - if u1.String() != u.String() { - t.Errorf("json decoded to: %s\nwant: %s\n", u1, u) - } -} - -func TestGob(t *testing.T) { - u, err := Parse("https://www.google.com/x?y=z") - if err != nil { - t.Fatal(err) - } - var w bytes.Buffer - err = gob.NewEncoder(&w).Encode(u) - if err != nil { - t.Fatal(err) - } - - u1 := new(URL) - err = gob.NewDecoder(&w).Decode(u1) - if err != nil { - t.Fatal(err) - } - if u1.String() != u.String() { - t.Errorf("json decoded to: %s\nwant: %s\n", u1, u) - } -} - -func TestNilUser(t *testing.T) { - defer func() { - if v := recover(); v != nil { - t.Fatalf("unexpected panic: %v", v) - } - }() - - u, err := Parse("http://foo.com/") - - if err != nil { - t.Fatalf("parse err: %v", err) - } - - if v := u.User.Username(); v != "" { - t.Fatalf("expected empty username, got %s", v) - } - - if v, ok := u.User.Password(); v != "" || ok { - t.Fatalf("expected empty password, got %s (%v)", v, ok) - } - - if v := u.User.String(); v != "" { - t.Fatalf("expected empty string, got %s", v) - } -} - -func TestInvalidUserPassword(t *testing.T) { - _, err := Parse("http://user^:passwo^rd@foo.com/") - if got, wantsub := fmt.Sprint(err), "net/url: invalid userinfo"; !strings.Contains(got, wantsub) { - t.Errorf("error = %q; want substring %q", got, wantsub) - } -} - -func TestRejectControlCharacters(t *testing.T) { - tests := []string{ - "http://foo.com/?foo\nbar", - "http\r://foo.com/", - "http://foo\x7f.com/", - } - for _, s := range tests { - _, err := Parse(s) - const wantSub = "net/url: invalid control character in URL" - if got := fmt.Sprint(err); !strings.Contains(got, wantSub) { - t.Errorf("Parse(%q) error = %q; want substring %q", s, got, wantSub) - } - } - - // But don't reject non-ASCII CTLs, at least for now: - if _, err := Parse("http://foo.com/ctl\x80"); err != nil { - t.Errorf("error parsing URL with non-ASCII control byte: %v", err) - } - -} - -var escapeBenchmarks = []struct { - unescaped string - query string - path string -}{ - { - unescaped: "one two", - query: "one+two", - path: "one%20two", - }, - { - unescaped: "Фотки собак", - query: "%D0%A4%D0%BE%D1%82%D0%BA%D0%B8+%D1%81%D0%BE%D0%B1%D0%B0%D0%BA", - path: "%D0%A4%D0%BE%D1%82%D0%BA%D0%B8%20%D1%81%D0%BE%D0%B1%D0%B0%D0%BA", - }, - - { - unescaped: "shortrun(break)shortrun", - query: "shortrun%28break%29shortrun", - path: "shortrun%28break%29shortrun", - }, - - { - unescaped: "longerrunofcharacters(break)anotherlongerrunofcharacters", - query: "longerrunofcharacters%28break%29anotherlongerrunofcharacters", - path: "longerrunofcharacters%28break%29anotherlongerrunofcharacters", - }, - - { - unescaped: strings.Repeat("padded/with+various%characters?that=need$some@escaping+paddedsowebreak/256bytes", 4), - query: strings.Repeat("padded%2Fwith%2Bvarious%25characters%3Fthat%3Dneed%24some%40escaping%2Bpaddedsowebreak%2F256bytes", 4), - path: strings.Repeat("padded%2Fwith+various%25characters%3Fthat=need$some@escaping+paddedsowebreak%2F256bytes", 4), - }, -} - -func BenchmarkQueryEscape(b *testing.B) { - for _, tc := range escapeBenchmarks { - b.Run("", func(b *testing.B) { - b.ReportAllocs() - var g string - for i := 0; i < b.N; i++ { - g = QueryEscape(tc.unescaped) - } - b.StopTimer() - if g != tc.query { - b.Errorf("QueryEscape(%q) == %q, want %q", tc.unescaped, g, tc.query) - } - - }) - } -} - -func BenchmarkPathEscape(b *testing.B) { - for _, tc := range escapeBenchmarks { - b.Run("", func(b *testing.B) { - b.ReportAllocs() - var g string - for i := 0; i < b.N; i++ { - g = PathEscape(tc.unescaped) - } - b.StopTimer() - if g != tc.path { - b.Errorf("PathEscape(%q) == %q, want %q", tc.unescaped, g, tc.path) - } - - }) - } -} - -func BenchmarkQueryUnescape(b *testing.B) { - for _, tc := range escapeBenchmarks { - b.Run("", func(b *testing.B) { - b.ReportAllocs() - var g string - for i := 0; i < b.N; i++ { - g, _ = QueryUnescape(tc.query) - } - b.StopTimer() - if g != tc.unescaped { - b.Errorf("QueryUnescape(%q) == %q, want %q", tc.query, g, tc.unescaped) - } - - }) - } -} - -func BenchmarkPathUnescape(b *testing.B) { - for _, tc := range escapeBenchmarks { - b.Run("", func(b *testing.B) { - b.ReportAllocs() - var g string - for i := 0; i < b.N; i++ { - g, _ = PathUnescape(tc.path) - } - b.StopTimer() - if g != tc.unescaped { - b.Errorf("PathUnescape(%q) == %q, want %q", tc.path, g, tc.unescaped) - } - - }) - } -} - -func TestJoinPath(t *testing.T) { - tests := []struct { - base string - elem []string - out string - }{ - { - base: "https://go.googlesource.com", - elem: []string{"go"}, - out: "https://go.googlesource.com/go", - }, - { - base: "https://go.googlesource.com/a/b/c", - elem: []string{"../../../go"}, - out: "https://go.googlesource.com/go", - }, - { - base: "https://go.googlesource.com/", - elem: []string{"../go"}, - out: "https://go.googlesource.com/go", - }, - { - base: "https://go.googlesource.com", - elem: []string{"../go"}, - out: "https://go.googlesource.com/go", - }, - { - base: "https://go.googlesource.com", - elem: []string{"../go", "../../go", "../../../go"}, - out: "https://go.googlesource.com/go", - }, - { - base: "https://go.googlesource.com/../go", - elem: nil, - out: "https://go.googlesource.com/go", - }, - { - base: "https://go.googlesource.com/", - elem: []string{"./go"}, - out: "https://go.googlesource.com/go", - }, - { - base: "https://go.googlesource.com//", - elem: []string{"/go"}, - out: "https://go.googlesource.com/go", - }, - { - base: "https://go.googlesource.com//", - elem: []string{"/go", "a", "b", "c"}, - out: "https://go.googlesource.com/go/a/b/c", - }, - { - base: "http://[fe80::1%en0]:8080/", - elem: []string{"/go"}, - out: "http://[fe80::1%25en0]:8080/go", - }, - { - base: "https://go.googlesource.com", - elem: []string{"go/"}, - out: "https://go.googlesource.com/go/", - }, - { - base: "https://go.googlesource.com", - elem: []string{"go//"}, - out: "https://go.googlesource.com/go/", - }, - { - base: "https://go.googlesource.com", - elem: nil, - out: "https://go.googlesource.com/", - }, - { - base: "https://go.googlesource.com/", - elem: nil, - out: "https://go.googlesource.com/", - }, - { - base: "https://go.googlesource.com/a%2fb", - elem: []string{"c"}, - out: "https://go.googlesource.com/a%2fb/c", - }, - { - base: "https://go.googlesource.com/a%2fb", - elem: []string{"c%2fd"}, - out: "https://go.googlesource.com/a%2fb/c%2fd", - }, - { - base: "https://go.googlesource.com/a/b", - elem: []string{"/go"}, - out: "https://go.googlesource.com/a/b/go", - }, - { - base: "/", - elem: nil, - out: "/", - }, - { - base: "a", - elem: nil, - out: "a", - }, - { - base: "a", - elem: []string{"b"}, - out: "a/b", - }, - { - base: "a", - elem: []string{"../b"}, - out: "b", - }, - { - base: "a", - elem: []string{"../../b"}, - out: "b", - }, - { - base: "", - elem: []string{"a"}, - out: "a", - }, - { - base: "", - elem: []string{"../a"}, - out: "a", - }, - } - for _, tt := range tests { - wantErr := "nil" - if tt.out == "" { - wantErr = "non-nil error" - } - if out, err := JoinPath(tt.base, tt.elem...); out != tt.out || (err == nil) != (tt.out != "") { - t.Errorf("JoinPath(%q, %q) = %q, %v, want %q, %v", tt.base, tt.elem, out, err, tt.out, wantErr) - } - var out string - u, err := Parse(tt.base) - if err == nil { - u = u.JoinPath(tt.elem...) - out = u.String() - } - if out != tt.out || (err == nil) != (tt.out != "") { - t.Errorf("Parse(%q).JoinPath(%q) = %q, %v, want %q, %v", tt.base, tt.elem, out, err, tt.out, wantErr) - } - } -}