summaryrefslogtreecommitdiff
path: root/src/pkg/http
diff options
context:
space:
mode:
Diffstat (limited to 'src/pkg/http')
-rw-r--r--src/pkg/http/client.go9
-rw-r--r--src/pkg/http/fs.go6
-rw-r--r--src/pkg/http/fs_test.go2
-rw-r--r--src/pkg/http/readrequest_test.go35
-rw-r--r--src/pkg/http/request.go2
-rw-r--r--src/pkg/http/serve_test.go150
-rw-r--r--src/pkg/http/server.go72
-rw-r--r--src/pkg/http/url.go182
-rw-r--r--src/pkg/http/url_test.go232
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.")
+ }
+
+}