summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/web
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/go/internal/web')
-rw-r--r--src/cmd/go/internal/web/api.go246
-rw-r--r--src/cmd/go/internal/web/bootstrap.go25
-rw-r--r--src/cmd/go/internal/web/file_test.go60
-rw-r--r--src/cmd/go/internal/web/http.go399
-rw-r--r--src/cmd/go/internal/web/url.go95
-rw-r--r--src/cmd/go/internal/web/url_other.go21
-rw-r--r--src/cmd/go/internal/web/url_other_test.go36
-rw-r--r--src/cmd/go/internal/web/url_test.go77
-rw-r--r--src/cmd/go/internal/web/url_windows.go43
-rw-r--r--src/cmd/go/internal/web/url_windows_test.go94
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",
+ },
+}