dep/gps/deduce.go

1008 строки
28 KiB
Go
Исходник Постоянная ссылка Обычный вид История

// 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]*)*)$`)
2016-06-08 22:32:43 +03:00
//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-_.~]+)*$`)
)
2017-01-10 06:46:01 +03:00
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
}
2016-08-05 17:46:16 +03:00
type pathDeducer interface {
2017-10-11 04:12:38 +03:00
// 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)
}
2016-08-05 17:46:16 +03:00
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
}
2016-08-05 17:46:16 +03:00
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
}
2016-08-05 17:46:16 +03:00
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)
2016-06-08 22:32:56 +03:00
} else {
u.Path = path.Join(v[2], v[3])
2016-06-08 22:32:56 +03:00
}
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:])
}
2016-06-08 22:32:56 +03:00
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
}
2016-08-05 17:46:16 +03:00
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
}
2016-08-05 17:46:16 +03:00
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
}
2016-08-05 17:46:16 +03:00
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())
}
}
2016-08-05 17:46:16 +03:00
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
}
2016-08-05 17:46:16 +03:00
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 {
2017-03-31 13:01:17 +03:00
suprvsr *supervisor
mut sync.RWMutex
rootxt *radix.Tree
deducext *deducerTrie
}
2017-03-31 13:01:17 +03:00
func newDeductionCoordinator(superv *supervisor) *deductionCoordinator {
dc := &deductionCoordinator{
2017-03-31 13:01:17 +03:00
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,
2017-03-31 13:01:17 +03:00
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)
2017-03-31 13:01:17 +03:00
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
2017-03-31 13:01:17 +03:00
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
2017-05-25 16:12:56 +03:00
// 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
}