go get 代理

go get 代理

代理服务端

要把 go get 请求转发别的 repo 上,需要利用 go get 的 discovery 特性去动态查找 repo。

首先要知道 go get 做了什么,定位到 repoRootForImportDynamic 函数(源文件 go/src/cmd/go/internal/get/vcs.go),可以知道命令将 ImportPath 作为 url 发起了一次请求(web.GetMaybeInsecure),并且将获取的到结果使用 parseMetaGoImports 函数(源文件 go/src/cmd/go/internal/get/discovery.go)解析。
根据 parseMetaGoImports 函数可以得知,结果作为 xml 解析,并且在查找所有 meta 元素是否存在 name 属性值为 go-import,如果存在则解析 content 属性值,拆分为 PrefixVCSRepoRoot

那我们可以明确知道了,我们需要返回一个 xml,内容格式可能是这样的

<html>
<head>
<meta name="go-import" content="PREFIX VCS REPOROOT" />
</head>
</html>

然后一个简单的可配置的 https go-get 服务端完成了

package main

import (
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "path"
    "regexp"
    "text/template"
)

type Meta struct {
    re         *regexp.Regexp
    Pattern    string `json:"pattern"`
    Pkg        string `json:"pkg"`
    VCS        string `json:"vcs"`
    Repo       string `json:"repo"`
    Source     string `json:"source"`
    SourceDir  string `json:"sourcedir"`
    SourceLine string `json:"sourceline"`
    Doc        string `json:"doc"`
    Body       string `json:"body"`
}

var metatpl = template.Must(template.New("meta").Parse(`<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="go-import" content="{{.Pkg}} {{.VCS}} {{.Repo}}"/>
{{- if .Source}}
<meta name="go-source" content="{{.Pkg}} {{.Source}} {{.SourceDir}} {{.SourceLine}}"/>
{{- end}}
{{- if .Doc}}
<meta http-equiv="refresh" content="0; url={{.Doc}}"/>
{{- end}}
</head>
<body>
{{- if .Body}}
{{.Body}}
{{- end}}
</body>
</html>
`))

var pkgs map[string]*Meta

func getpkg(w http.ResponseWriter, r *http.Request) {
    if r.Method != "GET" {
        http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        return
    }
    if r.FormValue("go-get") != "1" {
        http.Error(w, "Bad Request", http.StatusBadRequest)
        return
    }
    pkg := path.Join(r.Host, r.URL.Path)

    log.Printf("get %s", pkg)

    meta := pkgs[pkg]
    if meta == nil {
        for _, m := range pkgs {
            if m.re != nil && m.re.MatchString(pkg) {
                meta = m
                break
            }
        }
    }
    if meta == nil {
        http.Error(w, "Not Found", http.StatusNotFound)
        return
    }
    metatpl.Execute(w, meta)
}

func loadmeta(filename string) error {
    data, err := ioutil.ReadFile(filename)
    if err != nil {
        return err
    }
    var allmeta []*Meta
    err = json.Unmarshal(data, &allmeta)
    if err != nil {
        return err
    }
    pkgs = make(map[string]*Meta, len(allmeta))
    for _, m := range allmeta {
        if m.Pattern != "" {
            m.re = regexp.MustCompile(m.Pattern)
        }
        pkgs[m.Pkg] = m
    }
    return nil
}

func main() {
    err := loadmeta("./meta.json")
    if err != nil {
        log.Fatalln(err)
    }
    err = http.ListenAndServeTLS(":443", "./cart.pem", "./key.pri", http.HandlerFunc(getpkg))
    if err != nil {
        log.Fatalln(err)
    }
}

具体的输出内容,我是参考 curl 从 golang.org 拉取的结果调整的,问题不大,go https 服务端需要的证书和私钥文件生成是可以参考 Tony Bai 的文章

实际应用

我用这个服务代理所有对 golang.org/x/tools 的 get 请求,配置如下

[
    {
        "pattern": "golang.org/x/tools/.*",
        "pkg": "golang.org/x/tools",
        "vcs": "git",
        "repo": "https://github.com/golang/tools",
        "source": "https://github.com/golang/tools/",
        "sourcedir": "https://github.com/golang/tools/tree/master{/dir}",
        "sourceline": "https://github.com/golang/tools/blob/master{/dir}/{file}#L{line}"
    }
]

过程中遇到了几个问题,也是 Tony Bai 文章中提到的。

  1. bad certificate

    这个问题比较好解决,生成的证书 Common Name 不匹配,直接修改。

  2. unknown certificate authority

    这个比较难解决,好在 go get 提供了一个 -insecure 的命令允许 tls 建立连接过程跳过对证书的 CA 的校验。但这个做法只能在手动操作下比较好操作,如果走集成的命令,比如 vim-go 的 GoUpdateBinaries 命令的话,就要去改 vim-go 的 plugin 源码了。其实还有一种方式,如果只是我们私人使用的话,直接让系统 CA 信任我们自签的证书就完全没毛病了。(或者找黑心证书商买,大雾

  3. 多域名
    网上文章好好找找,问题不大。

标签: none

添加新评论