// 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 gps import ( "context" "fmt" "io" "io/ioutil" "net/http" "net/url" "os" "path" "path/filepath" "regexp" "runtime" "strconv" "strings" "sync" radix "github.com/armon/go-radix" "github.com/pkg/errors" ) var ( gitSchemes = []string{"https", "ssh", "git", "http"} bzrSchemes = []string{"https", "bzr+ssh", "bzr", "http"} hgSchemes = []string{"https", "ssh", "http"} svnSchemes = []string{"https", "http", "svn", "svn+ssh"} gopkginSchemes = []string{"https", "http"} netrc []netrcLine readNetrcOnce sync.Once ) const gopkgUnstableSuffix = "-unstable" func validateVCSScheme(scheme, typ string) bool { // everything allows plain ssh if scheme == "ssh" { return true } var schemes []string switch typ { case "git": schemes = gitSchemes case "bzr": schemes = bzrSchemes case "hg": schemes = hgSchemes case "svn": schemes = svnSchemes default: panic(fmt.Sprint("unsupported vcs type", scheme)) } for _, valid := range schemes { if scheme == valid { return true } } return false } // Regexes for the different known import path flavors var ( // This regex allows some usernames that github currently disallows. They // have allowed them in the past. ghRegex = regexp.MustCompile(`^(?Pgithub\.com(/[A-Za-z0-9][-A-Za-z0-9]*/[A-Za-z0-9_.\-]+))((?:/[A-Za-z0-9_.\-]+)*)$`) gpinNewRegex = regexp.MustCompile(`^(?Pgopkg\.in(?:(/[a-zA-Z0-9][-a-zA-Z0-9]+)?)(/[a-zA-Z][-.a-zA-Z0-9]*)\.((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2}(?:-unstable)?)(?:\.git)?)((?:/[a-zA-Z0-9][-.a-zA-Z0-9]*)*)$`) //gpinOldRegex = regexp.MustCompile(`^(?Pgopkg\.in/(?:([a-z0-9][-a-z0-9]+)/)?((?:v0|v[1-9][0-9]*)(?:\.0|\.[1-9][0-9]*){0,2}(-unstable)?)/([a-zA-Z][-a-zA-Z0-9]*)(?:\.git)?)((?:/[a-zA-Z][-a-zA-Z0-9]*)*)$`) bbRegex = regexp.MustCompile(`^(?Pbitbucket\.org(?P/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))((?:/[A-Za-z0-9_.\-]+)*)$`) //lpRegex = regexp.MustCompile(`^(?Plaunchpad\.net/([A-Za-z0-9-._]+)(/[A-Za-z0-9-._]+)?)(/.+)?`) lpRegex = regexp.MustCompile(`^(?Plaunchpad\.net(/[A-Za-z0-9-._]+))((?:/[A-Za-z0-9_.\-]+)*)?$`) //glpRegex = regexp.MustCompile(`^(?Pgit\.launchpad\.net/([A-Za-z0-9_.\-]+)|~[A-Za-z0-9_.\-]+/(\+git|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+)$`) glpRegex = regexp.MustCompile(`^(?Pgit\.launchpad\.net(/[A-Za-z0-9_.\-]+))((?:/[A-Za-z0-9_.\-]+)*)$`) //gcRegex = regexp.MustCompile(`^(?Pcode\.google\.com/[pr]/(?P[a-z0-9\-]+)(\.(?P[a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`) jazzRegex = regexp.MustCompile(`^(?Phub\.jazz\.net(/git/[a-z0-9]+/[A-Za-z0-9_.\-]+))((?:/[A-Za-z0-9_.\-]+)*)$`) apacheRegex = regexp.MustCompile(`^(?Pgit\.apache\.org(/[a-z0-9_.\-]+\.git))((?:/[A-Za-z0-9_.\-]+)*)$`) vcsExtensionRegex = regexp.MustCompile(`^(?P([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?/[A-Za-z0-9_.\-/~]*?\.(?Pbzr|git|hg|svn))((?:/[A-Za-z0-9_.\-]+)*)$`) ) // Other helper regexes var ( scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`) pathvld = regexp.MustCompile(`^([A-Za-z0-9-]+)(\.[A-Za-z0-9-]+)+(/[A-Za-z0-9-_.~]+)*$`) ) func pathDeducerTrie() *deducerTrie { dxt := newDeducerTrie() dxt.Insert("github.com/", githubDeducer{regexp: ghRegex}) dxt.Insert("gopkg.in/", gopkginDeducer{regexp: gpinNewRegex}) dxt.Insert("bitbucket.org/", bitbucketDeducer{regexp: bbRegex}) dxt.Insert("launchpad.net/", launchpadDeducer{regexp: lpRegex}) dxt.Insert("git.launchpad.net/", launchpadGitDeducer{regexp: glpRegex}) dxt.Insert("hub.jazz.net/", jazzDeducer{regexp: jazzRegex}) dxt.Insert("git.apache.org/", apacheDeducer{regexp: apacheRegex}) return dxt } type pathDeducer interface { // deduceRoot takes an import path such as // "github.com/some-user/some-package/some-subpackage" // and returns the root folder to where the version control // system exists. For example, the root folder where .git exists. // So the return of the above string would be // "github.com/some-user/some-package" deduceRoot(string) (string, error) deduceSource(string, *url.URL) (maybeSources, error) } type githubDeducer struct { regexp *regexp.Regexp } func (m githubDeducer) deduceRoot(path string) (string, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return "", fmt.Errorf("%s is not a valid path for a source on github.com", path) } return "github.com" + v[2], nil } func (m githubDeducer) deduceSource(path string, u *url.URL) (maybeSources, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return nil, fmt.Errorf("%s is not a valid path for a source on github.com", path) } u.Host = "github.com" u.Path = v[2] if u.Scheme == "ssh" && u.User != nil && u.User.Username() != "git" { return nil, fmt.Errorf("github ssh must be accessed via the 'git' user; %s was provided", u.User.Username()) } else if u.Scheme != "" { if !validateVCSScheme(u.Scheme, "git") { return nil, fmt.Errorf("%s is not a valid scheme for accessing a git repository", u.Scheme) } if u.Scheme == "ssh" { u.User = url.User("git") } return maybeSources{maybeGitSource{url: u}}, nil } mb := make(maybeSources, len(gitSchemes)) for k, scheme := range gitSchemes { u2 := *u if scheme == "ssh" { u2.User = url.User("git") } u2.Scheme = scheme mb[k] = maybeGitSource{url: &u2} } return mb, nil } type bitbucketDeducer struct { regexp *regexp.Regexp } func (m bitbucketDeducer) deduceRoot(path string) (string, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return "", fmt.Errorf("%s is not a valid path for a source on bitbucket.org", path) } return "bitbucket.org" + v[2], nil } func (m bitbucketDeducer) deduceSource(path string, u *url.URL) (maybeSources, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return nil, fmt.Errorf("%s is not a valid path for a source on bitbucket.org", path) } u.Host = "bitbucket.org" u.Path = v[2] // This isn't definitive, but it'll probably catch most isgit := strings.HasSuffix(u.Path, ".git") || (u.User != nil && u.User.Username() == "git") ishg := strings.HasSuffix(u.Path, ".hg") || (u.User != nil && u.User.Username() == "hg") // TODO(sdboyer) resolve scm ambiguity if needed by querying bitbucket's REST API if u.Scheme != "" { validgit, validhg := validateVCSScheme(u.Scheme, "git"), validateVCSScheme(u.Scheme, "hg") if isgit { if !validgit { // This is unreachable for now, as the git schemes are a // superset of the hg schemes return nil, fmt.Errorf("%s is not a valid scheme for accessing a git repository", u.Scheme) } return maybeSources{maybeGitSource{url: u}}, nil } else if ishg { if !validhg { return nil, fmt.Errorf("%s is not a valid scheme for accessing an hg repository", u.Scheme) } return maybeSources{maybeHgSource{url: u}}, nil } else if !validgit && !validhg { return nil, fmt.Errorf("%s is not a valid scheme for accessing either a git or hg repository", u.Scheme) } // No other choice, make an option for both git and hg return maybeSources{ maybeHgSource{url: u}, maybeGitSource{url: u}, }, nil } mb := make(maybeSources, 0) // git is probably more common, even on bitbucket. however, bitbucket // appears to fail _extremely_ slowly on git pings (ls-remote) when the // underlying repository is actually an hg repository, so it's better // to try hg first. if !isgit { for _, scheme := range hgSchemes { u2 := *u if scheme == "ssh" { u2.User = url.User("hg") } u2.Scheme = scheme mb = append(mb, maybeHgSource{url: &u2}) } } if !ishg { for _, scheme := range gitSchemes { u2 := *u if scheme == "ssh" { u2.User = url.User("git") } u2.Scheme = scheme mb = append(mb, maybeGitSource{url: &u2}) } } return mb, nil } type gopkginDeducer struct { regexp *regexp.Regexp } func (m gopkginDeducer) deduceRoot(p string) (string, error) { v, err := m.parseAndValidatePath(p) if err != nil { return "", err } return v[1], nil } func (m gopkginDeducer) parseAndValidatePath(p string) ([]string, error) { v := m.regexp.FindStringSubmatch(p) if v == nil { return nil, fmt.Errorf("%s is not a valid path for a source on gopkg.in", p) } // We duplicate some logic from the gopkg.in server in order to validate the // import path string without having to make a network request if strings.Contains(v[4], ".") { return nil, fmt.Errorf("%s is not a valid import path; gopkg.in only allows major versions (%q instead of %q)", p, v[4][:strings.Index(v[4], ".")], v[4]) } return v, nil } func (m gopkginDeducer) deduceSource(p string, u *url.URL) (maybeSources, error) { // Reuse root detection logic for initial validation v, err := m.parseAndValidatePath(p) if err != nil { return nil, err } // Putting a scheme on gopkg.in would be really weird, disallow it if u.Scheme != "" { return nil, fmt.Errorf("specifying alternate schemes on gopkg.in imports is not permitted") } // gopkg.in is always backed by github u.Host = "github.com" if v[2] == "" { elem := v[3][1:] u.Path = path.Join("/go-"+elem, elem) } else { u.Path = path.Join(v[2], v[3]) } unstable := false majorStr := v[4] if strings.HasSuffix(majorStr, gopkgUnstableSuffix) { unstable = true majorStr = strings.TrimSuffix(majorStr, gopkgUnstableSuffix) } major, err := strconv.ParseUint(majorStr[1:], 10, 64) if err != nil { // this should only be reachable if there's an error in the regex return nil, fmt.Errorf("could not parse %q as a gopkg.in major version", majorStr[1:]) } mb := make(maybeSources, len(gopkginSchemes)) for k, scheme := range gopkginSchemes { u2 := *u u2.Scheme = scheme mb[k] = maybeGopkginSource{ opath: v[1], url: &u2, major: major, unstable: unstable, } } return mb, nil } type launchpadDeducer struct { regexp *regexp.Regexp } func (m launchpadDeducer) deduceRoot(path string) (string, error) { // TODO(sdboyer) lp handling is nasty - there's ambiguities which can only really // be resolved with a metadata request. See https://github.com/golang/go/issues/11436 v := m.regexp.FindStringSubmatch(path) if v == nil { return "", fmt.Errorf("%s is not a valid path for a source on launchpad.net", path) } return "launchpad.net" + v[2], nil } func (m launchpadDeducer) deduceSource(path string, u *url.URL) (maybeSources, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return nil, fmt.Errorf("%s is not a valid path for a source on launchpad.net", path) } u.Host = "launchpad.net" u.Path = v[2] if u.Scheme != "" { if !validateVCSScheme(u.Scheme, "bzr") { return nil, fmt.Errorf("%s is not a valid scheme for accessing a bzr repository", u.Scheme) } return maybeSources{maybeBzrSource{url: u}}, nil } mb := make(maybeSources, len(bzrSchemes)) for k, scheme := range bzrSchemes { u2 := *u u2.Scheme = scheme mb[k] = maybeBzrSource{url: &u2} } return mb, nil } type launchpadGitDeducer struct { regexp *regexp.Regexp } func (m launchpadGitDeducer) deduceRoot(path string) (string, error) { // TODO(sdboyer) same ambiguity issues as with normal bzr lp v := m.regexp.FindStringSubmatch(path) if v == nil { return "", fmt.Errorf("%s is not a valid path for a source on git.launchpad.net", path) } return "git.launchpad.net" + v[2], nil } func (m launchpadGitDeducer) deduceSource(path string, u *url.URL) (maybeSources, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return nil, fmt.Errorf("%s is not a valid path for a source on git.launchpad.net", path) } u.Host = "git.launchpad.net" u.Path = v[2] if u.Scheme != "" { if !validateVCSScheme(u.Scheme, "git") { return nil, fmt.Errorf("%s is not a valid scheme for accessing a git repository", u.Scheme) } return maybeSources{maybeGitSource{url: u}}, nil } mb := make(maybeSources, len(gitSchemes)) for k, scheme := range gitSchemes { u2 := *u u2.Scheme = scheme mb[k] = maybeGitSource{url: &u2} } return mb, nil } type jazzDeducer struct { regexp *regexp.Regexp } func (m jazzDeducer) deduceRoot(path string) (string, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return "", fmt.Errorf("%s is not a valid path for a source on hub.jazz.net", path) } return "hub.jazz.net" + v[2], nil } func (m jazzDeducer) deduceSource(path string, u *url.URL) (maybeSources, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return nil, fmt.Errorf("%s is not a valid path for a source on hub.jazz.net", path) } u.Host = "hub.jazz.net" u.Path = v[2] switch u.Scheme { case "": u.Scheme = "https" fallthrough case "https": return maybeSources{maybeGitSource{url: u}}, nil default: return nil, fmt.Errorf("IBM's jazz hub only supports https, %s is not allowed", u.String()) } } type apacheDeducer struct { regexp *regexp.Regexp } func (m apacheDeducer) deduceRoot(path string) (string, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return "", fmt.Errorf("%s is not a valid path for a source on git.apache.org", path) } return "git.apache.org" + v[2], nil } func (m apacheDeducer) deduceSource(path string, u *url.URL) (maybeSources, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return nil, fmt.Errorf("%s is not a valid path for a source on git.apache.org", path) } u.Host = "git.apache.org" u.Path = v[2] if u.Scheme != "" { if !validateVCSScheme(u.Scheme, "git") { return nil, fmt.Errorf("%s is not a valid scheme for accessing a git repository", u.Scheme) } return maybeSources{maybeGitSource{url: u}}, nil } mb := make(maybeSources, len(gitSchemes)) for k, scheme := range gitSchemes { u2 := *u u2.Scheme = scheme mb[k] = maybeGitSource{url: &u2} } return mb, nil } type vcsExtensionDeducer struct { regexp *regexp.Regexp } func (m vcsExtensionDeducer) deduceRoot(path string) (string, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return "", fmt.Errorf("%s contains no vcs extension hints for matching", path) } return v[1], nil } func (m vcsExtensionDeducer) deduceSource(path string, u *url.URL) (maybeSources, error) { v := m.regexp.FindStringSubmatch(path) if v == nil { return nil, fmt.Errorf("%s contains no vcs extension hints for matching", path) } switch v[4] { case "git", "hg", "bzr": x := strings.SplitN(v[1], "/", 2) // TODO(sdboyer) is this actually correct for bzr? u.Host = x[0] u.Path = "/" + x[1] if u.Scheme != "" { if !validateVCSScheme(u.Scheme, v[4]) { return nil, fmt.Errorf("%s is not a valid scheme for accessing %s repositories (path %s)", u.Scheme, v[4], path) } switch v[4] { case "git": return maybeSources{maybeGitSource{url: u}}, nil case "bzr": return maybeSources{maybeBzrSource{url: u}}, nil case "hg": return maybeSources{maybeHgSource{url: u}}, nil } } var schemes []string var mb maybeSources var f func(k int, u *url.URL) switch v[4] { case "git": schemes = gitSchemes f = func(k int, u *url.URL) { mb[k] = maybeGitSource{url: u} } case "bzr": schemes = bzrSchemes f = func(k int, u *url.URL) { mb[k] = maybeBzrSource{url: u} } case "hg": schemes = hgSchemes f = func(k int, u *url.URL) { mb[k] = maybeHgSource{url: u} } } mb = make(maybeSources, len(schemes)) for k, scheme := range schemes { u2 := *u u2.Scheme = scheme f(k, &u2) } return mb, nil default: return nil, fmt.Errorf("unknown repository type: %q", v[4]) } } // A deducer takes an import path and inspects it to determine where the // corresponding project root should be. It applies a number of matching // techniques, eventually falling back to an HTTP request for go-get metadata if // none of the explicit rules succeed. // // The only real implementation is deductionCoordinator. The interface is // primarily intended for testing purposes. type deducer interface { deduceRootPath(ctx context.Context, path string) (pathDeduction, error) } type deductionCoordinator struct { suprvsr *supervisor mut sync.RWMutex rootxt *radix.Tree deducext *deducerTrie } func newDeductionCoordinator(superv *supervisor) *deductionCoordinator { dc := &deductionCoordinator{ suprvsr: superv, rootxt: radix.New(), deducext: pathDeducerTrie(), } return dc } // deduceRootPath takes an import path and attempts to deduce various // metadata about it - what type of source should handle it, and where its // "root" is (for vcs repositories, the repository root). // // If no errors are encountered, the returned pathDeduction will contain both // the root path and a list of maybeSources, which can be subsequently used to // create a handler that will manage the particular source. func (dc *deductionCoordinator) deduceRootPath(ctx context.Context, path string) (pathDeduction, error) { if err := dc.suprvsr.ctx.Err(); err != nil { return pathDeduction{}, err } // First, check the rootxt to see if there's a prefix match - if so, we // can return that and move on. dc.mut.RLock() prefix, data, has := dc.rootxt.LongestPrefix(path) dc.mut.RUnlock() if has && isPathPrefixOrEqual(prefix, path) { switch d := data.(type) { case maybeSources: return pathDeduction{root: prefix, mb: d}, nil case *httpMetadataDeducer: // Multiple calls have come in for a similar path shape during // the window in which the HTTP request to retrieve go get // metadata is in flight. Fold this request in with the existing // one(s) by calling the deduction method, which will avoid // duplication of work through a sync.Once. return d.deduce(ctx, path) } panic(fmt.Sprintf("unexpected %T in deductionCoordinator.rootxt: %v", data, data)) } // No match. Try known path deduction first. pd, err := dc.deduceKnownPaths(path) if err == nil { // Deduction worked; store it in the rootxt, send on retchan and // terminate. // FIXME(sdboyer) deal with changing path vs. root. Probably needs // to be predeclared and reused in the hmd returnFunc dc.mut.Lock() dc.rootxt.Insert(pd.root, pd.mb) dc.mut.Unlock() return pd, nil } if err != errNoKnownPathMatch { return pathDeduction{}, err } // The err indicates no known path matched. It's still possible that // retrieving go get metadata might do the trick. hmd := &httpMetadataDeducer{ basePath: path, suprvsr: dc.suprvsr, // The vanity deducer will call this func with a completed // pathDeduction if it succeeds in finding one. We process it // back through the action channel to ensure serialized // access to the rootxt map. returnFunc: func(pd pathDeduction) { dc.mut.Lock() dc.rootxt.Insert(pd.root, pd.mb) dc.mut.Unlock() }, } // Save the hmd in the rootxt so that calls checking on similar // paths made while the request is in flight can be folded together. dc.mut.Lock() dc.rootxt.Insert(path, hmd) dc.mut.Unlock() // Trigger the HTTP-backed deduction process for this requestor. return hmd.deduce(ctx, path) } // pathDeduction represents the results of a successful import path deduction - // a root path, plus a maybeSource that can be used to attempt to connect to // the source. type pathDeduction struct { root string mb maybeSources } var errNoKnownPathMatch = errors.New("no known path match") func (dc *deductionCoordinator) deduceKnownPaths(path string) (pathDeduction, error) { u, path, err := normalizeURI(path) if err != nil { return pathDeduction{}, err } // First, try the root path-based matches if _, mtch, has := dc.deducext.LongestPrefix(path); has { root, err := mtch.deduceRoot(path) if err != nil { return pathDeduction{}, err } mb, err := mtch.deduceSource(path, u) if err != nil { return pathDeduction{}, err } return pathDeduction{ root: root, mb: mb, }, nil } // Next, try the vcs extension-based (infix) matcher exm := vcsExtensionDeducer{regexp: vcsExtensionRegex} if root, err := exm.deduceRoot(path); err == nil { mb, err := exm.deduceSource(path, u) if err != nil { return pathDeduction{}, err } return pathDeduction{ root: root, mb: mb, }, nil } return pathDeduction{}, errNoKnownPathMatch } type httpMetadataDeducer struct { once sync.Once deduced pathDeduction deduceErr error basePath string returnFunc func(pathDeduction) suprvsr *supervisor } func (hmd *httpMetadataDeducer) deduce(ctx context.Context, path string) (pathDeduction, error) { hmd.once.Do(func() { opath := path u, path, err := normalizeURI(path) if err != nil { err = errors.Wrapf(err, "unable to normalize URI") hmd.deduceErr = err return } pd := pathDeduction{} // Make the HTTP call to attempt to retrieve go-get metadata var root, vcs, reporoot string err = hmd.suprvsr.do(ctx, path, ctHTTPMetadata, func(ctx context.Context) error { root, vcs, reporoot, err = getMetadata(ctx, path, u.Scheme) if err != nil { err = errors.Wrapf(err, "unable to read metadata") } return err }) if err != nil { err = errors.Wrapf(err, "unable to deduce repository and source type for %q", opath) hmd.deduceErr = err return } pd.root = root // If we got something back at all, then it supersedes the actual input for // the real URL to hit repoURL, err := url.Parse(reporoot) if err != nil { err = errors.Wrapf(err, "server returned bad URL in go-get metadata, reporoot=%q", reporoot) hmd.deduceErr = err return } // If the input path specified a scheme, then try to honor it. if u.Scheme != "" && repoURL.Scheme != u.Scheme { // If the input scheme was http, but the go-get metadata // nevertheless indicated https should be used for the repo, then // trust the metadata and use https. // // To err on the secure side, do NOT allow the same in the other // direction (https -> http). if u.Scheme != "http" || repoURL.Scheme != "https" { hmd.deduceErr = errors.Errorf("scheme mismatch for %q: input asked for %q, but go-get metadata specified %q", path, u.Scheme, repoURL.Scheme) return } } switch vcs { case "git": pd.mb = maybeSources{maybeGitSource{url: repoURL}} case "bzr": pd.mb = maybeSources{maybeBzrSource{url: repoURL}} case "hg": pd.mb = maybeSources{maybeHgSource{url: repoURL}} default: hmd.deduceErr = errors.Errorf("unsupported vcs type %s in go-get metadata from %s", vcs, path) return } hmd.deduced = pd // All data is assigned for other goroutines that may be waiting. Now, // send the pathDeduction back to the deductionCoordinator by calling // the returnFunc. This will also remove the reference to this hmd in // the coordinator's trie. // // When this call finishes, it is guaranteed the coordinator will have // at least begun running the action to insert the path deduction, which // means no other deduction request will be able to interleave and // request the same path before the pathDeduction can be processed, but // after this hmd has been dereferenced from the trie. hmd.returnFunc(pd) }) return hmd.deduced, hmd.deduceErr } // normalizeURI takes a path string - which can be a plain import path, or a // proper URI, or something SCP-shaped - performs basic validity checks, and // returns both a full URL and just the path portion. func normalizeURI(p string) (*url.URL, string, error) { var u *url.URL var newpath string if m := scpSyntaxRe.FindStringSubmatch(p); 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". u = &url.URL{ Scheme: "ssh", User: url.User(m[1]), Host: m[2], Path: "/" + m[3], // TODO(sdboyer) This is what stdlib sets; grok why better //RawPath: m[3], } } else { var err error u, err = url.Parse(p) if err != nil { return nil, "", errors.Errorf("%q is not a valid URI", p) } } // If no scheme was passed, then the entire path will have been put into // u.Path. Either way, construct the normalized path correctly. if u.Host == "" { newpath = p } else { newpath = path.Join(u.Host, u.Path) } return u, newpath, nil } // fetchMetadata fetches the remote metadata for path. func fetchMetadata(ctx context.Context, path, scheme string) (rc io.ReadCloser, err error) { if scheme == "http" { rc, err = doFetchMetadata(ctx, "http", path) return } rc, err = doFetchMetadata(ctx, "https", path) if err == nil { return } rc, err = doFetchMetadata(ctx, "http", path) return } func doFetchMetadata(ctx context.Context, scheme, path string) (io.ReadCloser, error) { url := fmt.Sprintf("%s://%s?go-get=1", scheme, path) switch scheme { case "https", "http": req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, errors.Wrapf(err, "unable to build HTTP request for URL %q", url) } req = addAuthFromNetrc(url, req) resp, err := http.DefaultClient.Do(req.WithContext(ctx)) if err != nil { return nil, errors.Wrapf(err, "failed HTTP request to URL %q", url) } return resp.Body, nil default: return nil, errors.Errorf("unknown remote protocol scheme: %q", scheme) } } // See https://github.com/golang/go/blob/master/src/cmd/go/internal/web2/web.go // for implementation // Temporary netrc reader until https://github.com/golang/go/issues/31334 is solved type netrcLine struct { machine string login string password string } 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 "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 := os.Getenv("HOME") base := ".netrc" if runtime.GOOS == "windows" { base = "_netrc" } return filepath.Join(dir, base), nil } // readNetrc parses a user's netrc file, ignoring any errors that occur. func readNetrc() { path, err := netrcPath() if err != nil { return } data, err := ioutil.ReadFile(path) if err != nil { return } netrc = parseNetrc(string(data)) } // addAuthFromNetrc uses basic authentication on go-get requests // for private repositories. func addAuthFromNetrc(rawurl string, req *http.Request) *http.Request { readNetrcOnce.Do(readNetrc) for _, m := range netrc { u, err := url.Parse(rawurl) if err != nil { continue } if u.Host == m.machine { req.SetBasicAuth(m.login, m.password) break } } return req } // getMetadata fetches and decodes remote metadata for path. // // scheme is optional. If it's http, only http will be attempted for fetching. // Any other scheme (including none) will first try https, then fall back to // http. func getMetadata(ctx context.Context, path, scheme string) (string, string, string, error) { rc, err := fetchMetadata(ctx, path, scheme) if err != nil { return "", "", "", errors.Wrapf(err, "unable to fetch raw metadata") } defer rc.Close() imports, err := parseMetaGoImports(rc) if err != nil { return "", "", "", errors.Wrapf(err, "unable to parse go-import metadata") } match := -1 for i, im := range imports { if !strings.HasPrefix(path, im.Prefix) { continue } if match != -1 { return "", "", "", errors.Errorf("multiple meta tags match import path %q", path) } match = i } if match == -1 { return "", "", "", errors.Errorf("go-import metadata not found") } return imports[match].Prefix, imports[match].VCS, imports[match].RepoRoot, nil }