diff options
Diffstat (limited to 'src/pkg/http')
-rw-r--r-- | src/pkg/http/client.go | 9 | ||||
-rw-r--r-- | src/pkg/http/fs.go | 6 | ||||
-rw-r--r-- | src/pkg/http/fs_test.go | 2 | ||||
-rw-r--r-- | src/pkg/http/readrequest_test.go | 35 | ||||
-rw-r--r-- | src/pkg/http/request.go | 2 | ||||
-rw-r--r-- | src/pkg/http/serve_test.go | 150 | ||||
-rw-r--r-- | src/pkg/http/server.go | 72 | ||||
-rw-r--r-- | src/pkg/http/url.go | 182 | ||||
-rw-r--r-- | src/pkg/http/url_test.go | 232 |
9 files changed, 554 insertions, 136 deletions
diff --git a/src/pkg/http/client.go b/src/pkg/http/client.go index 29678ee32..022f4f124 100644 --- a/src/pkg/http/client.go +++ b/src/pkg/http/client.go @@ -120,6 +120,7 @@ func Get(url string) (r *Response, finalURL string, err os.Error) { // TODO: if/when we add cookie support, the redirected request shouldn't // necessarily supply the same cookies as the original. // TODO: set referrer header on redirects. + var base *URL for redirect := 0; ; redirect++ { if redirect >= 10 { err = os.ErrorString("stopped after 10 redirects") @@ -127,7 +128,12 @@ func Get(url string) (r *Response, finalURL string, err os.Error) { } var req Request - if req.URL, err = ParseURL(url); err != nil { + if base == nil { + req.URL, err = ParseURL(url) + } else { + req.URL, err = base.ParseURL(url) + } + if err != nil { break } url = req.URL.String() @@ -140,6 +146,7 @@ func Get(url string) (r *Response, finalURL string, err os.Error) { err = os.ErrorString(fmt.Sprintf("%d response missing Location header", r.StatusCode)) break } + base = req.URL continue } finalURL = url diff --git a/src/pkg/http/fs.go b/src/pkg/http/fs.go index 143a839a8..bbfa58d26 100644 --- a/src/pkg/http/fs.go +++ b/src/pkg/http/fs.go @@ -166,7 +166,7 @@ func serveFile(w ResponseWriter, r *Request, name string, redirect bool) { } size = ra.length code = StatusPartialContent - w.SetHeader("Content-Range", fmt.Sprintf("%d-%d/%d", ra.start, ra.start+ra.length, d.Size)) + w.SetHeader("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, d.Size)) } w.SetHeader("Accept-Ranges", "bytes") @@ -174,7 +174,9 @@ func serveFile(w ResponseWriter, r *Request, name string, redirect bool) { w.WriteHeader(code) - io.Copyn(w, f, size) + if r.Method != "HEAD" { + io.Copyn(w, f, size) + } } // ServeFile replies to the request with the contents of the named file or directory. diff --git a/src/pkg/http/fs_test.go b/src/pkg/http/fs_test.go index 0f7135692..0a5636b88 100644 --- a/src/pkg/http/fs_test.go +++ b/src/pkg/http/fs_test.go @@ -134,7 +134,7 @@ func TestServeFile(t *testing.T) { if rt.code == StatusRequestedRangeNotSatisfiable { continue } - h := fmt.Sprintf("%d-%d/%d", rt.start, rt.end, testFileLength) + h := fmt.Sprintf("bytes %d-%d/%d", rt.start, rt.end-1, testFileLength) if rt.r == "" { h = "" } diff --git a/src/pkg/http/readrequest_test.go b/src/pkg/http/readrequest_test.go index 067e17dda..5e1cbcbcb 100644 --- a/src/pkg/http/readrequest_test.go +++ b/src/pkg/http/readrequest_test.go @@ -69,6 +69,41 @@ var reqTests = []reqTest{ "abcdef\n", }, + + // Tests that we don't parse a path that looks like a + // scheme-relative URI as a scheme-relative URI. + { + "GET //user@host/is/actually/a/path/ HTTP/1.1\r\n" + + "Host: test\r\n\r\n", + + Request{ + Method: "GET", + RawURL: "//user@host/is/actually/a/path/", + URL: &URL{ + Raw: "//user@host/is/actually/a/path/", + Scheme: "", + RawPath: "//user@host/is/actually/a/path/", + RawAuthority: "", + RawUserinfo: "", + Host: "", + Path: "//user@host/is/actually/a/path/", + RawQuery: "", + Fragment: "", + }, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: map[string]string{}, + Close: false, + ContentLength: -1, + Host: "test", + Referer: "", + UserAgent: "", + Form: map[string][]string{}, + }, + + "", + }, } func TestReadRequest(t *testing.T) { diff --git a/src/pkg/http/request.go b/src/pkg/http/request.go index b88689988..04bebaaf5 100644 --- a/src/pkg/http/request.go +++ b/src/pkg/http/request.go @@ -504,7 +504,7 @@ func ReadRequest(b *bufio.Reader) (req *Request, err os.Error) { return nil, &badStringError{"malformed HTTP version", req.Proto} } - if req.URL, err = ParseURL(req.RawURL); err != nil { + if req.URL, err = ParseRequestURL(req.RawURL); err != nil { return nil, err } diff --git a/src/pkg/http/serve_test.go b/src/pkg/http/serve_test.go index 43e1b93a5..7da3fc6f3 100644 --- a/src/pkg/http/serve_test.go +++ b/src/pkg/http/serve_test.go @@ -7,7 +7,9 @@ package http import ( + "bufio" "bytes" + "io" "os" "net" "testing" @@ -133,3 +135,151 @@ func TestConsumingBodyOnNextConn(t *testing.T) { t.Errorf("Serve returned %q; expected EOF", serveerr) } } + +type stringHandler string + +func (s stringHandler) ServeHTTP(w ResponseWriter, r *Request) { + w.SetHeader("Result", string(s)) +} + +var handlers = []struct { + pattern string + msg string +}{ + {"/", "Default"}, + {"/someDir/", "someDir"}, + {"someHost.com/someDir/", "someHost.com/someDir"}, +} + +var vtests = []struct { + url string + expected string +}{ + {"http://localhost/someDir/apage", "someDir"}, + {"http://localhost/otherDir/apage", "Default"}, + {"http://someHost.com/someDir/apage", "someHost.com/someDir"}, + {"http://otherHost.com/someDir/apage", "someDir"}, + {"http://otherHost.com/aDir/apage", "Default"}, +} + +func TestHostHandlers(t *testing.T) { + for _, h := range handlers { + Handle(h.pattern, stringHandler(h.msg)) + } + l, err := net.Listen("tcp", "127.0.0.1:0") // any port + if err != nil { + t.Fatal(err) + } + defer l.Close() + go Serve(l, nil) + conn, err := net.Dial("tcp", "", l.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + cc := NewClientConn(conn, nil) + for _, vt := range vtests { + var r *Response + var req Request + if req.URL, err = ParseURL(vt.url); err != nil { + t.Errorf("cannot parse url: %v", err) + continue + } + if err := cc.Write(&req); err != nil { + t.Errorf("writing request: %v", err) + continue + } + r, err := cc.Read() + if err != nil { + t.Errorf("reading response: %v", err) + continue + } + s := r.Header["Result"] + if s != vt.expected { + t.Errorf("Get(%q) = %q, want %q", vt.url, s, vt.expected) + } + } +} + +type responseWriterMethodCall struct { + method string + headerKey, headerValue string // if method == "SetHeader" + bytesWritten []byte // if method == "Write" + responseCode int // if method == "WriteHeader" +} + +type recordingResponseWriter struct { + log []*responseWriterMethodCall +} + +func (rw *recordingResponseWriter) RemoteAddr() string { + return "1.2.3.4" +} + +func (rw *recordingResponseWriter) UsingTLS() bool { + return false +} + +func (rw *recordingResponseWriter) SetHeader(k, v string) { + rw.log = append(rw.log, &responseWriterMethodCall{method: "SetHeader", headerKey: k, headerValue: v}) +} + +func (rw *recordingResponseWriter) Write(buf []byte) (int, os.Error) { + rw.log = append(rw.log, &responseWriterMethodCall{method: "Write", bytesWritten: buf}) + return len(buf), nil +} + +func (rw *recordingResponseWriter) WriteHeader(code int) { + rw.log = append(rw.log, &responseWriterMethodCall{method: "WriteHeader", responseCode: code}) +} + +func (rw *recordingResponseWriter) Flush() { + rw.log = append(rw.log, &responseWriterMethodCall{method: "Flush"}) +} + +func (rw *recordingResponseWriter) Hijack() (io.ReadWriteCloser, *bufio.ReadWriter, os.Error) { + panic("Not supported") +} + +// Tests for http://code.google.com/p/go/issues/detail?id=900 +func TestMuxRedirectLeadingSlashes(t *testing.T) { + paths := []string{"//foo.txt", "///foo.txt", "/../../foo.txt"} + for _, path := range paths { + req, err := ReadRequest(bufio.NewReader(bytes.NewBufferString("GET " + path + " HTTP/1.1\r\nHost: test\r\n\r\n"))) + if err != nil { + t.Errorf("%s", err) + } + mux := NewServeMux() + resp := new(recordingResponseWriter) + resp.log = make([]*responseWriterMethodCall, 0) + + mux.ServeHTTP(resp, req) + + dumpLog := func() { + t.Logf("For path %q:", path) + for _, call := range resp.log { + t.Logf("Got call: %s, header=%s, value=%s, buf=%q, code=%d", call.method, + call.headerKey, call.headerValue, call.bytesWritten, call.responseCode) + } + } + + if len(resp.log) != 2 { + dumpLog() + t.Errorf("expected 2 calls to response writer; got %d", len(resp.log)) + return + } + + if resp.log[0].method != "SetHeader" || + resp.log[0].headerKey != "Location" || resp.log[0].headerValue != "/foo.txt" { + dumpLog() + t.Errorf("Expected SetHeader of Location to /foo.txt") + return + } + + if resp.log[1].method != "WriteHeader" || resp.log[1].responseCode != StatusMovedPermanently { + dumpLog() + t.Errorf("Expected WriteHeader of StatusMovedPermanently") + return + } + } +} diff --git a/src/pkg/http/server.go b/src/pkg/http/server.go index b8783da28..6672c494b 100644 --- a/src/pkg/http/server.go +++ b/src/pkg/http/server.go @@ -181,7 +181,9 @@ func (c *conn) readRequest() (w *response, err os.Error) { w.SetHeader("Content-Type", "text/html; charset=utf-8") w.SetHeader("Date", time.UTC().Format(TimeFormat)) - if req.ProtoAtLeast(1, 1) { + if req.Method == "HEAD" { + // do nothing + } else if req.ProtoAtLeast(1, 1) { // HTTP/1.1 or greater: use chunked transfer encoding // to avoid closing the connection at EOF. w.chunking = true @@ -227,6 +229,10 @@ func (w *response) WriteHeader(code int) { w.header["Transfer-Encoding"] = "", false w.chunking = false } + // Cannot use Content-Length with non-identity Transfer-Encoding. + if w.chunking { + w.header["Content-Length"] = "", false + } if !w.req.ProtoAtLeast(1, 0) { return } @@ -268,7 +274,7 @@ func (w *response) Write(data []byte) (n int, err os.Error) { return 0, nil } - if w.status == StatusNotModified { + if w.status == StatusNotModified || w.req.Method == "HEAD" { // Must not have body. return 0, ErrBodyNotAllowed } @@ -495,11 +501,11 @@ func Redirect(w ResponseWriter, r *Request, url string, code int) { // RFC2616 recommends that a short note "SHOULD" be included in the // response because older user agents may not understand 301/307. - note := "<a href=\"" + htmlEscape(url) + "\">" + statusText[code] + "</a>.\n" - if r.Method == "POST" { - note = "" + // Shouldn't send the response for POST or HEAD; that leaves GET. + if r.Method == "GET" { + note := "<a href=\"" + htmlEscape(url) + "\">" + statusText[code] + "</a>.\n" + fmt.Fprintln(w, note) } - fmt.Fprintln(w, note) } func htmlEscape(s string) string { @@ -533,9 +539,8 @@ func RedirectHandler(url string, code int) Handler { // patterns and calls the handler for the pattern that // most closely matches the URL. // -// Patterns named fixed paths, like "/favicon.ico", -// or subtrees, like "/images/" (note the trailing slash). -// Patterns must begin with /. +// Patterns named fixed, rooted paths, like "/favicon.ico", +// or rooted subtrees, like "/images/" (note the trailing slash). // Longer patterns take precedence over shorter ones, so that // if there are handlers registered for both "/images/" // and "/images/thumbnails/", the latter handler will be @@ -543,11 +548,11 @@ func RedirectHandler(url string, code int) Handler { // former will receiver requests for any other paths in the // "/images/" subtree. // -// In the future, the pattern syntax may be relaxed to allow -// an optional host-name at the beginning of the pattern, -// so that a handler might register for the two patterns -// "/codesearch" and "codesearch.google.com/" -// without taking over requests for http://www.google.com/. +// Patterns may optionally begin with a host name, restricting matches to +// URLs on that host only. Host-specific patterns take precedence over +// general patterns, so that a handler might register for the two patterns +// "/codesearch" and "codesearch.google.com/" without also taking over +// requests for "http://www.google.com/". // // ServeMux also takes care of sanitizing the URL request path, // redirecting any request containing . or .. elements to an @@ -592,21 +597,13 @@ func cleanPath(p string) string { return np } -// ServeHTTP dispatches the request to the handler whose -// pattern most closely matches the request URL. -func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { - // Clean path to canonical form and redirect. - if p := cleanPath(r.URL.Path); p != r.URL.Path { - w.SetHeader("Location", p) - w.WriteHeader(StatusMovedPermanently) - return - } - - // Most-specific (longest) pattern wins. +// Find a handler on a handler map given a path string +// Most-specific (longest) pattern wins +func (mux *ServeMux) match(path string) Handler { var h Handler var n = 0 for k, v := range mux.m { - if !pathMatch(k, r.URL.Path) { + if !pathMatch(k, path) { continue } if h == nil || len(k) > n { @@ -614,6 +611,23 @@ func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { h = v } } + return h +} + +// ServeHTTP dispatches the request to the handler whose +// pattern most closely matches the request URL. +func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { + // Clean path to canonical form and redirect. + if p := cleanPath(r.URL.Path); p != r.URL.Path { + w.SetHeader("Location", p) + w.WriteHeader(StatusMovedPermanently) + return + } + // Host-specific pattern takes precedence over generic ones + h := mux.match(r.Host + r.URL.Path) + if h == nil { + h = mux.match(r.URL.Path) + } if h == nil { h = NotFoundHandler() } @@ -622,7 +636,7 @@ func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { // Handle registers the handler for the given pattern. func (mux *ServeMux) Handle(pattern string, handler Handler) { - if pattern == "" || pattern[0] != '/' { + if pattern == "" { panic("http: invalid pattern " + pattern) } @@ -697,7 +711,7 @@ func Serve(l net.Listener, handler Handler) os.Error { // http.HandleFunc("/hello", HelloServer) // err := http.ListenAndServe(":12345", nil) // if err != nil { -// log.Exit("ListenAndServe: ", err.String()) +// log.Fatal("ListenAndServe: ", err.String()) // } // } func ListenAndServe(addr string, handler Handler) os.Error { @@ -731,7 +745,7 @@ func ListenAndServe(addr string, handler Handler) os.Error { // log.Printf("About to listen on 10443. Go to https://127.0.0.1:10443/") // err := http.ListenAndServeTLS(":10443", "cert.pem", "key.pem", nil) // if err != nil { -// log.Exit(err) +// log.Fatal(err) // } // } // diff --git a/src/pkg/http/url.go b/src/pkg/http/url.go index f0ac4c1df..efd90d81e 100644 --- a/src/pkg/http/url.go +++ b/src/pkg/http/url.go @@ -114,62 +114,6 @@ func shouldEscape(c byte, mode encoding) bool { return true } -// CanonicalPath applies the algorithm specified in RFC 2396 to -// simplify the path, removing unnecessary . and .. elements. -func CanonicalPath(path string) string { - buf := []byte(path) - a := buf[0:0] - // state helps to find /.. ^.. ^. and /. patterns. - // state == 1 - prev char is '/' or beginning of the string. - // state > 1 - prev state > 0 and prev char was '.' - // state == 0 - otherwise - state := 1 - cnt := 0 - for _, v := range buf { - switch v { - case '/': - s := state - state = 1 - switch s { - case 2: - a = a[0 : len(a)-1] - continue - case 3: - if cnt > 0 { - i := len(a) - 4 - for ; i >= 0 && a[i] != '/'; i-- { - } - a = a[0 : i+1] - cnt-- - continue - } - default: - if len(a) > 0 { - cnt++ - } - } - case '.': - if state > 0 { - state++ - } - default: - state = 0 - } - l := len(a) - a = a[0 : l+1] - a[l] = v - } - switch { - case state == 2: - a = a[0 : len(a)-1] - case state == 3 && cnt > 0: - i := len(a) - 4 - for ; i >= 0 && a[i] != '/'; i-- { - } - a = a[0 : i+1] - } - return string(a) -} // URLUnescape unescapes a string in ``URL encoded'' form, // converting %AB into the byte 0xAB and '+' into ' ' (space). @@ -385,7 +329,25 @@ func split(s string, c byte, cutc bool) (string, string) { // ParseURL parses rawurl into a URL structure. // The string rawurl is assumed not to have a #fragment suffix. // (Web browsers strip #fragment before sending the URL to a web server.) +// The rawurl may be relative or absolute. func ParseURL(rawurl string) (url *URL, err os.Error) { + return parseURL(rawurl, false) +} + +// ParseRequestURL parses rawurl into a URL structure. It assumes that +// rawurl was received from an HTTP request, so the rawurl is interpreted +// only as an absolute URI or an absolute path. +// The string rawurl is assumed not to have a #fragment suffix. +// (Web browsers strip #fragment before sending the URL to a web server.) +func ParseRequestURL(rawurl string) (url *URL, err os.Error) { + return parseURL(rawurl, true) +} + +// parseURL 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 parseURL(rawurl string, viaRequest bool) (url *URL, err os.Error) { if rawurl == "" { err = os.ErrorString("empty url") goto Error @@ -400,7 +362,9 @@ func ParseURL(rawurl string) (url *URL, err os.Error) { goto Error } - if url.Scheme != "" && (len(path) == 0 || path[0] != '/') { + leadingSlash := strings.HasPrefix(path, "/") + + if url.Scheme != "" && !leadingSlash { // RFC 2396: // Absolute URI (has scheme) with non-rooted path // is uninterpreted. It doesn't even have a ?query. @@ -412,6 +376,11 @@ func ParseURL(rawurl string) (url *URL, err os.Error) { } url.OpaquePath = true } else { + if viaRequest && !leadingSlash { + err = os.ErrorString("invalid URI for request") + goto Error + } + // Split off query before parsing path further. url.RawPath = path path, query := split(path, '?', false) @@ -420,7 +389,8 @@ func ParseURL(rawurl string) (url *URL, err os.Error) { } // Maybe path is //authority/path - if url.Scheme != "" && len(path) > 2 && path[0:2] == "//" { + if (url.Scheme != "" || !viaRequest) && + strings.HasPrefix(path, "//") && !strings.HasPrefix(path, "///") { url.RawAuthority, path = split(path[2:], '/', false) url.RawPath = url.RawPath[2+len(url.RawAuthority):] } @@ -527,3 +497,99 @@ func EncodeQuery(m map[string][]string) string { } return strings.Join(parts, "&") } + +// resolvePath applies special path segments from refs and applies +// them to base, per RFC 2396. +func resolvePath(basepath string, refpath string) string { + base := strings.Split(basepath, "/", -1) + refs := strings.Split(refpath, "/", -1) + if len(base) == 0 { + base = []string{""} + } + for idx, ref := range refs { + switch { + case ref == ".": + base[len(base)-1] = "" + case ref == "..": + newLen := len(base) - 1 + if newLen < 1 { + newLen = 1 + } + base = base[0:newLen] + base[len(base)-1] = "" + default: + if idx == 0 || base[len(base)-1] == "" { + base[len(base)-1] = ref + } else { + base = append(base, ref) + } + } + } + return strings.Join(base, "/") +} + +// IsAbs returns true if the URL is absolute. +func (url *URL) IsAbs() bool { + return url.Scheme != "" +} + +// ParseURL parses a URL in the context of a base URL. The URL in ref +// may be relative or absolute. ParseURL returns nil, err on parse +// failure, otherwise its return value is the same as ResolveReference. +func (base *URL) ParseURL(ref string) (*URL, os.Error) { + refurl, err := ParseURL(ref) + if err != nil { + return nil, err + } + return base.ResolveReference(refurl), nil +} + +// ResolveReference resolves a URI reference to an absolute URI from +// an absolute base URI, per RFC 2396 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 (base *URL) ResolveReference(ref *URL) *URL { + url := new(URL) + switch { + case ref.IsAbs(): + *url = *ref + default: + // relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + *url = *base + if ref.RawAuthority != "" { + // The "net_path" case. + url.RawAuthority = ref.RawAuthority + url.Host = ref.Host + url.RawUserinfo = ref.RawUserinfo + } + switch { + case url.OpaquePath: + url.Path = ref.Path + url.RawPath = ref.RawPath + url.RawQuery = ref.RawQuery + case strings.HasPrefix(ref.Path, "/"): + // The "abs_path" case. + url.Path = ref.Path + url.RawPath = ref.RawPath + url.RawQuery = ref.RawQuery + default: + // The "rel_path" case. + path := resolvePath(base.Path, ref.Path) + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + url.Path = path + url.RawPath = url.Path + url.RawQuery = ref.RawQuery + if ref.RawQuery != "" { + url.RawPath += "?" + url.RawQuery + } + } + + url.Fragment = ref.Fragment + } + url.Raw = url.String() + return url +} diff --git a/src/pkg/http/url_test.go b/src/pkg/http/url_test.go index 447d5390e..0801f7ff3 100644 --- a/src/pkg/http/url_test.go +++ b/src/pkg/http/url_test.go @@ -188,14 +188,48 @@ var urltests = []URLTest{ }, "", }, - // leading // without scheme shouldn't create an authority + // leading // without scheme should create an authority { "//foo", &URL{ - Raw: "//foo", - Scheme: "", - RawPath: "//foo", - Path: "//foo", + RawAuthority: "foo", + Raw: "//foo", + Host: "foo", + Scheme: "", + RawPath: "", + Path: "", + }, + "", + }, + // leading // without scheme, with userinfo, path, and query + { + "//user@foo/path?a=b", + &URL{ + Raw: "//user@foo/path?a=b", + RawAuthority: "user@foo", + RawUserinfo: "user", + Scheme: "", + RawPath: "/path?a=b", + Path: "/path", + RawQuery: "a=b", + Host: "foo", + }, + "", + }, + // 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 -> ParseURL, which is arguably a + // different URL parsing context, but currently shares the + // same codepath) + { + "///threeslashes", + &URL{ + RawAuthority: "", + Raw: "///threeslashes", + Host: "", + Scheme: "", + RawPath: "///threeslashes", + Path: "///threeslashes", }, "", }, @@ -272,7 +306,7 @@ var urlfragtests = []URLTest{ // more useful string for debugging than fmt's struct printer func ufmt(u *URL) string { - return fmt.Sprintf("%q, %q, %q, %q, %q, %q, %q, %q, %q", + return fmt.Sprintf("raw=%q, scheme=%q, rawpath=%q, auth=%q, userinfo=%q, host=%q, path=%q, rawq=%q, frag=%q", u.Raw, u.Scheme, u.RawPath, u.RawAuthority, u.RawUserinfo, u.Host, u.Path, u.RawQuery, u.Fragment) } @@ -301,6 +335,40 @@ func TestParseURLReference(t *testing.T) { DoTest(t, ParseURLReference, "ParseURLReference", urlfragtests) } +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}, + {"foo.html", false}, + {"../dir/", false}, +} + +func TestParseRequestURL(t *testing.T) { + for _, test := range parseRequestUrlTests { + _, err := ParseRequestURL(test.url) + valid := err == nil + if valid != test.expectedValid { + t.Errorf("Expected valid=%v for %q; got %v", test.expectedValid, test.url, valid) + } + } + + url, err := ParseRequestURL(pathThatLooksSchemeRelative) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if url.Path != pathThatLooksSchemeRelative { + t.Errorf("Expected path %q; got %q", pathThatLooksSchemeRelative, url.Path) + } +} + func DoTestString(t *testing.T, parse func(string) (*URL, os.Error), name string, tests []URLTest) { for _, tt := range tests { u, err := parse(tt.in) @@ -442,44 +510,6 @@ func TestURLEscape(t *testing.T) { } } -type CanonicalPathTest struct { - in string - out string -} - -var canonicalTests = []CanonicalPathTest{ - {"", ""}, - {"/", "/"}, - {".", ""}, - {"./", ""}, - {"/a/", "/a/"}, - {"a/", "a/"}, - {"a/./", "a/"}, - {"./a", "a"}, - {"/a/../b", "/b"}, - {"a/../b", "b"}, - {"a/../../b", "../b"}, - {"a/.", "a/"}, - {"../.././a", "../../a"}, - {"/../.././a", "/../../a"}, - {"a/b/g/../..", "a/"}, - {"a/b/..", "a/"}, - {"a/b/.", "a/b/"}, - {"a/b/../../../..", "../.."}, - {"a./", "a./"}, - {"/../a/b/../../../", "/../../"}, - {"../a/b/../../../", "../../"}, -} - -func TestCanonicalPath(t *testing.T) { - for _, tt := range canonicalTests { - actual := CanonicalPath(tt.in) - if tt.out != actual { - t.Errorf("CanonicalPath(%q) = %q, want %q", tt.in, actual, tt.out) - } - } -} - type UserinfoTest struct { User string Password string @@ -529,3 +559,117 @@ func TestEncodeQuery(t *testing.T) { } } } + +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/../"}, + {"a/.././b", "c", "a/.././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) + } + } +} + +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/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?c=d", "http://foo.com/baz?c=d"}, + + // 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/"}, + + // "." and ".." in the base aren't special + {"http://foo.com/dot/./dotdot/../foo/bar", "../baz", "http://foo.com/dot/./dotdot/../baz"}, + + // Triple dot isn't special + {"http://foo.com/bar", "...", "http://foo.com/..."}, + + // Fragment + {"http://foo.com/bar", ".#frag", "http://foo.com/#frag"}, +} + +func TestResolveReference(t *testing.T) { + mustParseURL := func(url string) *URL { + u, err := ParseURLReference(url) + if err != nil { + t.Fatalf("Expected URL to parse: %q, got error: %v", url, err) + } + return u + } + for _, test := range resolveReferenceTests { + base := mustParseURL(test.base) + rel := mustParseURL(test.rel) + url := base.ResolveReference(rel) + urlStr := url.String() + if urlStr != test.expected { + t.Errorf("Resolving %q + %q != %q; got %q", test.base, test.rel, test.expected, urlStr) + } + } + + // Test that new instances are returned. + base := mustParseURL("http://foo.com/") + abs := base.ResolveReference(mustParseURL(".")) + if base == abs { + t.Errorf("Expected no-op reference to return new URL instance.") + } + barRef := mustParseURL("http://bar.com/") + abs = base.ResolveReference(barRef) + if abs == barRef { + t.Errorf("Expected resolution of absolute reference to return new URL instance.") + } + + // Test the convenience wrapper too + base = mustParseURL("http://foo.com/path/one/") + abs, _ = base.ParseURL("../two") + expected := "http://foo.com/path/two" + if abs.String() != expected { + t.Errorf("ParseURL wrapper got %q; expected %q", abs.String(), expected) + } + _, err := base.ParseURL("") + if err == nil { + t.Errorf("Expected an error from ParseURL wrapper parsing an empty string.") + } + +} |