blob: 2494f80ab02f2c5ffe1a5947f4f1560435fba8ef [file] [log] [blame]
// 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.
// Support for custom domains.
package modfetch
import (
"encoding/xml"
"fmt"
"io"
"net/url"
"os"
"strings"
"cmd/go/internal/modfetch/codehost"
"cmd/go/internal/modfetch/gitrepo"
)
// metaImport represents the parsed <meta name="go-import"
// content="prefix vcs reporoot" /> tags from HTML files.
type metaImport struct {
Prefix, VCS, RepoRoot string
}
func lookupCustomDomain(path string) (Repo, error) {
dom := path
if i := strings.Index(dom, "/"); i >= 0 {
dom = dom[:i]
}
if !strings.Contains(dom, ".") {
return nil, fmt.Errorf("unknown module %s: not a domain name", path)
}
var body io.ReadCloser
err := webGetGoGet("https://"+path+"?go-get=1", &body)
if body != nil {
defer body.Close()
}
if err != nil {
fmt.Fprintf(os.Stderr, "FindRepo: %v\n", err)
return nil, err
}
// Note: accepting a non-200 OK here, so people can serve a
// meta import in their http 404 page.
imports, err := parseMetaGoImports(body)
if err != nil {
fmt.Fprintf(os.Stderr, "findRepo: %v\n", err)
return nil, err
}
if len(imports) == 0 {
return nil, fmt.Errorf("unknown module %s: no go-import tags", path)
}
// First look for new module definition.
for _, imp := range imports {
if path == imp.Prefix || strings.HasPrefix(path, imp.Prefix+"/") {
if imp.VCS == "mod" {
u, err := url.Parse(imp.RepoRoot)
if err != nil {
return nil, fmt.Errorf("invalid module URL %q", imp.RepoRoot)
} else if u.Scheme != "https" {
// TODO: Allow -insecure flag as a build flag?
return nil, fmt.Errorf("invalid module URL %q: must be HTTPS", imp.RepoRoot)
}
return newProxyRepo(imp.RepoRoot, imp.Prefix), nil
}
}
}
// Fall back to redirections to known version control systems.
for _, imp := range imports {
if path == imp.Prefix {
if !strings.HasPrefix(imp.RepoRoot, "https://") {
// TODO: Allow -insecure flag as a build flag?
return nil, fmt.Errorf("invalid server URL %q: must be HTTPS", imp.RepoRoot)
}
if imp.VCS == "git" {
code, err := gitrepo.Repo(imp.RepoRoot, imp.Prefix)
if err != nil {
return nil, err
}
return newCodeRepo(code, path)
}
return nil, fmt.Errorf("unknown VCS, Repo: %s, %s", imp.VCS, imp.RepoRoot)
}
}
// Check for redirect to repo root.
for _, imp := range imports {
if strings.HasPrefix(path, imp.Prefix+"/") {
return nil, &ModuleSubdirError{imp.Prefix}
}
}
return nil, fmt.Errorf("unknown module %s: no matching go-import tags", path)
}
type ModuleSubdirError struct {
ModulePath string
}
func (e *ModuleSubdirError) Error() string {
return fmt.Sprintf("module root is %q", e.ModulePath)
}
type customPrefix struct {
codehost.Repo
root string
}
func (c *customPrefix) Root() string {
return c.root
}
// parseMetaGoImports returns meta imports from the HTML in r.
// Parsing ends at the end of the <head> section or the beginning of the <body>.
func parseMetaGoImports(r io.Reader) (imports []metaImport, err error) {
d := xml.NewDecoder(r)
d.CharsetReader = charsetReader
d.Strict = false
var t xml.Token
for {
t, err = d.RawToken()
if err != nil {
if err == io.EOF || len(imports) > 0 {
err = nil
}
return
}
if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {
return
}
if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") {
return
}
e, ok := t.(xml.StartElement)
if !ok || !strings.EqualFold(e.Name.Local, "meta") {
continue
}
if attrValue(e.Attr, "name") != "go-import" {
continue
}
if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 {
imports = append(imports, metaImport{
Prefix: f[0],
VCS: f[1],
RepoRoot: f[2],
})
}
}
}
// attrValue returns the attribute value for the case-insensitive key
// `name', or the empty string if nothing is found.
func attrValue(attrs []xml.Attr, name string) string {
for _, a := range attrs {
if strings.EqualFold(a.Name.Local, name) {
return a.Value
}
}
return ""
}
// charsetReader returns a reader for the given charset. Currently
// it only supports UTF-8 and ASCII. Otherwise, it returns a meaningful
// error which is printed by go get, so the user can find why the package
// wasn't downloaded if the encoding is not supported. Note that, in
// order to reduce potential errors, ASCII is treated as UTF-8 (i.e. characters
// greater than 0x7f are not rejected).
func charsetReader(charset string, input io.Reader) (io.Reader, error) {
switch strings.ToLower(charset) {
case "ascii":
return input, nil
default:
return nil, fmt.Errorf("can't decode XML document using charset %q", charset)
}
}