--- /dev/null
+// 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 auth provides access to user-provided authentication credentials.
+package auth
+
+import "net/http"
+
+// AddCredentials fills in the user's credentials for req, if any.
+// The return value reports whether any matching credentials were found.
+func AddCredentials(req *http.Request) (added bool) {
+ // TODO(golang.org/issue/26232): Support arbitrary user-provided credentials.
+ netrcOnce.Do(readNetrc)
+ for _, l := range netrc {
+ if l.machine == req.URL.Host {
+ req.SetBasicAuth(l.login, l.password)
+ return true
+ }
+ }
+
+ return false
+}
--- /dev/null
+// 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 auth
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+)
+
+type netrcLine struct {
+ machine string
+ login string
+ password string
+}
+
+var (
+ netrcOnce sync.Once
+ netrc []netrcLine
+ netrcErr error
+)
+
+func parseNetrc(data string) []netrcLine {
+ // See https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html
+ // for documentation on the .netrc format.
+ var nrc []netrcLine
+ var l netrcLine
+ inMacro := false
+ for _, line := range strings.Split(data, "\n") {
+ if inMacro {
+ if line == "" {
+ inMacro = false
+ }
+ continue
+ }
+
+ f := strings.Fields(line)
+ i := 0
+ for ; i < len(f)-1; i += 2 {
+ // Reset at each "machine" token.
+ // “The auto-login process searches the .netrc file for a machine token
+ // that matches […]. Once a match is made, the subsequent .netrc tokens
+ // are processed, stopping when the end of file is reached or another
+ // machine or a default token is encountered.”
+ switch f[i] {
+ case "machine":
+ l = netrcLine{machine: f[i+1]}
+ case "default":
+ break
+ case "login":
+ l.login = f[i+1]
+ case "password":
+ l.password = f[i+1]
+ case "macdef":
+ // “A macro is defined with the specified name; its contents begin with
+ // the next .netrc line and continue until a null line (consecutive
+ // new-line characters) is encountered.”
+ inMacro = true
+ }
+ if l.machine != "" && l.login != "" && l.password != "" {
+ nrc = append(nrc, l)
+ l = netrcLine{}
+ }
+ }
+
+ if i < len(f) && f[i] == "default" {
+ // “There can be only one default token, and it must be after all machine tokens.”
+ break
+ }
+ }
+
+ return nrc
+}
+
+func netrcPath() (string, error) {
+ if env := os.Getenv("NETRC"); env != "" {
+ return env, nil
+ }
+ dir, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ base := ".netrc"
+ if runtime.GOOS == "windows" {
+ base = "_netrc"
+ }
+ return filepath.Join(dir, base), nil
+}
+
+func readNetrc() {
+ path, err := netrcPath()
+ if err != nil {
+ netrcErr = err
+ return
+ }
+
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ netrcErr = err
+ }
+ return
+ }
+
+ netrc = parseNetrc(string(data))
+}
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package web2
+package auth
import (
"reflect"
password too-late-in-file
`
-func TestReadNetrc(t *testing.T) {
+func TestParseNetrc(t *testing.T) {
lines := parseNetrc(testNetrc)
want := []netrcLine{
{"api.github.com", "user", "pwd"},
"fmt"
"io"
"io/ioutil"
+ urlpkg "net/url"
"os"
"os/exec"
"path/filepath"
fmt.Fprintln(&buf, "```")
body := buf.String()
- url := "https://github.com/golang/go/issues/new?body=" + web.QueryEscape(body)
+ url := "https://github.com/golang/go/issues/new?body=" + urlpkg.QueryEscape(body)
if !web.OpenBrowser(url) {
fmt.Print("Please file a new issue at golang.org/issue/new using this template:\n\n")
fmt.Print(body)
}
func inspectGoVersion(w io.Writer) {
- data, err := web.Get("https://golang.org/VERSION?m=text")
+ data, err := web.GetBytes(&urlpkg.URL{
+ Scheme: "https",
+ Host: "golang.org",
+ Path: "/VERSION",
+ RawQuery: "?m=text",
+ })
if err != nil {
if cfg.BuildV {
fmt.Printf("failed to read from golang.org/VERSION: %v\n", err)
blindRepo bool // set if the repo has unusual configuration
)
- security := web.Secure
+ security := web.SecureOnly
if Insecure {
security = web.Insecure
}
"internal/lazyregexp"
"internal/singleflight"
"log"
- "net/url"
+ urlpkg "net/url"
"os"
"os/exec"
"path/filepath"
}
func (v *vcsCmd) isSecure(repo string) bool {
- u, err := url.Parse(repo)
+ u, err := urlpkg.Parse(repo)
if err != nil {
// If repo is not a URL, it's not secure.
return false
}
out := strings.TrimSpace(string(outb))
- var repoURL *url.URL
+ var repoURL *urlpkg.URL
if m := scpSyntaxRe.FindStringSubmatch(out); m != nil {
// Match SCP-like syntax and convert it to a URL.
// Eg, "git@github.com:user/repo" becomes
// "ssh://git@github.com/user/repo".
- repoURL = &url.URL{
+ repoURL = &urlpkg.URL{
Scheme: "ssh",
- User: url.User(m[1]),
+ User: urlpkg.User(m[1]),
Host: m[2],
Path: m[3],
}
} else {
- repoURL, err = url.Parse(out)
+ repoURL, err = urlpkg.Parse(out)
if err != nil {
return "", err
}
match["repo"] = scheme + "://" + match["repo"]
} else {
for _, scheme := range vcs.scheme {
- if security == web.Secure && !vcs.isSecureScheme(scheme) {
+ if security == web.SecureOnly && !vcs.isSecureScheme(scheme) {
continue
}
if vcs.pingCmd != "" && vcs.ping(scheme, match["repo"]) == nil {
return nil, errUnknownSite
}
-// repoRootForImportDynamic finds a *RepoRoot for a custom domain that's not
-// statically known by repoRootForImportPathStatic.
+// urlForImportPath returns a partially-populated URL for the given Go import path.
//
-// This handles custom import paths like "name.tld/pkg/foo" or just "name.tld".
-func repoRootForImportDynamic(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) {
+// The URL leaves the Scheme field blank so that web.Get will try any scheme
+// allowed by the selected security mode.
+func urlForImportPath(importPath string) (*urlpkg.URL, error) {
slash := strings.Index(importPath, "/")
if slash < 0 {
slash = len(importPath)
}
- host := importPath[:slash]
+ host, path := importPath[:slash], importPath[slash:]
if !strings.Contains(host, ".") {
return nil, errors.New("import path does not begin with hostname")
}
- urlStr, body, err := web.GetMaybeInsecure(importPath, security)
+ if len(path) == 0 {
+ path = "/"
+ }
+ return &urlpkg.URL{Host: host, Path: path, RawQuery: "go-get=1"}, nil
+}
+
+// repoRootForImportDynamic finds a *RepoRoot for a custom domain that's not
+// statically known by repoRootForImportPathStatic.
+//
+// This handles custom import paths like "name.tld/pkg/foo" or just "name.tld".
+func repoRootForImportDynamic(importPath string, mod ModuleMode, security web.SecurityMode) (*RepoRoot, error) {
+ url, err := urlForImportPath(importPath)
+ if err != nil {
+ return nil, err
+ }
+ url, resp, err := web.Get(security, url)
if err != nil {
msg := "https fetch: %v"
if security == web.Insecure {
}
return nil, fmt.Errorf(msg, err)
}
+ body := resp.Body
defer body.Close()
imports, err := parseMetaGoImports(body, mod)
if err != nil {
mmi, err := matchGoImport(imports, importPath)
if err != nil {
if _, ok := err.(ImportMismatchError); !ok {
- return nil, fmt.Errorf("parse %s: %v", urlStr, err)
+ return nil, fmt.Errorf("parse %s: %v", url, err)
}
- return nil, fmt.Errorf("parse %s: no go-import meta tags (%s)", urlStr, err)
+ return nil, fmt.Errorf("parse %s: no go-import meta tags (%s)", url, err)
}
if cfg.BuildV {
- log.Printf("get %q: found meta tag %#v at %s", importPath, mmi, urlStr)
+ log.Printf("get %q: found meta tag %#v at %s", importPath, mmi, url)
}
// If the import was "uni.edu/bob/project", which said the
// prefix was "uni.edu" and the RepoRoot was "evilroot.com",
if cfg.BuildV {
log.Printf("get %q: verifying non-authoritative meta tag", importPath)
}
- urlStr0 := urlStr
+ url0 := *url
var imports []metaImport
- urlStr, imports, err = metaImportsForPrefix(mmi.Prefix, mod, security)
+ url, imports, err = metaImportsForPrefix(mmi.Prefix, mod, security)
if err != nil {
return nil, err
}
metaImport2, err := matchGoImport(imports, importPath)
if err != nil || mmi != metaImport2 {
- return nil, fmt.Errorf("%s and %s disagree about go-import for %s", urlStr0, urlStr, mmi.Prefix)
+ return nil, fmt.Errorf("%s and %s disagree about go-import for %s", &url0, url, mmi.Prefix)
}
}
if err := validateRepoRoot(mmi.RepoRoot); err != nil {
- return nil, fmt.Errorf("%s: invalid repo root %q: %v", urlStr, mmi.RepoRoot, err)
+ return nil, fmt.Errorf("%s: invalid repo root %q: %v", url, mmi.RepoRoot, err)
}
vcs := vcsByCmd(mmi.VCS)
if vcs == nil && mmi.VCS != "mod" {
- return nil, fmt.Errorf("%s: unknown vcs %q", urlStr, mmi.VCS)
+ return nil, fmt.Errorf("%s: unknown vcs %q", url, mmi.VCS)
}
rr := &RepoRoot{
// validateRepoRoot returns an error if repoRoot does not seem to be
// a valid URL with scheme.
func validateRepoRoot(repoRoot string) error {
- url, err := url.Parse(repoRoot)
+ url, err := urlpkg.Parse(repoRoot)
if err != nil {
return err
}
//
// The importPath is of the form "golang.org/x/tools".
// It is an error if no imports are found.
-// urlStr will still be valid if err != nil.
-// The returned urlStr will be of the form "https://golang.org/x/tools?go-get=1"
-func metaImportsForPrefix(importPrefix string, mod ModuleMode, security web.SecurityMode) (urlStr string, imports []metaImport, err error) {
+// url will still be valid if err != nil.
+// The returned url will be of the form "https://golang.org/x/tools?go-get=1"
+func metaImportsForPrefix(importPrefix string, mod ModuleMode, security web.SecurityMode) (*urlpkg.URL, []metaImport, error) {
setCache := func(res fetchResult) (fetchResult, error) {
fetchCacheMu.Lock()
defer fetchCacheMu.Unlock()
}
fetchCacheMu.Unlock()
- urlStr, body, err := web.GetMaybeInsecure(importPrefix, security)
+ url, err := urlForImportPath(importPrefix)
if err != nil {
- return setCache(fetchResult{urlStr: urlStr, err: fmt.Errorf("fetch %s: %v", urlStr, err)})
+ return setCache(fetchResult{err: err})
}
+ url, resp, err := web.Get(security, url)
+ if err != nil {
+ return setCache(fetchResult{url: url, err: fmt.Errorf("fetch %s: %v", url, err)})
+ }
+ body := resp.Body
+ defer body.Close()
imports, err := parseMetaGoImports(body, mod)
if err != nil {
- return setCache(fetchResult{urlStr: urlStr, err: fmt.Errorf("parsing %s: %v", urlStr, err)})
+ return setCache(fetchResult{url: url, err: fmt.Errorf("parsing %s: %v", url, err)})
}
if len(imports) == 0 {
- err = fmt.Errorf("fetch %s: no go-import meta tag", urlStr)
+ err = fmt.Errorf("fetch %s: no go-import meta tag", url)
}
- return setCache(fetchResult{urlStr: urlStr, imports: imports, err: err})
+ return setCache(fetchResult{url: url, imports: imports, err: err})
})
res := resi.(fetchResult)
- return res.urlStr, res.imports, res.err
+ return res.url, res.imports, res.err
}
type fetchResult struct {
- urlStr string // e.g. "https://foo.com/x/bar?go-get=1"
+ url *urlpkg.URL
imports []metaImport
err error
}
var resp struct {
SCM string `json:"scm"`
}
- url := expand(match, "https://api.bitbucket.org/2.0/repositories/{bitname}?fields=scm")
- data, err := web.Get(url)
+ url := &urlpkg.URL{
+ Scheme: "https",
+ Host: "api.bitbucket.org",
+ Path: expand(match, "/2.0/repositories/{bitname}"),
+ RawQuery: "fields=scm",
+ }
+ data, err := web.GetBytes(url)
if err != nil {
if httpErr, ok := err.(*web.HTTPError); ok && httpErr.StatusCode == 403 {
// this may be a private repository. If so, attempt to determine which
if match["project"] == "" || match["series"] == "" {
return nil
}
- _, err := web.Get(expand(match, "https://code.launchpad.net/{project}{series}/.bzr/branch-format"))
+ url := &urlpkg.URL{
+ Scheme: "https",
+ Host: "code.launchpad.net",
+ Path: expand(match, "/{project}{series}/.bzr/branch-format"),
+ }
+ _, err := web.GetBytes(url)
if err != nil {
match["root"] = expand(match, "launchpad.net/{project}")
match["repo"] = expand(match, "https://{root}")
}
for _, test := range tests {
- got, err := RepoRootForImportPath(test.path, IgnoreMod, web.Secure)
+ got, err := RepoRootForImportPath(test.path, IgnoreMod, web.SecureOnly)
want := test.want
if want == nil {
+++ /dev/null
-// Copyright 2018 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 cmd_go_bootstrap
-
-package modfetch
-
-import (
- "fmt"
- "io"
-)
-
-func webGetGoGet(url string, body *io.ReadCloser) error {
- return fmt.Errorf("no network in go_bootstrap")
-}
-
-func webGetBytes(url string, body *[]byte) error {
- return fmt.Errorf("no network in go_bootstrap")
-}
-
-func webGetBody(url string, body *io.ReadCloser) error {
- return fmt.Errorf("no network in go_bootstrap")
-}
"encoding/json"
"fmt"
"io"
- "net/url"
+ "io/ioutil"
+ urlpkg "net/url"
+ "os"
+ pathpkg "path"
+ "path/filepath"
"strings"
"time"
"cmd/go/internal/modfetch/codehost"
"cmd/go/internal/module"
"cmd/go/internal/semver"
+ "cmd/go/internal/web"
)
var HelpGoproxy = &base.Command{
if strings.Contains(proxyURL, ",") {
return nil, fmt.Errorf("invalid $GOPROXY setting: cannot have comma")
}
- u, err := url.Parse(proxyURL)
- if err != nil || u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "file" {
- // Don't echo $GOPROXY back in case it has user:password in it (sigh).
- return nil, fmt.Errorf("invalid $GOPROXY setting: malformed URL or invalid scheme (must be http, https, file)")
+ r, err := newProxyRepo(proxyURL, path)
+ if err != nil {
+ return nil, err
}
- return newProxyRepo(u.String(), path)
+ return r, nil
}
type proxyRepo struct {
- url string
+ url *urlpkg.URL
path string
}
func newProxyRepo(baseURL, path string) (Repo, error) {
+ url, err := urlpkg.Parse(baseURL)
+ if err != nil {
+ return nil, err
+ }
+ switch url.Scheme {
+ case "file":
+ if *url != (urlpkg.URL{Scheme: url.Scheme, Path: url.Path, RawPath: url.RawPath}) {
+ return nil, fmt.Errorf("proxy URL %q uses file scheme with non-path elements", web.PasswordRedacted(url))
+ }
+ case "http", "https":
+ case "":
+ return nil, fmt.Errorf("proxy URL %q missing scheme", web.PasswordRedacted(url))
+ default:
+ return nil, fmt.Errorf("unsupported proxy scheme %q", url.Scheme)
+ }
+
enc, err := module.EncodePath(path)
if err != nil {
return nil, err
}
- return &proxyRepo{strings.TrimSuffix(baseURL, "/") + "/" + pathEscape(enc), path}, nil
+
+ url.Path = strings.TrimSuffix(url.Path, "/") + "/" + enc
+ url.RawPath = strings.TrimSuffix(url.RawPath, "/") + "/" + pathEscape(enc)
+ return &proxyRepo{url, path}, nil
}
func (p *proxyRepo) ModulePath() string {
return p.path
}
+func (p *proxyRepo) getBytes(path string) ([]byte, error) {
+ body, err := p.getBody(path)
+ if err != nil {
+ return nil, err
+ }
+ defer body.Close()
+ return ioutil.ReadAll(body)
+}
+
+func (p *proxyRepo) getBody(path string) (io.ReadCloser, error) {
+ fullPath := pathpkg.Join(p.url.Path, path)
+ if p.url.Scheme == "file" {
+ rawPath, err := urlpkg.PathUnescape(fullPath)
+ if err != nil {
+ return nil, err
+ }
+ return os.Open(filepath.FromSlash(rawPath))
+ }
+
+ url := new(urlpkg.URL)
+ *url = *p.url
+ url.Path = fullPath
+ url.RawPath = pathpkg.Join(url.RawPath, pathEscape(path))
+
+ _, resp, err := web.Get(web.DefaultSecurity, url)
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("unexpected status (%s): %v", web.PasswordRedacted(url), resp.Status)
+ }
+ return resp.Body, nil
+}
+
func (p *proxyRepo) Versions(prefix string) ([]string, error) {
- var data []byte
- err := webGetBytes(p.url+"/@v/list", &data)
+ data, err := p.getBytes("@v/list")
if err != nil {
return nil, err
}
}
func (p *proxyRepo) latest() (*RevInfo, error) {
- var data []byte
- err := webGetBytes(p.url+"/@v/list", &data)
+ data, err := p.getBytes("@v/list")
if err != nil {
return nil, err
}
}
func (p *proxyRepo) Stat(rev string) (*RevInfo, error) {
- var data []byte
encRev, err := module.EncodeVersion(rev)
if err != nil {
return nil, err
}
- err = webGetBytes(p.url+"/@v/"+pathEscape(encRev)+".info", &data)
+ data, err := p.getBytes("@v/" + encRev + ".info")
if err != nil {
return nil, err
}
}
func (p *proxyRepo) Latest() (*RevInfo, error) {
- var data []byte
- u := p.url + "/@latest"
- err := webGetBytes(u, &data)
+ data, err := p.getBytes("@latest")
if err != nil {
// TODO return err if not 404
return p.latest()
}
func (p *proxyRepo) GoMod(version string) ([]byte, error) {
- var data []byte
encVer, err := module.EncodeVersion(version)
if err != nil {
return nil, err
}
- err = webGetBytes(p.url+"/@v/"+pathEscape(encVer)+".mod", &data)
+ data, err := p.getBytes("@v/" + encVer + ".mod")
if err != nil {
return nil, err
}
}
func (p *proxyRepo) Zip(dst io.Writer, version string) error {
- var body io.ReadCloser
encVer, err := module.EncodeVersion(version)
if err != nil {
return err
}
- err = webGetBody(p.url+"/@v/"+pathEscape(encVer)+".zip", &body)
+ body, err := p.getBody("@v/" + encVer + ".zip")
if err != nil {
return err
}
// That is, it escapes things like ? and # (which really shouldn't appear anyway).
// It does not escape / to %2F: our REST API is designed so that / can be left as is.
func pathEscape(s string) string {
- return strings.ReplaceAll(url.PathEscape(s), "%2F", "/")
+ return strings.ReplaceAll(urlpkg.PathEscape(s), "%2F", "/")
}
return lookupProxy(path)
}
- security := web.Secure
+ security := web.SecureOnly
if get.Insecure {
security = web.Insecure
}
// Note: Because we are converting a code reference from a legacy
// version control system, we ignore meta tags about modules
// and use only direct source control entries (get.IgnoreMod).
- security := web.Secure
+ security := web.SecureOnly
if get.Insecure {
security = web.Insecure
}
+++ /dev/null
-// Copyright 2018 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 !cmd_go_bootstrap
-
-package modfetch
-
-import (
- "io"
-
- web "cmd/go/internal/web2"
-)
-
-// webGetGoGet fetches a go-get=1 URL and returns the body in *body.
-// It allows non-200 responses, as usual for these URLs.
-func webGetGoGet(url string, body *io.ReadCloser) error {
- return web.Get(url, web.Non200OK(), web.Body(body))
-}
-
-// webGetBytes returns the body returned by an HTTP GET, as a []byte.
-// It insists on a 200 response.
-func webGetBytes(url string, body *[]byte) error {
- return web.Get(url, web.ReadAllBody(body))
-}
-
-// webGetBody returns the body returned by an HTTP GET, as a io.ReadCloser.
-// It insists on a 200 response.
-func webGetBody(url string, body *io.ReadCloser) error {
- return web.Get(url, web.Body(body))
-}
--- /dev/null
+// 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 dependenicies 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 (
+ "fmt"
+ "io"
+ "io/ioutil"
+ urlpkg "net/url"
+)
+
+// 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.
+)
+
+type HTTPError struct {
+ status string
+ StatusCode int
+ url *urlpkg.URL
+}
+
+func (e *HTTPError) Error() string {
+ return fmt.Sprintf("%s: %s", e.url, e.status)
+}
+
+// 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.
+func GetBytes(url *urlpkg.URL) ([]byte, error) {
+ url, resp, err := Get(DefaultSecurity, url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 200 {
+ err := &HTTPError{status: resp.Status, StatusCode: resp.StatusCode, url: url}
+ return nil, err
+ }
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %v", url, err)
+ }
+ return b, nil
+}
+
+type Response struct {
+ Status string
+ StatusCode int
+ Header map[string][]string
+ Body io.ReadCloser
+}
+
+// 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 returned URL indicates which scheme was actually used.
+//
+// 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, url *urlpkg.URL) (*urlpkg.URL, *Response, error) {
+ return get(security, url)
+}
+
+// PasswordRedacted returns url directly if it does not encode a password,
+// or else a copy of url with the password redacted.
+func PasswordRedacted(url *urlpkg.URL) *urlpkg.URL {
+ if url.User != nil {
+ if _, ok := url.User.Password(); ok {
+ redacted := *url
+ redacted.User = urlpkg.UserPassword(url.User.Username(), "[redacted]")
+ return &redacted
+ }
+ }
+ return url
+}
+
+// OpenBrowser attempts to open the requested URL in a web browser.
+func OpenBrowser(url string) (opened bool) {
+ return openBrowser(url)
+}
// This code is compiled only into the bootstrap 'go' binary.
// These stubs avoid importing packages with large dependency
-// trees, like the use of "net/http" in vcs.go.
+// trees that potentially require C linking,
+// like the use of "net/http" in vcs.go.
package web
import (
"errors"
- "io"
+ urlpkg "net/url"
)
-var errHTTP = errors.New("no http in bootstrap go command")
-
-type HTTPError struct {
- StatusCode int
-}
-
-func (e *HTTPError) Error() string {
- panic("unreachable")
-}
-
-func Get(url string) ([]byte, error) {
- return nil, errHTTP
-}
-
-func GetMaybeInsecure(importPath string, security SecurityMode) (string, io.ReadCloser, error) {
- return "", nil, errHTTP
+func get(security SecurityMode, url *urlpkg.URL) (*urlpkg.URL, *Response, error) {
+ return nil, nil, errors.New("no http in bootstrap go command")
}
-func QueryEscape(s string) string { panic("unreachable") }
-func OpenBrowser(url string) bool { panic("unreachable") }
+func openBrowser(url string) bool { return false }
import (
"crypto/tls"
"fmt"
- "io"
- "io/ioutil"
"log"
"net/http"
- "net/url"
+ urlpkg "net/url"
"time"
+ "cmd/go/internal/auth"
"cmd/go/internal/cfg"
"cmd/internal/browser"
)
},
}
-type HTTPError struct {
- status string
- StatusCode int
- url string
-}
-
-func (e *HTTPError) Error() string {
- return fmt.Sprintf("%s: %s", e.url, e.status)
-}
-
-// Get returns the data from an HTTP GET request for the given URL.
-func Get(url string) ([]byte, error) {
- resp, err := securityPreservingHTTPClient.Get(url)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if resp.StatusCode != 200 {
- err := &HTTPError{status: resp.Status, StatusCode: resp.StatusCode, url: url}
-
- return nil, err
- }
- b, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("%s: %v", url, err)
- }
- return b, nil
-}
+func get(security SecurityMode, url *urlpkg.URL) (*urlpkg.URL, *Response, error) {
+ fetch := func(url *urlpkg.URL) (*urlpkg.URL, *http.Response, error) {
+ if cfg.BuildV {
+ log.Printf("Fetching %s", url)
+ }
-// GetMaybeInsecure returns the body of either the importPath's
-// https resource or, if unavailable and permitted by the security mode, the http resource.
-func GetMaybeInsecure(importPath string, security SecurityMode) (urlStr string, body io.ReadCloser, err error) {
- fetch := func(scheme string) (urlStr string, res *http.Response, err error) {
- u, err := url.Parse(scheme + "://" + importPath)
+ req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
- return "", nil, err
+ return nil, nil, err
}
- u.RawQuery = "go-get=1"
- urlStr = u.String()
- if cfg.BuildV {
- log.Printf("Fetching %s", urlStr)
+ if url.Scheme == "https" {
+ auth.AddCredentials(req)
}
- if security == Insecure && scheme == "https" { // fail earlier
- res, err = impatientInsecureHTTPClient.Get(urlStr)
+
+ var res *http.Response
+ if security == Insecure && url.Scheme == "https" { // fail earlier
+ res, err = impatientInsecureHTTPClient.Do(req)
} else {
- res, err = securityPreservingHTTPClient.Get(urlStr)
+ res, err = securityPreservingHTTPClient.Do(req)
}
- return
+ return url, res, err
}
- closeBody := func(res *http.Response) {
- if res != nil {
- res.Body.Close()
+
+ var (
+ fetched *urlpkg.URL
+ res *http.Response
+ err error
+ )
+ if url.Scheme == "" || url.Scheme == "https" {
+ secure := new(urlpkg.URL)
+ *secure = *url
+ secure.Scheme = "https"
+
+ fetched, res, err = fetch(secure)
+ if err != nil {
+ if cfg.BuildV {
+ log.Printf("https fetch failed: %v", 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, nil, err
+ }
}
}
- urlStr, res, err := fetch("https")
- if err != nil {
- if cfg.BuildV {
- log.Printf("https fetch failed: %v", err)
+
+ if res == nil {
+ switch url.Scheme {
+ case "http":
+ if security == SecureOnly {
+ return nil, nil, fmt.Errorf("URL %q is not secure", PasswordRedacted(url))
+ }
+ case "":
+ if security != Insecure {
+ panic("should have returned after HTTPS failure")
+ }
+ default:
+ return nil, nil, fmt.Errorf("unsupported scheme %s", url.Scheme)
}
- if security == Insecure {
- closeBody(res)
- urlStr, res, err = fetch("http")
+
+ insecure := new(urlpkg.URL)
+ *insecure = *url
+ insecure.Scheme = "http"
+ if insecure.User != nil && security != Insecure {
+ return nil, nil, fmt.Errorf("refusing to pass credentials to insecure URL %q", PasswordRedacted(insecure))
+ }
+
+ fetched, res, err = fetch(insecure)
+ if err != nil {
+ // HTTP failed, and we already tried HTTPS if applicable.
+ // Report the error from the HTTP attempt.
+ return nil, nil, err
}
}
- if err != nil {
- closeBody(res)
- return "", nil, err
- }
+
// Note: accepting a non-200 OK here, so people can serve a
// meta import in their http 404 page.
if cfg.BuildV {
- log.Printf("Parsing meta tags from %s (status code %d)", urlStr, res.StatusCode)
+ log.Printf("Parsing meta tags from %s (status code %d)", PasswordRedacted(fetched), res.StatusCode)
}
- return urlStr, res.Body, nil
+ return fetched, &Response{
+ Status: res.Status,
+ StatusCode: res.StatusCode,
+ Header: map[string][]string(res.Header),
+ Body: res.Body,
+ }, nil
}
-func QueryEscape(s string) string { return url.QueryEscape(s) }
-func OpenBrowser(url string) bool { return browser.Open(url) }
+func openBrowser(url string) bool { return browser.Open(url) }
+++ /dev/null
-// 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 helper routines for accessing HTTP/HTTPS resources.
-package web
-
-// 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 (
- Secure SecurityMode = iota
- Insecure
-)
+++ /dev/null
-// Copyright 2018 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 web2
-
-import (
- "bytes"
- "cmd/go/internal/base"
- "cmd/go/internal/cfg"
- "encoding/json"
- "flag"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "net/http"
- "os"
- "path/filepath"
- "runtime"
- "runtime/debug"
- "strings"
- "sync"
-)
-
-var TraceGET = false
-var webstack = false
-
-func init() {
- flag.BoolVar(&TraceGET, "webtrace", TraceGET, "trace GET requests")
- flag.BoolVar(&webstack, "webstack", webstack, "print stack for GET requests")
-}
-
-type netrcLine struct {
- machine string
- login string
- password string
-}
-
-var (
- netrcOnce sync.Once
- netrc []netrcLine
- netrcErr error
-)
-
-func parseNetrc(data string) []netrcLine {
- // See https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html
- // for documentation on the .netrc format.
- var nrc []netrcLine
- var l netrcLine
- inMacro := false
- for _, line := range strings.Split(data, "\n") {
- if inMacro {
- if line == "" {
- inMacro = false
- }
- continue
- }
-
- f := strings.Fields(line)
- i := 0
- for ; i < len(f)-1; i += 2 {
- // Reset at each "machine" token.
- // “The auto-login process searches the .netrc file for a machine token
- // that matches […]. Once a match is made, the subsequent .netrc tokens
- // are processed, stopping when the end of file is reached or another
- // machine or a default token is encountered.”
- switch f[i] {
- case "machine":
- l = netrcLine{machine: f[i+1]}
- case "default":
- break
- case "login":
- l.login = f[i+1]
- case "password":
- l.password = f[i+1]
- case "macdef":
- // “A macro is defined with the specified name; its contents begin with
- // the next .netrc line and continue until a null line (consecutive
- // new-line characters) is encountered.”
- inMacro = true
- }
- if l.machine != "" && l.login != "" && l.password != "" {
- nrc = append(nrc, l)
- l = netrcLine{}
- }
- }
-
- if i < len(f) && f[i] == "default" {
- // “There can be only one default token, and it must be after all machine tokens.”
- break
- }
- }
-
- return nrc
-}
-
-func havePassword(machine string) bool {
- netrcOnce.Do(readNetrc)
- for _, line := range netrc {
- if line.machine == machine {
- return true
- }
- }
- return false
-}
-
-func netrcPath() (string, error) {
- if env := os.Getenv("NETRC"); env != "" {
- return env, nil
- }
- dir, err := os.UserHomeDir()
- if err != nil {
- return "", err
- }
- base := ".netrc"
- if runtime.GOOS == "windows" {
- base = "_netrc"
- }
- return filepath.Join(dir, base), nil
-}
-
-func readNetrc() {
- path, err := netrcPath()
- if err != nil {
- netrcErr = err
- return
- }
-
- data, err := ioutil.ReadFile(path)
- if err != nil {
- if !os.IsNotExist(err) {
- netrcErr = err
- }
- return
- }
-
- netrc = parseNetrc(string(data))
-}
-
-type getState struct {
- req *http.Request
- resp *http.Response
- body io.ReadCloser
- non200ok bool
-}
-
-type Option interface {
- option(*getState) error
-}
-
-func Non200OK() Option {
- return optionFunc(func(g *getState) error {
- g.non200ok = true
- return nil
- })
-}
-
-type optionFunc func(*getState) error
-
-func (f optionFunc) option(g *getState) error {
- return f(g)
-}
-
-func DecodeJSON(dst interface{}) Option {
- return optionFunc(func(g *getState) error {
- if g.resp != nil {
- return json.NewDecoder(g.body).Decode(dst)
- }
- return nil
- })
-}
-
-func ReadAllBody(body *[]byte) Option {
- return optionFunc(func(g *getState) error {
- if g.resp != nil {
- var err error
- *body, err = ioutil.ReadAll(g.body)
- return err
- }
- return nil
- })
-}
-
-func Body(body *io.ReadCloser) Option {
- return optionFunc(func(g *getState) error {
- if g.resp != nil {
- *body = g.body
- g.body = nil
- }
- return nil
- })
-}
-
-func Header(hdr *http.Header) Option {
- return optionFunc(func(g *getState) error {
- if g.resp != nil {
- *hdr = CopyHeader(g.resp.Header)
- }
- return nil
- })
-}
-
-func CopyHeader(hdr http.Header) http.Header {
- if hdr == nil {
- return nil
- }
- h2 := make(http.Header)
- for k, v := range hdr {
- v2 := make([]string, len(v))
- copy(v2, v)
- h2[k] = v2
- }
- return h2
-}
-
-var cache struct {
- mu sync.Mutex
- byURL map[string]*cacheEntry
-}
-
-type cacheEntry struct {
- mu sync.Mutex
- resp *http.Response
- body []byte
-}
-
-var httpDo = http.DefaultClient.Do
-
-func SetHTTPDoForTesting(do func(*http.Request) (*http.Response, error)) {
- if do == nil {
- do = http.DefaultClient.Do
- }
- httpDo = do
-}
-
-func Get(url string, options ...Option) error {
- if TraceGET || webstack || cfg.BuildV {
- log.Printf("Fetching %s", url)
- if webstack {
- log.Println(string(debug.Stack()))
- }
- }
-
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- return err
- }
-
- netrcOnce.Do(readNetrc)
- for _, l := range netrc {
- if l.machine == req.URL.Host {
- req.SetBasicAuth(l.login, l.password)
- break
- }
- }
-
- g := &getState{req: req}
- for _, o := range options {
- if err := o.option(g); err != nil {
- return err
- }
- }
-
- cache.mu.Lock()
- e := cache.byURL[url]
- if e == nil {
- e = new(cacheEntry)
- if !strings.HasPrefix(url, "file:") {
- if cache.byURL == nil {
- cache.byURL = make(map[string]*cacheEntry)
- }
- cache.byURL[url] = e
- }
- }
- cache.mu.Unlock()
-
- e.mu.Lock()
- if strings.HasPrefix(url, "file:") {
- body, err := ioutil.ReadFile(req.URL.Path)
- if err != nil {
- e.mu.Unlock()
- return err
- }
- e.body = body
- e.resp = &http.Response{
- StatusCode: 200,
- }
- } else if e.resp == nil {
- resp, err := httpDo(req)
- if err != nil {
- e.mu.Unlock()
- return err
- }
- e.resp = resp
- // TODO: Spool to temp file.
- body, err := ioutil.ReadAll(resp.Body)
- resp.Body.Close()
- resp.Body = nil
- if err != nil {
- e.mu.Unlock()
- return err
- }
- e.body = body
- }
- g.resp = e.resp
- g.body = ioutil.NopCloser(bytes.NewReader(e.body))
- e.mu.Unlock()
-
- defer func() {
- if g.body != nil {
- g.body.Close()
- }
- }()
-
- if g.resp.StatusCode == 403 && req.URL.Host == "api.github.com" && !havePassword("api.github.com") {
- base.Errorf("%s", githubMessage)
- }
- if !g.non200ok && g.resp.StatusCode != 200 {
- return fmt.Errorf("unexpected status (%s): %v", url, g.resp.Status)
- }
-
- for _, o := range options {
- if err := o.option(g); err != nil {
- return err
- }
- }
- return err
-}
-
-var githubMessage = `go: 403 response from api.github.com
-
-GitHub applies fairly small rate limits to unauthenticated users, and
-you appear to be hitting them. To authenticate, please visit
-https://github.com/settings/tokens and click "Generate New Token" to
-create a Personal Access Token. The token only needs "public_repo"
-scope, but you can add "repo" if you want to access private
-repositories too.
-
-Add the token to your $HOME/.netrc (%USERPROFILE%\_netrc on Windows):
-
- machine api.github.com login YOU password TOKEN
-
-Sorry for the interruption.
-`
+++ /dev/null
-// Copyright 2018 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 webtest
-
-import (
- "bufio"
- "bytes"
- "encoding/hex"
- "flag"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "net/http"
- "os"
- "sort"
- "strconv"
- "strings"
- "sync"
- "unicode/utf8"
-
- web "cmd/go/internal/web2"
-)
-
-var mode = flag.String("webtest", "replay", "set webtest `mode` - record, replay, bypass")
-
-func Hook() {
- if *mode == "bypass" {
- return
- }
- web.SetHTTPDoForTesting(Do)
-}
-
-func Unhook() {
- web.SetHTTPDoForTesting(nil)
-}
-
-func Print() {
- web.SetHTTPDoForTesting(DoPrint)
-}
-
-var responses struct {
- mu sync.Mutex
- byURL map[string]*respEntry
-}
-
-type respEntry struct {
- status string
- code int
- hdr http.Header
- body []byte
-}
-
-func Serve(url string, status string, hdr http.Header, body []byte) {
- if status == "" {
- status = "200 OK"
- }
- code, err := strconv.Atoi(strings.Fields(status)[0])
- if err != nil {
- panic("bad Serve status - " + status + " - " + err.Error())
- }
-
- responses.mu.Lock()
- defer responses.mu.Unlock()
-
- if responses.byURL == nil {
- responses.byURL = make(map[string]*respEntry)
- }
- responses.byURL[url] = &respEntry{status: status, code: code, hdr: web.CopyHeader(hdr), body: body}
-}
-
-func Do(req *http.Request) (*http.Response, error) {
- if req.Method != "GET" {
- return nil, fmt.Errorf("bad method - must be GET")
- }
-
- responses.mu.Lock()
- e := responses.byURL[req.URL.String()]
- responses.mu.Unlock()
-
- if e == nil {
- if *mode == "record" {
- loaded.mu.Lock()
- if len(loaded.did) != 1 {
- loaded.mu.Unlock()
- return nil, fmt.Errorf("cannot use -webtest=record with multiple loaded response files")
- }
- var file string
- for file = range loaded.did {
- break
- }
- loaded.mu.Unlock()
- return doSave(file, req)
- }
- e = &respEntry{code: 599, status: "599 unexpected request (no canned response)"}
- }
- resp := &http.Response{
- Status: e.status,
- StatusCode: e.code,
- Header: web.CopyHeader(e.hdr),
- Body: ioutil.NopCloser(bytes.NewReader(e.body)),
- }
- return resp, nil
-}
-
-func DoPrint(req *http.Request) (*http.Response, error) {
- return doSave("", req)
-}
-
-func doSave(file string, req *http.Request) (*http.Response, error) {
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return nil, err
- }
- data, err := ioutil.ReadAll(resp.Body)
- resp.Body.Close()
- if err != nil {
- return nil, err
- }
- resp.Body = ioutil.NopCloser(bytes.NewReader(data))
-
- var f *os.File
- if file == "" {
- f = os.Stderr
- } else {
- f, err = os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
- if err != nil {
- log.Fatal(err)
- }
- defer f.Close()
- }
-
- fmt.Fprintf(f, "GET %s\n", req.URL.String())
- fmt.Fprintf(f, "%s\n", resp.Status)
- var keys []string
- for k := range resp.Header {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- for _, k := range keys {
- if k == "Set-Cookie" {
- continue
- }
- for _, v := range resp.Header[k] {
- fmt.Fprintf(f, "%s: %s\n", k, v)
- }
- }
- fmt.Fprintf(f, "\n")
- if utf8.Valid(data) && !bytes.Contains(data, []byte("\nGET")) && !isHexDump(data) {
- fmt.Fprintf(f, "%s\n\n", data)
- } else {
- fmt.Fprintf(f, "%s\n", hex.Dump(data))
- }
- return resp, err
-}
-
-var loaded struct {
- mu sync.Mutex
- did map[string]bool
-}
-
-func LoadOnce(file string) {
- loaded.mu.Lock()
- if loaded.did[file] {
- loaded.mu.Unlock()
- return
- }
- if loaded.did == nil {
- loaded.did = make(map[string]bool)
- }
- loaded.did[file] = true
- loaded.mu.Unlock()
-
- f, err := os.Open(file)
- if err != nil {
- log.Fatal(err)
- }
- defer f.Close()
-
- b := bufio.NewReader(f)
- var ungetLine string
- nextLine := func() string {
- if ungetLine != "" {
- l := ungetLine
- ungetLine = ""
- return l
- }
- line, err := b.ReadString('\n')
- if err != nil {
- if err == io.EOF {
- return ""
- }
- log.Fatalf("%s: unexpected read error: %v", file, err)
- }
- return line
- }
-
- for {
- line := nextLine()
- if line == "" { // EOF
- break
- }
- line = strings.TrimSpace(line)
- if strings.HasPrefix(line, "#") || line == "" {
- continue
- }
- if !strings.HasPrefix(line, "GET ") {
- log.Fatalf("%s: malformed GET line: %s", file, line)
- }
- url := line[len("GET "):]
- status := nextLine()
- if _, err := strconv.Atoi(strings.Fields(status)[0]); err != nil {
- log.Fatalf("%s: malformed status line (after GET %s): %s", file, url, status)
- }
- hdr := make(http.Header)
- for {
- kv := strings.TrimSpace(nextLine())
- if kv == "" {
- break
- }
- i := strings.Index(kv, ":")
- if i < 0 {
- log.Fatalf("%s: malformed header line (after GET %s): %s", file, url, kv)
- }
- k, v := kv[:i], strings.TrimSpace(kv[i+1:])
- hdr[k] = append(hdr[k], v)
- }
-
- var body []byte
- Body:
- for n := 0; ; n++ {
- line := nextLine()
- if n == 0 && isHexDump([]byte(line)) {
- ungetLine = line
- b, err := parseHexDump(nextLine)
- if err != nil {
- log.Fatalf("%s: malformed hex dump (after GET %s): %v", file, url, err)
- }
- body = b
- break
- }
- if line == "" { // EOF
- for i := 0; i < 2; i++ {
- if len(body) > 0 && body[len(body)-1] == '\n' {
- body = body[:len(body)-1]
- }
- }
- break
- }
- body = append(body, line...)
- for line == "\n" {
- line = nextLine()
- if strings.HasPrefix(line, "GET ") {
- ungetLine = line
- body = body[:len(body)-1]
- if len(body) > 0 {
- body = body[:len(body)-1]
- }
- break Body
- }
- body = append(body, line...)
- }
- }
-
- Serve(url, status, hdr, body)
- }
-}
-
-func isHexDump(data []byte) bool {
- return bytes.HasPrefix(data, []byte("00000000 ")) || bytes.HasPrefix(data, []byte("0000000 "))
-}
-
-// parseHexDump parses the hex dump in text, which should be the
-// output of "hexdump -C" or Plan 9's "xd -b" or Go's hex.Dump
-// and returns the original data used to produce the dump.
-// It is meant to enable storing golden binary files as text, so that
-// changes to the golden files can be seen during code reviews.
-func parseHexDump(nextLine func() string) ([]byte, error) {
- var out []byte
- for {
- line := nextLine()
- if line == "" || line == "\n" {
- break
- }
- if i := strings.Index(line, "|"); i >= 0 { // remove text dump
- line = line[:i]
- }
- f := strings.Fields(line)
- if len(f) > 1+16 {
- return nil, fmt.Errorf("parsing hex dump: too many fields on line %q", line)
- }
- if len(f) == 0 || len(f) == 1 && f[0] == "*" { // all zeros block omitted
- continue
- }
- addr64, err := strconv.ParseUint(f[0], 16, 0)
- if err != nil {
- return nil, fmt.Errorf("parsing hex dump: invalid address %q", f[0])
- }
- addr := int(addr64)
- if len(out) < addr {
- out = append(out, make([]byte, addr-len(out))...)
- }
- for _, x := range f[1:] {
- val, err := strconv.ParseUint(x, 16, 8)
- if err != nil {
- return nil, fmt.Errorf("parsing hexdump: invalid hex byte %q", x)
- }
- out = append(out, byte(val))
- }
- }
- return out, nil
-}
--- /dev/null
+# golang.org/issue/13037: 'go get' was not parsing <meta> tags in 404 served over HTTPS.
+
+[!net] skip
+
+env GO111MODULE=off
+go get -d -insecure bazil.org/fuse/fs/fstestutil
+
+env GO111MODULE=on
+env GOPROXY=direct
+go get -d -insecure bazil.org/fuse/fs/fstestutil
-# golang.org/issue/13037: 'go get' was not parsing <meta> tags in 404 served over HTTPS.
# golang.org/issue/29591: 'go get' was following plain-HTTP redirects even without -insecure.
[!net] skip
--- /dev/null
+[!net] skip
+
+env GO111MODULE=on
+env GOPROXY=direct
+
+# Without credentials, downloading a module from a path that requires HTTPS
+# basic auth should fail.
+env NETRC=$WORK/empty
+! go list all
+
+# With credentials from a netrc file, it should succeed.
+env NETRC=$WORK/netrc
+go mod tidy
+go list all
+stdout vcs-test.golang.org/auth/or401
+stdout vcs-test.golang.org/auth/or404
+
+-- go.mod --
+module private.example.com
+-- main.go --
+package useprivate
+
+import (
+ _ "vcs-test.golang.org/auth/or401"
+ _ "vcs-test.golang.org/auth/or404"
+)
+-- $WORK/empty --
+-- $WORK/netrc --
+machine vcs-test.golang.org
+ login aladdin
+ password opensesame