]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go/internal/web: support file:// URLs
authorBryan C. Mills <bcmills@google.com>
Thu, 6 Jun 2019 15:08:37 +0000 (11:08 -0400)
committerBryan C. Mills <bcmills@google.com>
Mon, 10 Jun 2019 13:43:51 +0000 (13:43 +0000)
Fixes #27698
Fixes #32227

Change-Id: I2416408b3de2f9f1ae1af2911cc327a65d2c0170
Reviewed-on: https://go-review.googlesource.com/c/go/+/181037
Reviewed-by: Jay Conrod <jayconrod@google.com>
12 files changed:
src/cmd/go/internal/get/vcs.go
src/cmd/go/internal/modfetch/proxy.go
src/cmd/go/internal/web/file_test.go [new file with mode: 0644]
src/cmd/go/internal/web/http.go
src/cmd/go/internal/web/url.go [new file with mode: 0644]
src/cmd/go/internal/web/url_other.go [new file with mode: 0644]
src/cmd/go/internal/web/url_other_test.go [new file with mode: 0644]
src/cmd/go/internal/web/url_test.go [new file with mode: 0644]
src/cmd/go/internal/web/url_windows.go [new file with mode: 0644]
src/cmd/go/internal/web/url_windows_test.go [new file with mode: 0644]
src/cmd/go/testdata/script/mod_file_proxy.txt
src/cmd/go/testdata/script/mod_sumdb_file_path.txt [new file with mode: 0644]

index c6516c8bb38426e2a6628dca252091111fbd61cb..29d58e635033a72b67774387062c45fc1076396e 100644 (file)
@@ -856,6 +856,9 @@ func validateRepoRoot(repoRoot string) error {
        if url.Scheme == "" {
                return errors.New("no scheme")
        }
+       if url.Scheme == "file" {
+               return errors.New("file scheme disallowed")
+       }
        return nil
 }
 
