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,内容格式可能是这样的

1
2
3
4
5
<html>
<head>
<meta name="go-import" content="PREFIX VCS REPOROOT" />
</head>
</html>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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 请求,配置如下

1
2
3
4
5
6
7
8
9
10
11
[
{
"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. 多域名
    网上文章好好找找,问题不大。