You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
goproxy/main.go

278 lines
7.4 KiB

// Copyright 2019 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.
// Usage:
//
// goproxy [-listen [host]:port] [-cacheDir /tmp]
//
// goproxy serves the Go module proxy HTTP protocol at the given address (default 0.0.0.0:8081).
// It invokes the local go command to answer requests and therefore reuses
// the current GOPATH's module download cache and configuration (GOPROXY, GOSUMDB, and so on).
//
// While the proxy is running, setting GOPROXY=http://host:port will instruct the go command to use it.
// Note that the module proxy cannot share a GOPATH with its own clients or else fetches will deadlock.
// (The client will lock the entry as “being downloaded” before sending the request to the proxy,
// which will then wait for the apparently-in-progress download to finish.)
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/goproxyio/goproxy/v2/proxy"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/mod/module"
)
var downloadRoot string
var listen, promListen string
var cacheDir string
var proxyHost string
var excludeHost string
var cacheExpire time.Duration
func init() {
flag.StringVar(&excludeHost, "exclude", "", "exclude host pattern, you can exclude internal Git services")
flag.StringVar(&proxyHost, "proxy", "", "next hop proxy for Go Modules, recommend use https://goproxy.io")
flag.StringVar(&cacheDir, "cacheDir", "", "Go Modules cache dir, default is $GOPATH/pkg/mod/cache/download")
flag.StringVar(&listen, "listen", "0.0.0.0:8081", "service listen address")
flag.DurationVar(&cacheExpire, "cacheExpire", 5*time.Minute, "Go Modules cache expiration (min), default is 5 min")
flag.Parse()
if os.Getenv("GIT_TERMINAL_PROMPT") == "" {
os.Setenv("GIT_TERMINAL_PROMPT", "0")
}
if os.Getenv("GIT_SSH") == "" && os.Getenv("GIT_SSH_COMMAND") == "" {
os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no")
}
if excludeHost != "" {
os.Setenv("GOPRIVATE", excludeHost)
}
// Enable Go module
os.Setenv("GO111MODULE", "on")
os.Setenv("GOPROXY", "direct")
os.Setenv("GOSUMDB", "off")
downloadRoot = getDownloadRoot()
}
func main() {
log.SetPrefix("goproxy.io: ")
log.SetFlags(0)
var handle http.Handler
if proxyHost != "" {
log.Printf("ProxyHost %s\n", proxyHost)
if excludeHost != "" {
log.Printf("ExcludeHost %s\n", excludeHost)
}
handle = &logger{proxy.NewRouter(proxy.NewServer(new(ops)), &proxy.RouterOptions{
Pattern: excludeHost,
Proxy: proxyHost,
DownloadRoot: downloadRoot,
CacheExpire: cacheExpire,
})}
} else {
handle = &logger{proxy.NewServer(new(ops))}
}
server := &http.Server{Addr: listen, Handler: handle}
go func() {
if err := server.ListenAndServe(); err != nil {
if err != http.ErrServerClosed {
log.Fatal(err)
}
}
}()
s := make(chan os.Signal, 1)
signal.Notify(s, os.Interrupt, syscall.SIGTERM)
<-s
log.Println("Making a graceful shutdown...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := server.Shutdown(ctx)
if err != nil {
log.Fatalf("Error while shutting down the server: %v", err)
}
log.Println("Successful server shutdown.")
}
func getDownloadRoot() string {
var env struct {
GOPATH string
}
if cacheDir != "" {
os.Setenv("GOMODCACHE", filepath.Join(cacheDir, "pkg", "mod"))
return filepath.Join(cacheDir, "pkg", "mod", "cache", "download")
}
if err := goJSON(&env, "go", "env", "-json", "GOPATH"); err != nil {
log.Fatal(err)
}
list := filepath.SplitList(env.GOPATH)
if len(list) == 0 || list[0] == "" {
log.Fatalf("missing $GOPATH")
}
os.Setenv("GOMODCACHE", filepath.Join(list[0], "pkg", "mod"))
return filepath.Join(list[0], "pkg", "mod", "cache", "download")
}
// goJSON runs the go command and parses its JSON output into dst.
func goJSON(dst interface{}, command ...string) error {
cmd := exec.Command(command[0], command[1:]...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s:\n%s%s", strings.Join(command, " "), stderr.String(), stdout.String())
}
if err := json.Unmarshal(stdout.Bytes(), dst); err != nil {
return fmt.Errorf("%s: reading json: %v", strings.Join(command, " "), err)
}
return nil
}
// A logger is an http.Handler that logs traffic to standard error.
type logger struct {
h http.Handler
}
type responseLogger struct {
code int
http.ResponseWriter
}
// WriteHeader writes header code into responser writer.
func (r *responseLogger) WriteHeader(code int) {
r.code = code
r.ResponseWriter.WriteHeader(code)
}
// ServeHTTP implements http handler.
func (l *logger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Prometheus metrics
if r.URL.Path == "/metrics" {
promhttp.Handler().ServeHTTP(w, r)
return
}
start := time.Now()
rl := &responseLogger{code: 200, ResponseWriter: w}
l.h.ServeHTTP(rl, r)
log.Printf("%.3fs %d %s\n", time.Since(start).Seconds(), rl.code, r.URL)
}
// An ops is a proxy.ServerOps implementation.
type ops struct{}
// NewContext creates a context.
func (*ops) NewContext(r *http.Request) (context.Context, error) {
return context.Background(), nil
}
// List lists proxy files.
func (*ops) List(ctx context.Context, mpath string) (proxy.File, error) {
escMod, err := module.EscapePath(mpath)
if err != nil {
return nil, err
}
file := filepath.Join(downloadRoot, escMod, "@v", "list")
if info, err := os.Stat(file); err == nil && time.Since(info.ModTime()) < cacheExpire {
return os.Open(file)
}
var list struct {
Path string
Versions []string
}
if err := goJSON(&list, "go", "list", "-m", "-json", "-versions", mpath+"@latest"); err != nil {
return nil, err
}
if list.Path != mpath {
return nil, fmt.Errorf("go list -m: asked for %s but got %s", mpath, list.Path)
}
data := []byte(strings.Join(list.Versions, "\n") + "\n")
if len(data) == 1 {
data = nil
}
err = os.MkdirAll(path.Dir(file), os.ModePerm)
if err != nil {
log.Printf("make cache dir failed, err: %v.", err)
return nil, err
}
if err := ioutil.WriteFile(file, data, 0666); err != nil {
return nil, err
}
return os.Open(file)
}
// Latest fetches latest file.
func (*ops) Latest(ctx context.Context, path string) (proxy.File, error) {
d, err := download(module.Version{Path: path, Version: "latest"})
if err != nil {
return nil, err
}
return os.Open(d.Info)
}
// Info fetches info file.
func (*ops) Info(ctx context.Context, m module.Version) (proxy.File, error) {
d, err := download(m)
if err != nil {
return nil, err
}
return os.Open(d.Info)
}
// GoMod fetches go mod file.
func (*ops) GoMod(ctx context.Context, m module.Version) (proxy.File, error) {
d, err := download(m)
if err != nil {
return nil, err
}
return os.Open(d.GoMod)
}
// Zip fetches zip file.
func (*ops) Zip(ctx context.Context, m module.Version) (proxy.File, error) {
d, err := download(m)
if err != nil {
return nil, err
}
return os.Open(d.Zip)
}
type downloadInfo struct {
Path string
Version string
Info string
GoMod string
Zip string
Dir string
Sum string
GoModSum string
}
func download(m module.Version) (*downloadInfo, error) {
d := new(downloadInfo)
return d, goJSON(d, "go", "mod", "download", "-json", m.String())
}