index c1bc2776b95d7251d207726447c21429fa176392..50e26625a1023ac49a2b8c9948e11208073d2829 100644 (file)
@@ -13,12 +13,9 @@ import (
        "net/url"
        "os"
        pathpkg "path"
-       "path/filepath"
-       "runtime"
        "strings"
        "sync"
        "time"
-       "unicode"
 
        "cmd/go/internal/base"
        "cmd/go/internal/cfg"
@@ -201,18 +198,6 @@ func (p *proxyRepo) getBytes(path string) ([]byte, error) {
 
 func (p *proxyRepo) getBody(path string) (io.ReadCloser, error) {
        fullPath := pathpkg.Join(p.url.Path, path)
-       if p.url.Scheme == "file" {
-               rawPath, err := url.PathUnescape(fullPath)
-               if err != nil {
-                       return nil, err
-               }
-               if runtime.GOOS == "windows" && len(rawPath) >= 4 && rawPath[0] == '/' && unicode.IsLetter(rune(rawPath[1])) && rawPath[2] == ':' {
-                       // On Windows, file URLs look like "file:///C:/foo/bar". url.Path will
-                       // start with a slash which must be removed. See golang.org/issue/6027.
-                       rawPath = rawPath[1:]
-               }
-               return os.Open(filepath.FromSlash(rawPath))
-       }
 
        target := *p.url
        target.Path = fullPath
diff --git a/src/cmd/go/internal/web/file_test.go b/src/cmd/go/internal/web/file_test.go
new file mode 100644 (file)
index 0000000..e31ad71
--- /dev/null
@@ -0,0 +1,58 @@
+// 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/ioutil"
+       "os"
+       "path/filepath"
+       "testing"
+)
+
+func TestGetFileURL(t *testing.T) {
+       const content = "Hello, file!\n"
+
+       f, err := ioutil.TempFile("", "web-TestGetFileURL")
+       if err != nil {
+               t.Fatal(err)
+       }
+       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, os.ErrNotExist) {
+               t.Fatalf("GetBytes(%v) = %q, %v; want _, os.ErrNotExist", u, b, err)
+       }
+}
index 4e2b1c33dc82c5236454e7ff2b1f65b042587675..b790fe9916e8c64772e73d5d62a5e06519fb09f1 100644 (file)
@@ -53,6 +53,11 @@ var securityPreservingHTTPClient = &http.Client{
 
 func get(security SecurityMode, url *urlpkg.URL) (*Response, error) {
        start := time.Now()
+
+       if url.Scheme == "file" {
+               return getFile(url)
+       }
+
        if os.Getenv("TESTGOPROXY404") == "1" && url.Host == "proxy.golang.org" {
                res := &Response{
                        URL:        Redacted(url),
@@ -172,4 +177,41 @@ func get(security SecurityMode, url *urlpkg.URL) (*Response, error) {
        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:        Redacted(u),
+                       Status:     http.StatusText(http.StatusNotFound),
+                       StatusCode: http.StatusNotFound,
+                       Body:       http.NoBody,
+               }, nil
+       }
+
+       if os.IsPermission(err) {
+               return &Response{
+                       URL:        Redacted(u),
+                       Status:     http.StatusText(http.StatusForbidden),
+                       StatusCode: http.StatusForbidden,
+                       Body:       http.NoBody,
+               }, nil
+       }
+
+       if err != nil {
+               return nil, err
+       }
+
+       return &Response{
+               URL:        Redacted(u),
+               Status:     http.StatusText(http.StatusOK),
+               StatusCode: http.StatusOK,
+               Body:       f,
+       }, nil
+}
+
 func openBrowser(url string) bool { return browser.Open(url) }
diff --git a/src/cmd/go/internal/web/url.go b/src/cmd/go/internal/web/url.go
new file mode 100644 (file)
index 0000000..146c51f
--- /dev/null
@@ -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 (file)
index 0000000..bd243e5
--- /dev/null
@@ -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.
+
+//+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 (file)
index 0000000..b4a74d9
--- /dev/null
@@ -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.
+
+//+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 (file)
index 0000000..8f462f5
--- /dev/null
@@ -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 (file)
index 0000000..2a65ec8
--- /dev/null
@@ -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 (file)
index 0000000..06386a0
--- /dev/null
@@ -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",
+       },
+}
index cf097f8c8071f9671089c60c6179736298816267..38d9fd25fbebeb8c4e0d62a77e2632e4161e46ec 100644 (file)
@@ -11,11 +11,12 @@ go list
 grep v1.5.1 $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/list
 
 # Use download cache as file:/// proxy.
-[windows] stop # TODO: file://$WORK puts backslashes in the URL
 env GOPATH=$WORK/gopath2
-env GOPROXY=file:///nonexist
+[windows] env GOPROXY=file:///C:/nonexist
+[!windows] env GOPROXY=file:///nonexist
 ! go list
-env GOPROXY=file://$WORK/gopath1/pkg/mod/cache/download
+[windows] env GOPROXY=file:///$WORK/gopath1/pkg/mod/cache/download
+[!windows] env GOPROXY=file://$WORK/gopath1/pkg/mod/cache/download
 go list
 grep v1.5.1 $GOPATH/pkg/mod/cache/download/rsc.io/quote/@v/list
 
diff --git a/src/cmd/go/testdata/script/mod_sumdb_file_path.txt b/src/cmd/go/testdata/script/mod_sumdb_file_path.txt
new file mode 100644 (file)
index 0000000..744632e
--- /dev/null
@@ -0,0 +1,41 @@
+[!net] skip
+
+env GO111MODULE=on
+env GOSUMDB=
+
+# With a file-based proxy with an empty checksum directory,
+# downloading a new module should fail, even if a subsequent
+# proxy contains a more complete mirror of the sum database.
+[windows] env GOPROXY=file:///$WORK/sumproxy,https://proxy.golang.org
+[!windows] env GOPROXY=file://$WORK/sumproxy,https://proxy.golang.org
+! go get -d golang.org/x/text@v0.3.2
+stderr '^verifying golang.org/x/text.*: Not Found'
+
+# If the proxy does not claim to support the database,
+# checksum verification should fall through to the next proxy,
+# and downloading should succeed.
+[windows] env GOPROXY=file:///$WORK/emptyproxy,https://proxy.golang.org
+[!windows] env GOPROXY=file://$WORK/emptyproxy,https://proxy.golang.org
+go get -d golang.org/x/text@v0.3.2
+
+# Once the checksum is present in the go.sum file,
+# an empty file-based sumdb can be used in conjunction with
+# a fallback module mirror.
+grep golang.org/x/text go.sum
+go clean -modcache
+[windows] env GOPROXY=file:///$WORK/sumproxy
+[!windows] env GOPROXY=file://$WORK/sumproxy
+! go get -d golang.org/x/text@v0.3.2
+[windows] env GOPROXY=file:///$WORK/sumproxy,https://proxy.golang.org
+[!windows] env GOPROXY=file://$WORK/sumproxy,https://proxy.golang.org
+go get -d golang.org/x/text@v0.3.2
+
+-- go.mod --
+module example.com
+go 1.13
+-- $WORK/emptyproxy/README.md --
+This proxy contains no modules.
+-- $WORK/sumproxy/README.md --
+This proxy contains no modules.
+-- $WORK/sumproxy/sumdb/sum.golang.org/supported --
+This proxy blocks checksum downloads from sum.golang.org.