| // 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(`^(?P<root>github\.com(/[A-Za-z0-9][-A-Za-z0-9]*/[A-Za-z0-9_.\-]+))((?:/[A-Za-z0-9_.\-]+)*)$`) |
| gpinNewRegex = regexp.MustCompile(`^(?P<root>gopkg\.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(`^(?P<root>gopkg\.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(`^(?P<root>bitbucket\.org(?P<bitname>/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))((?:/[A-Za-z0-9_.\-]+)*)$`) |
| //lpRegex = regexp.MustCompile(`^(?P<root>launchpad\.net/([A-Za-z0-9-._]+)(/[A-Za-z0-9-._]+)?)(/.+)?`) |
| lpRegex = regexp.MustCompile(`^(?P<root>launchpad\.net(/[A-Za-z0-9-._]+))((?:/[A-Za-z0-9_.\-]+)*)?$`) |
| //glpRegex = regexp.MustCompile(`^(?P<root>git\.launchpad\.net/([A-Za-z0-9_.\-]+)|~[A-Za-z0-9_.\-]+/(\+git|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+)$`) |
| glpRegex = regexp.MustCompile(`^(?P<root>git\.launchpad\.net(/[A-Za-z0-9_.\-]+))((?:/[A-Za-z0-9_.\-]+)*)$`) |
| //gcRegex = regexp.MustCompile(`^(?P<root>code\.google\.com/[pr]/(?P<project>[a-z0-9\-]+)(\.(?P<subrepo>[a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`) |
| jazzRegex = regexp.MustCompile(`^(?P<root>hub\.jazz\.net(/git/[a-z0-9]+/[A-Za-z0-9_.\-]+))((?:/[A-Za-z0-9_.\-]+)*)$`) |
| apacheRegex = regexp.MustCompile(`^(?P<root>git\.apache\.org(/[a-z0-9_.\-]+\.git))((?:/[A-Za-z0-9_.\-]+)*)$`) |
| vcsExtensionRegex = regexp.MustCompile(`^(?P<root>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?/[A-Za-z0-9_.\-/~]*?\.(?P<vcs>bzr|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 |
| } |