diff options
Diffstat (limited to 'src/cmd/go/internal/web')
-rw-r--r-- | src/cmd/go/internal/web/api.go | 246 | ||||
-rw-r--r-- | src/cmd/go/internal/web/bootstrap.go | 25 | ||||
-rw-r--r-- | src/cmd/go/internal/web/file_test.go | 60 | ||||
-rw-r--r-- | src/cmd/go/internal/web/http.go | 399 | ||||
-rw-r--r-- | src/cmd/go/internal/web/url.go | 95 | ||||
-rw-r--r-- | src/cmd/go/internal/web/url_other.go | 21 | ||||
-rw-r--r-- | src/cmd/go/internal/web/url_other_test.go | 36 | ||||
-rw-r--r-- | src/cmd/go/internal/web/url_test.go | 77 | ||||
-rw-r--r-- | src/cmd/go/internal/web/url_windows.go | 43 | ||||
-rw-r--r-- | src/cmd/go/internal/web/url_windows_test.go | 94 |
10 files changed, 1096 insertions, 0 deletions
diff --git a/src/cmd/go/internal/web/api.go b/src/cmd/go/internal/web/api.go new file mode 100644 index 0000000..7a6e0c3 --- /dev/null +++ b/src/cmd/go/internal/web/api.go @@ -0,0 +1,246 @@ +// Copyright 2017 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 web defines minimal helper routines for accessing HTTP/HTTPS +// resources without requiring external dependencies on the net package. +// +// If the cmd_go_bootstrap build tag is present, web avoids the use of the net +// package and returns errors for all network operations. +package web + +import ( + "bytes" + "fmt" + "io" + "io/fs" + "net/url" + "strings" + "unicode" + "unicode/utf8" +) + +// SecurityMode specifies whether a function should make network +// calls using insecure transports (eg, plain text HTTP). +// The zero value is "secure". +type SecurityMode int + +const ( + SecureOnly SecurityMode = iota // Reject plain HTTP; validate HTTPS. + DefaultSecurity // Allow plain HTTP if explicit; validate HTTPS. + Insecure // Allow plain HTTP if not explicitly HTTPS; skip HTTPS validation. +) + +// An HTTPError describes an HTTP error response (non-200 result). +type HTTPError struct { + URL string // redacted + Status string + StatusCode int + Err error // underlying error, if known + Detail string // limited to maxErrorDetailLines and maxErrorDetailBytes +} + +const ( + maxErrorDetailLines = 8 + maxErrorDetailBytes = maxErrorDetailLines * 81 +) + +func (e *HTTPError) Error() string { + if e.Detail != "" { + detailSep := " " + if strings.ContainsRune(e.Detail, '\n') { + detailSep = "\n\t" + } + return fmt.Sprintf("reading %s: %v\n\tserver response:%s%s", e.URL, e.Status, detailSep, e.Detail) + } + + if eErr := e.Err; eErr != nil { + if pErr, ok := e.Err.(*fs.PathError); ok { + if u, err := url.Parse(e.URL); err == nil { + if fp, err := urlToFilePath(u); err == nil && pErr.Path == fp { + // Remove the redundant copy of the path. + eErr = pErr.Err + } + } + } + return fmt.Sprintf("reading %s: %v", e.URL, eErr) + } + + return fmt.Sprintf("reading %s: %v", e.URL, e.Status) +} + +func (e *HTTPError) Is(target error) bool { + return target == fs.ErrNotExist && (e.StatusCode == 404 || e.StatusCode == 410) +} + +func (e *HTTPError) Unwrap() error { + return e.Err +} + +// GetBytes returns the body of the requested resource, or an error if the +// response status was not http.StatusOK. +// +// GetBytes is a convenience wrapper around Get and Response.Err. +func GetBytes(u *url.URL) ([]byte, error) { + resp, err := Get(DefaultSecurity, u) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if err := resp.Err(); err != nil { + return nil, err + } + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading %s: %v", u.Redacted(), err) + } + return b, nil +} + +type Response struct { + URL string // redacted + Status string + StatusCode int + Header map[string][]string + Body io.ReadCloser // Either the original body or &errorDetail. + + fileErr error + errorDetail errorDetailBuffer +} + +// Err returns an *HTTPError corresponding to the response r. +// If the response r has StatusCode 200 or 0 (unset), Err returns nil. +// Otherwise, Err may read from r.Body in order to extract relevant error detail. +func (r *Response) Err() error { + if r.StatusCode == 200 || r.StatusCode == 0 { + return nil + } + + return &HTTPError{ + URL: r.URL, + Status: r.Status, + StatusCode: r.StatusCode, + Err: r.fileErr, + Detail: r.formatErrorDetail(), + } +} + +// formatErrorDetail converts r.errorDetail (a prefix of the output of r.Body) +// into a short, tab-indented summary. +func (r *Response) formatErrorDetail() string { + if r.Body != &r.errorDetail { + return "" // Error detail collection not enabled. + } + + // Ensure that r.errorDetail has been populated. + _, _ = io.Copy(io.Discard, r.Body) + + s := r.errorDetail.buf.String() + if !utf8.ValidString(s) { + return "" // Don't try to recover non-UTF-8 error messages. + } + for _, r := range s { + if !unicode.IsGraphic(r) && !unicode.IsSpace(r) { + return "" // Don't let the server do any funny business with the user's terminal. + } + } + + var detail strings.Builder + for i, line := range strings.Split(s, "\n") { + if strings.TrimSpace(line) == "" { + break // Stop at the first blank line. + } + if i > 0 { + detail.WriteString("\n\t") + } + if i >= maxErrorDetailLines { + detail.WriteString("[Truncated: too many lines.]") + break + } + if detail.Len()+len(line) > maxErrorDetailBytes { + detail.WriteString("[Truncated: too long.]") + break + } + detail.WriteString(line) + } + + return detail.String() +} + +// Get returns the body of the HTTP or HTTPS resource specified at the given URL. +// +// If the URL does not include an explicit scheme, Get first tries "https". +// If the server does not respond under that scheme and the security mode is +// Insecure, Get then tries "http". +// The URL included in the response indicates which scheme was actually used, +// and it is a redacted URL suitable for use in error messages. +// +// For the "https" scheme only, credentials are attached using the +// cmd/go/internal/auth package. If the URL itself includes a username and +// password, it will not be attempted under the "http" scheme unless the +// security mode is Insecure. +// +// Get returns a non-nil error only if the request did not receive a response +// under any applicable scheme. (A non-2xx response does not cause an error.) +func Get(security SecurityMode, u *url.URL) (*Response, error) { + return get(security, u) +} + +// OpenBrowser attempts to open the requested URL in a web browser. +func OpenBrowser(url string) (opened bool) { + return openBrowser(url) +} + +// Join returns the result of adding the slash-separated +// path elements to the end of u's path. +func Join(u *url.URL, path string) *url.URL { + j := *u + if path == "" { + return &j + } + j.Path = strings.TrimSuffix(u.Path, "/") + "/" + strings.TrimPrefix(path, "/") + j.RawPath = strings.TrimSuffix(u.RawPath, "/") + "/" + strings.TrimPrefix(path, "/") + return &j +} + +// An errorDetailBuffer is an io.ReadCloser that copies up to +// maxErrorDetailLines into a buffer for later inspection. +type errorDetailBuffer struct { + r io.ReadCloser + buf strings.Builder + bufLines int +} + +func (b *errorDetailBuffer) Close() error { + return b.r.Close() +} + +func (b *errorDetailBuffer) Read(p []byte) (n int, err error) { + n, err = b.r.Read(p) + + // Copy the first maxErrorDetailLines+1 lines into b.buf, + // discarding any further lines. + // + // Note that the read may begin or end in the middle of a UTF-8 character, + // so don't try to do anything fancy with characters that encode to larger + // than one byte. + if b.bufLines <= maxErrorDetailLines { + for _, line := range bytes.SplitAfterN(p[:n], []byte("\n"), maxErrorDetailLines-b.bufLines) { + b.buf.Write(line) + if len(line) > 0 && line[len(line)-1] == '\n' { + b.bufLines++ + if b.bufLines > maxErrorDetailLines { + break + } + } + } + } + + return n, err +} + +// IsLocalHost reports whether the given URL refers to a local +// (loopback) host, such as "localhost" or "127.0.0.1:8080". +func IsLocalHost(u *url.URL) bool { + return isLocalHost(u) +} diff --git a/src/cmd/go/internal/web/bootstrap.go b/src/cmd/go/internal/web/bootstrap.go new file mode 100644 index 0000000..6312169 --- /dev/null +++ b/src/cmd/go/internal/web/bootstrap.go @@ -0,0 +1,25 @@ +// Copyright 2012 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. + +//go:build cmd_go_bootstrap + +// This code is compiled only into the bootstrap 'go' binary. +// These stubs avoid importing packages with large dependency +// trees that potentially require C linking, +// like the use of "net/http" in vcs.go. + +package web + +import ( + "errors" + urlpkg "net/url" +) + +func get(security SecurityMode, url *urlpkg.URL) (*Response, error) { + return nil, errors.New("no http in bootstrap go command") +} + +func openBrowser(url string) bool { return false } + +func isLocalHost(u *urlpkg.URL) bool { return false } diff --git a/src/cmd/go/internal/web/file_test.go b/src/cmd/go/internal/web/file_test.go new file mode 100644 index 0000000..3734df5 --- /dev/null +++ b/src/cmd/go/internal/web/file_test.go @@ -0,0 +1,60 @@ +// Copyright 2019 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 web + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "testing" +) + +func TestGetFileURL(t *testing.T) { + const content = "Hello, file!\n" + + f, err := os.CreateTemp("", "web-TestGetFileURL") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + + if _, err := f.WriteString(content); err != nil { + t.Error(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + + u, err := urlFromFilePath(f.Name()) + if err != nil { + t.Fatal(err) + } + + b, err := GetBytes(u) + if err != nil { + t.Fatalf("GetBytes(%v) = _, %v", u, err) + } + if string(b) != content { + t.Fatalf("after writing %q to %s, GetBytes(%v) read %q", content, f.Name(), u, b) + } +} + +func TestGetNonexistentFile(t *testing.T) { + path, err := filepath.Abs("nonexistent") + if err != nil { + t.Fatal(err) + } + + u, err := urlFromFilePath(path) + if err != nil { + t.Fatal(err) + } + + b, err := GetBytes(u) + if !errors.Is(err, fs.ErrNotExist) { + t.Fatalf("GetBytes(%v) = %q, %v; want _, fs.ErrNotExist", u, b, err) + } +} diff --git a/src/cmd/go/internal/web/http.go b/src/cmd/go/internal/web/http.go new file mode 100644 index 0000000..bd5f828 --- /dev/null +++ b/src/cmd/go/internal/web/http.go @@ -0,0 +1,399 @@ +// Copyright 2012 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. + +//go:build !cmd_go_bootstrap + +// This code is compiled into the real 'go' binary, but it is not +// compiled into the binary that is built during all.bash, so as +// to avoid needing to build net (and thus use cgo) during the +// bootstrap process. + +package web + +import ( + "crypto/tls" + "errors" + "fmt" + "io" + "mime" + "net" + "net/http" + urlpkg "net/url" + "os" + "strings" + "time" + + "cmd/go/internal/auth" + "cmd/go/internal/base" + "cmd/go/internal/cfg" + "cmd/internal/browser" +) + +// impatientInsecureHTTPClient is used with GOINSECURE, +// when we're connecting to https servers that might not be there +// or might be using self-signed certificates. +var impatientInsecureHTTPClient = &http.Client{ + CheckRedirect: checkRedirect, + Timeout: 5 * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, +} + +var securityPreservingDefaultClient = securityPreservingHTTPClient(http.DefaultClient) + +// securityPreservingHTTPClient returns a client that is like the original +// but rejects redirects to plain-HTTP URLs if the original URL was secure. +func securityPreservingHTTPClient(original *http.Client) *http.Client { + c := new(http.Client) + *c = *original + c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if len(via) > 0 && via[0].URL.Scheme == "https" && req.URL.Scheme != "https" { + lastHop := via[len(via)-1].URL + return fmt.Errorf("redirected from secure URL %s to insecure URL %s", lastHop, req.URL) + } + return checkRedirect(req, via) + } + return c +} + +func checkRedirect(req *http.Request, via []*http.Request) error { + // Go's http.DefaultClient allows 10 redirects before returning an error. + // Mimic that behavior here. + if len(via) >= 10 { + return errors.New("stopped after 10 redirects") + } + + interceptRequest(req) + return nil +} + +type Interceptor struct { + Scheme string + FromHost string + ToHost string + Client *http.Client +} + +func EnableTestHooks(interceptors []Interceptor) error { + if enableTestHooks { + return errors.New("web: test hooks already enabled") + } + + for _, t := range interceptors { + if t.FromHost == "" { + panic("EnableTestHooks: missing FromHost") + } + if t.ToHost == "" { + panic("EnableTestHooks: missing ToHost") + } + } + + testInterceptors = interceptors + enableTestHooks = true + return nil +} + +func DisableTestHooks() { + if !enableTestHooks { + panic("web: test hooks not enabled") + } + enableTestHooks = false + testInterceptors = nil +} + +var ( + enableTestHooks = false + testInterceptors []Interceptor +) + +func interceptURL(u *urlpkg.URL) (*Interceptor, bool) { + if !enableTestHooks { + return nil, false + } + for i, t := range testInterceptors { + if u.Host == t.FromHost && (u.Scheme == "" || u.Scheme == t.Scheme) { + return &testInterceptors[i], true + } + } + return nil, false +} + +func interceptRequest(req *http.Request) { + if t, ok := interceptURL(req.URL); ok { + req.Host = req.URL.Host + req.URL.Host = t.ToHost + } +} + +func get(security SecurityMode, url *urlpkg.URL) (*Response, error) { + start := time.Now() + + if url.Scheme == "file" { + return getFile(url) + } + + if enableTestHooks { + switch url.Host { + case "proxy.golang.org": + if os.Getenv("TESTGOPROXY404") == "1" { + res := &Response{ + URL: url.Redacted(), + Status: "404 testing", + StatusCode: 404, + Header: make(map[string][]string), + Body: http.NoBody, + } + if cfg.BuildX { + fmt.Fprintf(os.Stderr, "# get %s: %v (%.3fs)\n", url.Redacted(), res.Status, time.Since(start).Seconds()) + } + return res, nil + } + + case "localhost.localdev": + return nil, fmt.Errorf("no such host localhost.localdev") + + default: + if os.Getenv("TESTGONETWORK") == "panic" { + if _, ok := interceptURL(url); !ok { + host := url.Host + if h, _, err := net.SplitHostPort(url.Host); err == nil && h != "" { + host = h + } + addr := net.ParseIP(host) + if addr == nil || (!addr.IsLoopback() && !addr.IsUnspecified()) { + panic("use of network: " + url.String()) + } + } + } + } + } + + fetch := func(url *urlpkg.URL) (*http.Response, error) { + // Note: The -v build flag does not mean "print logging information", + // despite its historical misuse for this in GOPATH-based go get. + // We print extra logging in -x mode instead, which traces what + // commands are executed. + if cfg.BuildX { + fmt.Fprintf(os.Stderr, "# get %s\n", url.Redacted()) + } + + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + if url.Scheme == "https" { + auth.AddCredentials(req) + } + t, intercepted := interceptURL(req.URL) + if intercepted { + req.Host = req.URL.Host + req.URL.Host = t.ToHost + } + + release, err := base.AcquireNet() + if err != nil { + return nil, err + } + + var res *http.Response + if security == Insecure && url.Scheme == "https" { // fail earlier + res, err = impatientInsecureHTTPClient.Do(req) + } else { + if intercepted && t.Client != nil { + client := securityPreservingHTTPClient(t.Client) + res, err = client.Do(req) + } else { + res, err = securityPreservingDefaultClient.Do(req) + } + } + + if err != nil { + // Per the docs for [net/http.Client.Do], “On error, any Response can be + // ignored. A non-nil Response with a non-nil error only occurs when + // CheckRedirect fails, and even then the returned Response.Body is + // already closed.” + release() + return nil, err + } + + // “If the returned error is nil, the Response will contain a non-nil Body + // which the user is expected to close.” + body := res.Body + res.Body = hookCloser{ + ReadCloser: body, + afterClose: release, + } + return res, err + } + + var ( + fetched *urlpkg.URL + res *http.Response + err error + ) + if url.Scheme == "" || url.Scheme == "https" { + secure := new(urlpkg.URL) + *secure = *url + secure.Scheme = "https" + + res, err = fetch(secure) + if err == nil { + fetched = secure + } else { + if cfg.BuildX { + fmt.Fprintf(os.Stderr, "# get %s: %v\n", secure.Redacted(), err) + } + if security != Insecure || url.Scheme == "https" { + // HTTPS failed, and we can't fall back to plain HTTP. + // Report the error from the HTTPS attempt. + return nil, err + } + } + } + + if res == nil { + switch url.Scheme { + case "http": + if security == SecureOnly { + if cfg.BuildX { + fmt.Fprintf(os.Stderr, "# get %s: insecure\n", url.Redacted()) + } + return nil, fmt.Errorf("insecure URL: %s", url.Redacted()) + } + case "": + if security != Insecure { + panic("should have returned after HTTPS failure") + } + default: + if cfg.BuildX { + fmt.Fprintf(os.Stderr, "# get %s: unsupported\n", url.Redacted()) + } + return nil, fmt.Errorf("unsupported scheme: %s", url.Redacted()) + } + + insecure := new(urlpkg.URL) + *insecure = *url + insecure.Scheme = "http" + if insecure.User != nil && security != Insecure { + if cfg.BuildX { + fmt.Fprintf(os.Stderr, "# get %s: insecure credentials\n", insecure.Redacted()) + } + return nil, fmt.Errorf("refusing to pass credentials to insecure URL: %s", insecure.Redacted()) + } + + res, err = fetch(insecure) + if err == nil { + fetched = insecure + } else { + if cfg.BuildX { + fmt.Fprintf(os.Stderr, "# get %s: %v\n", insecure.Redacted(), err) + } + // HTTP failed, and we already tried HTTPS if applicable. + // Report the error from the HTTP attempt. + return nil, err + } + } + + // Note: accepting a non-200 OK here, so people can serve a + // meta import in their http 404 page. + if cfg.BuildX { + fmt.Fprintf(os.Stderr, "# get %s: %v (%.3fs)\n", fetched.Redacted(), res.Status, time.Since(start).Seconds()) + } + + r := &Response{ + URL: fetched.Redacted(), + Status: res.Status, + StatusCode: res.StatusCode, + Header: map[string][]string(res.Header), + Body: res.Body, + } + + if res.StatusCode != http.StatusOK { + contentType := res.Header.Get("Content-Type") + if mediaType, params, _ := mime.ParseMediaType(contentType); mediaType == "text/plain" { + switch charset := strings.ToLower(params["charset"]); charset { + case "us-ascii", "utf-8", "": + // Body claims to be plain text in UTF-8 or a subset thereof. + // Try to extract a useful error message from it. + r.errorDetail.r = res.Body + r.Body = &r.errorDetail + } + } + } + + return r, nil +} + +func getFile(u *urlpkg.URL) (*Response, error) { + path, err := urlToFilePath(u) + if err != nil { + return nil, err + } + f, err := os.Open(path) + + if os.IsNotExist(err) { + return &Response{ + URL: u.Redacted(), + Status: http.StatusText(http.StatusNotFound), + StatusCode: http.StatusNotFound, + Body: http.NoBody, + fileErr: err, + }, nil + } + + if os.IsPermission(err) { + return &Response{ + URL: u.Redacted(), + Status: http.StatusText(http.StatusForbidden), + StatusCode: http.StatusForbidden, + Body: http.NoBody, + fileErr: err, + }, nil + } + + if err != nil { + return nil, err + } + + return &Response{ + URL: u.Redacted(), + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: f, + }, nil +} + +func openBrowser(url string) bool { return browser.Open(url) } + +func isLocalHost(u *urlpkg.URL) bool { + // VCSTestRepoURL itself is secure, and it may redirect requests to other + // ports (such as a port serving the "svn" protocol) which should also be + // considered secure. + host, _, err := net.SplitHostPort(u.Host) + if err != nil { + host = u.Host + } + if host == "localhost" { + return true + } + if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() { + return true + } + return false +} + +type hookCloser struct { + io.ReadCloser + afterClose func() +} + +func (c hookCloser) Close() error { + err := c.ReadCloser.Close() + c.afterClose() + return err +} diff --git a/src/cmd/go/internal/web/url.go b/src/cmd/go/internal/web/url.go new file mode 100644 index 0000000..146c51f --- /dev/null +++ b/src/cmd/go/internal/web/url.go @@ -0,0 +1,95 @@ +// Copyright 2019 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 web + +import ( + "errors" + "net/url" + "path/filepath" + "strings" +) + +// TODO(golang.org/issue/32456): If accepted, move these functions into the +// net/url package. + +var errNotAbsolute = errors.New("path is not absolute") + +func urlToFilePath(u *url.URL) (string, error) { + if u.Scheme != "file" { + return "", errors.New("non-file URL") + } + + checkAbs := func(path string) (string, error) { + if !filepath.IsAbs(path) { + return "", errNotAbsolute + } + return path, nil + } + + if u.Path == "" { + if u.Host != "" || u.Opaque == "" { + return "", errors.New("file URL missing path") + } + return checkAbs(filepath.FromSlash(u.Opaque)) + } + + path, err := convertFileURLPath(u.Host, u.Path) + if err != nil { + return path, err + } + return checkAbs(path) +} + +func urlFromFilePath(path string) (*url.URL, error) { + if !filepath.IsAbs(path) { + return nil, errNotAbsolute + } + + // If path has a Windows volume name, convert the volume to a host and prefix + // per https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/. + if vol := filepath.VolumeName(path); vol != "" { + if strings.HasPrefix(vol, `\\`) { + path = filepath.ToSlash(path[2:]) + i := strings.IndexByte(path, '/') + + if i < 0 { + // A degenerate case. + // \\host.example.com (without a share name) + // becomes + // file://host.example.com/ + return &url.URL{ + Scheme: "file", + Host: path, + Path: "/", + }, nil + } + + // \\host.example.com\Share\path\to\file + // becomes + // file://host.example.com/Share/path/to/file + return &url.URL{ + Scheme: "file", + Host: path[:i], + Path: filepath.ToSlash(path[i:]), + }, nil + } + + // C:\path\to\file + // becomes + // file:///C:/path/to/file + return &url.URL{ + Scheme: "file", + Path: "/" + filepath.ToSlash(path), + }, nil + } + + // /path/to/file + // becomes + // file:///path/to/file + return &url.URL{ + Scheme: "file", + Path: filepath.ToSlash(path), + }, nil +} diff --git a/src/cmd/go/internal/web/url_other.go b/src/cmd/go/internal/web/url_other.go new file mode 100644 index 0000000..84bbd72 --- /dev/null +++ b/src/cmd/go/internal/web/url_other.go @@ -0,0 +1,21 @@ +// Copyright 2019 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. + +//go:build !windows + +package web + +import ( + "errors" + "path/filepath" +) + +func convertFileURLPath(host, path string) (string, error) { + switch host { + case "", "localhost": + default: + return "", errors.New("file URL specifies non-local host") + } + return filepath.FromSlash(path), nil +} diff --git a/src/cmd/go/internal/web/url_other_test.go b/src/cmd/go/internal/web/url_other_test.go new file mode 100644 index 0000000..5c197de --- /dev/null +++ b/src/cmd/go/internal/web/url_other_test.go @@ -0,0 +1,36 @@ +// Copyright 2019 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. + +//go:build !windows + +package web + +var urlTests = []struct { + url string + filePath string + canonicalURL string // If empty, assume equal to url. + wantErr string +}{ + // Examples from RFC 8089: + { + url: `file:///path/to/file`, + filePath: `/path/to/file`, + }, + { + url: `file:/path/to/file`, + filePath: `/path/to/file`, + canonicalURL: `file:///path/to/file`, + }, + { + url: `file://localhost/path/to/file`, + filePath: `/path/to/file`, + canonicalURL: `file:///path/to/file`, + }, + + // We reject non-local files. + { + url: `file://host.example.com/path/to/file`, + wantErr: "file URL specifies non-local host", + }, +} diff --git a/src/cmd/go/internal/web/url_test.go b/src/cmd/go/internal/web/url_test.go new file mode 100644 index 0000000..8f462f5 --- /dev/null +++ b/src/cmd/go/internal/web/url_test.go @@ -0,0 +1,77 @@ +// Copyright 2019 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 web + +import ( + "net/url" + "testing" +) + +func TestURLToFilePath(t *testing.T) { + for _, tc := range urlTests { + if tc.url == "" { + continue + } + tc := tc + + t.Run(tc.url, func(t *testing.T) { + u, err := url.Parse(tc.url) + if err != nil { + t.Fatalf("url.Parse(%q): %v", tc.url, err) + } + + path, err := urlToFilePath(u) + if err != nil { + if err.Error() == tc.wantErr { + return + } + if tc.wantErr == "" { + t.Fatalf("urlToFilePath(%v): %v; want <nil>", u, err) + } else { + t.Fatalf("urlToFilePath(%v): %v; want %s", u, err, tc.wantErr) + } + } + + if path != tc.filePath || tc.wantErr != "" { + t.Fatalf("urlToFilePath(%v) = %q, <nil>; want %q, %s", u, path, tc.filePath, tc.wantErr) + } + }) + } +} + +func TestURLFromFilePath(t *testing.T) { + for _, tc := range urlTests { + if tc.filePath == "" { + continue + } + tc := tc + + t.Run(tc.filePath, func(t *testing.T) { + u, err := urlFromFilePath(tc.filePath) + if err != nil { + if err.Error() == tc.wantErr { + return + } + if tc.wantErr == "" { + t.Fatalf("urlFromFilePath(%v): %v; want <nil>", tc.filePath, err) + } else { + t.Fatalf("urlFromFilePath(%v): %v; want %s", tc.filePath, err, tc.wantErr) + } + } + + if tc.wantErr != "" { + t.Fatalf("urlFromFilePath(%v) = <nil>; want error: %s", tc.filePath, tc.wantErr) + } + + wantURL := tc.url + if tc.canonicalURL != "" { + wantURL = tc.canonicalURL + } + if u.String() != wantURL { + t.Errorf("urlFromFilePath(%v) = %v; want %s", tc.filePath, u, wantURL) + } + }) + } +} diff --git a/src/cmd/go/internal/web/url_windows.go b/src/cmd/go/internal/web/url_windows.go new file mode 100644 index 0000000..2a65ec8 --- /dev/null +++ b/src/cmd/go/internal/web/url_windows.go @@ -0,0 +1,43 @@ +// Copyright 2019 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 web + +import ( + "errors" + "path/filepath" + "strings" +) + +func convertFileURLPath(host, path string) (string, error) { + if len(path) == 0 || path[0] != '/' { + return "", errNotAbsolute + } + + path = filepath.FromSlash(path) + + // We interpret Windows file URLs per the description in + // https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/. + + // The host part of a file URL (if any) is the UNC volume name, + // but RFC 8089 reserves the authority "localhost" for the local machine. + if host != "" && host != "localhost" { + // A common "legacy" format omits the leading slash before a drive letter, + // encoding the drive letter as the host instead of part of the path. + // (See https://blogs.msdn.microsoft.com/freeassociations/2005/05/19/the-bizarre-and-unhappy-story-of-file-urls/.) + // We do not support that format, but we should at least emit a more + // helpful error message for it. + if filepath.VolumeName(host) != "" { + return "", errors.New("file URL encodes volume in host field: too few slashes?") + } + return `\\` + host + path, nil + } + + // If host is empty, path must contain an initial slash followed by a + // drive letter and path. Remove the slash and verify that the path is valid. + if vol := filepath.VolumeName(path[1:]); vol == "" || strings.HasPrefix(vol, `\\`) { + return "", errors.New("file URL missing drive letter") + } + return path[1:], nil +} diff --git a/src/cmd/go/internal/web/url_windows_test.go b/src/cmd/go/internal/web/url_windows_test.go new file mode 100644 index 0000000..06386a0 --- /dev/null +++ b/src/cmd/go/internal/web/url_windows_test.go @@ -0,0 +1,94 @@ +// Copyright 2019 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 web + +var urlTests = []struct { + url string + filePath string + canonicalURL string // If empty, assume equal to url. + wantErr string +}{ + // Examples from https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/: + + { + url: `file://laptop/My%20Documents/FileSchemeURIs.doc`, + filePath: `\\laptop\My Documents\FileSchemeURIs.doc`, + }, + { + url: `file:///C:/Documents%20and%20Settings/davris/FileSchemeURIs.doc`, + filePath: `C:\Documents and Settings\davris\FileSchemeURIs.doc`, + }, + { + url: `file:///D:/Program%20Files/Viewer/startup.htm`, + filePath: `D:\Program Files\Viewer\startup.htm`, + }, + { + url: `file:///C:/Program%20Files/Music/Web%20Sys/main.html?REQUEST=RADIO`, + filePath: `C:\Program Files\Music\Web Sys\main.html`, + canonicalURL: `file:///C:/Program%20Files/Music/Web%20Sys/main.html`, + }, + { + url: `file://applib/products/a-b/abc_9/4148.920a/media/start.swf`, + filePath: `\\applib\products\a-b\abc_9\4148.920a\media\start.swf`, + }, + { + url: `file:////applib/products/a%2Db/abc%5F9/4148.920a/media/start.swf`, + wantErr: "file URL missing drive letter", + }, + { + url: `C:\Program Files\Music\Web Sys\main.html?REQUEST=RADIO`, + wantErr: "non-file URL", + }, + + // The example "file://D:\Program Files\Viewer\startup.htm" errors out in + // url.Parse, so we substitute a slash-based path for testing instead. + { + url: `file://D:/Program Files/Viewer/startup.htm`, + wantErr: "file URL encodes volume in host field: too few slashes?", + }, + + // The blog post discourages the use of non-ASCII characters because they + // depend on the user's current codepage. However, when we are working with Go + // strings we assume UTF-8 encoding, and our url package refuses to encode + // URLs to non-ASCII strings. + { + url: `file:///C:/exampleㄓ.txt`, + filePath: `C:\exampleㄓ.txt`, + canonicalURL: `file:///C:/example%E3%84%93.txt`, + }, + { + url: `file:///C:/example%E3%84%93.txt`, + filePath: `C:\exampleㄓ.txt`, + }, + + // Examples from RFC 8089: + + // We allow the drive-letter variation from section E.2, because it is + // simpler to support than not to. However, we do not generate the shorter + // form in the reverse direction. + { + url: `file:c:/path/to/file`, + filePath: `c:\path\to\file`, + canonicalURL: `file:///c:/path/to/file`, + }, + + // We encode the UNC share name as the authority following section E.3.1, + // because that is what the Microsoft blog post explicitly recommends. + { + url: `file://host.example.com/Share/path/to/file.txt`, + filePath: `\\host.example.com\Share\path\to\file.txt`, + }, + + // We decline the four- and five-slash variations from section E.3.2. + // The paths in these URLs would change meaning under path.Clean. + { + url: `file:////host.example.com/path/to/file`, + wantErr: "file URL missing drive letter", + }, + { + url: `file://///host.example.com/path/to/file`, + wantErr: "file URL missing drive letter", + }, +} |