refactor handlers

pull/89/head
kun 2019-07-25 16:10:52 +08:00
parent 3d49ac1020
commit b259869435
13 changed files with 427 additions and 702 deletions

View File

@ -5,13 +5,13 @@ version: 2
jobs: jobs:
build: build:
docker: docker:
- image: circleci/golang:1.11 - image: circleci/golang:1.12
working_directory: /go/src/github.com/goproxyio/goproxy/ working_directory: /go/src/github.com/goproxyio/goproxy/
steps: steps:
- checkout - checkout
- run: - run:
name: install deps name: install deps
command: make generate command: make tidy
- run: - run:
name: test self name: test self
command: make test command: make test

4
.gitignore vendored
View File

@ -3,7 +3,3 @@ go_repos/*
cacheDir/* cacheDir/*
.idea/* .idea/*
bin/* bin/*
internal/
pkg/*
!pkg/proxy

View File

@ -1,21 +1,20 @@
.PHONY: build generate image clean test .PHONY: build image clean test
export GO111MODULE=on export GO111MODULE=on
all: build all: build
build: generate build: tidy
@go build -o bin/goproxy -ldflags "-s -w" . @go build -o bin/goproxy -ldflags "-s -w" .
generate: tidy:
@go generate
@go mod tidy @go mod tidy
image: image:
@docker build -t goproxy/goproxy . @docker build -t goproxy/goproxy .
test: generate test: tidy
@go test -v `(go list ./... | grep "pkg/proxy")` @go test -v ./...
clean: clean:
@git clean -f -d -X @git clean -f -d -X

View File

@ -3,12 +3,15 @@
A global proxy for go modules. see: [https://goproxy.io](https://goproxy.io) A global proxy for go modules. see: [https://goproxy.io](https://goproxy.io)
## Requirements
It invokes the local go command to answer requests.
## Build ## Build
make make
## Started ## Started
./goproxy -listen=0.0.0.0:80 -cacheDir=/data ./bin/goproxy -listen=0.0.0.0:80 -cacheDir=/tmp/test
## Use docker image ## Use docker image

View File

@ -1,44 +0,0 @@
#!/bin/sh
PKG="${PWD}/internal"
GOROOT="$(go env GOROOT)"
GOVERSION=`go version | awk -F ' ' '{print $3}'|grep '1.12'`
GTGO12=0
echo "ENV GOLANG VER: $GOVERSION"
if [ "$GOVERSION" != "" ]
then
GTGO12=1
echo "use 1.12 mode"
else
echo "use 1.11 mode"
fi
mkdir -p "${PKG}"
cp -r "${GOROOT}/src/cmd/go/internal/"* "${PKG}"
cp -r "${GOROOT}/src/cmd/internal/browser" "${PKG}"
cp -r "${GOROOT}/src/cmd/internal/buildid" "${PKG}"
cp -r "${GOROOT}/src/cmd/internal/objabi" "${PKG}"
cp -r "${GOROOT}/src/cmd/internal/test2json" "${PKG}"
cp -r "${GOROOT}/src/internal/singleflight" "${PKG}"
cp -r "${GOROOT}/src/internal/testenv" "${PKG}"
if [ "$GTGO12" = "1" ]
then
cp -r "${GOROOT}/src/internal/xcoff" "${PKG}"
cp -r "${GOROOT}/src/internal/goroot" "${PKG}"
cp -r "${GOROOT}/src/cmd/internal/sys" "${PKG}"
fi
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/cmd\/go\/internal/github.com\/goproxyio\/goproxy\/internal/g' {} +
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/cmd\/internal/github.com\/goproxyio\/goproxy\/internal/g' {} +
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/internal\/singleflight/github.com\/goproxyio\/goproxy\/internal\/singleflight/g' {} +
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/internal\/testenv/github.com\/goproxyio\/goproxy\/internal\/testenv/g' {} +
if [ "$GTGO12" = "1" ]
then
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/internal\/goroot/github.com\/goproxyio\/goproxy\/internal\/goroot/g' {} +
find "${PKG}" -type f -name '*.go' -exec sed -i -e 's/internal\/xcoff/github.com\/goproxyio\/goproxy\/internal\/xcoff/g' {} +
fi

2
go.mod
View File

@ -1,3 +1,5 @@
module github.com/goproxyio/goproxy module github.com/goproxyio/goproxy
go 1.12 go 1.12
require golang.org/x/mod v0.1.0

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

224
main.go
View File

@ -1,94 +1,194 @@
//go:generate ./build/generate.sh // 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 package main
import ( import (
"bytes"
"context" "context"
"encoding/json"
"flag" "flag"
"fmt"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall"
"time" "time"
"github.com/goproxyio/goproxy/pkg/proxy" "github.com/goproxyio/goproxy/proxy"
"golang.org/x/mod/module"
) )
var downloadRoot string
const listExpire = 5 * time.Minute
var listen string var listen string
var cacheDir string var cacheDir string
func init() { func init() {
log.SetOutput(os.Stdout)
flag.StringVar(&cacheDir, "cacheDir", "", "go modules cache dir") flag.StringVar(&cacheDir, "cacheDir", "", "go modules cache dir")
flag.StringVar(&listen, "listen", "0.0.0.0:8081", "service listen address") flag.StringVar(&listen, "listen", "0.0.0.0:8081", "service listen address")
flag.Parse() flag.Parse()
isGitValid := checkGitVersion()
if !isGitValid {
log.Fatal("Error in git version, please check your git installed in local, must be great 2.0")
}
}
func checkGitVersion() bool { if os.Getenv("GIT_TERMINAL_PROMPT") == "" {
var err error os.Setenv("GIT_TERMINAL_PROMPT", "0")
var ret []byte
cmd := exec.Command("git", "version")
if ret, err = cmd.Output(); err != nil {
return false
} }
if strings.HasPrefix(string(ret), "git version 2") {
return true if os.Getenv("GIT_SSH") == "" && os.Getenv("GIT_SSH_COMMAND") == "" {
os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no")
} }
return false
} }
func main() { func main() {
errCh := make(chan error) log.SetPrefix("goproxy.io: ")
log.SetFlags(0)
log.Printf("goproxy: %s inited. listen on %s\n", time.Now().Format("2006-01-02 15:04:05"), listen) // TODO flags
var env struct {
if cacheDir == "" { GOPATH string
cacheDir = "/go"
gpEnv := os.Getenv("GOPATH")
if gpEnv != "" {
gp := filepath.SplitList(gpEnv)
if gp[0] != "" {
cacheDir = gp[0]
}
}
} }
fullCacheDir := filepath.Join(cacheDir, "pkg", "mod", "cache", "download") if err := goJSON(&env, "go", "env", "-json", "GOPATH"); err != nil {
if _, err := os.Stat(fullCacheDir); os.IsNotExist(err) {
log.Printf("goproxy: cache dir %s is not exist. To create it.\n", fullCacheDir)
if err := os.MkdirAll(fullCacheDir, 0755); err != nil {
log.Fatalf("goproxy: make cache dir failed: %s", err)
}
}
server := http.Server{
Addr: listen,
Handler: proxy.NewProxy(cacheDir),
}
go func() {
err := server.ListenAndServe()
if err != nil {
errCh <- err
}
}()
signCh := make(chan os.Signal)
signal.Notify(signCh, os.Interrupt, syscall.SIGTERM)
select {
case err := <-errCh:
log.Fatal(err) log.Fatal(err)
case sign := <-signCh:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = server.Shutdown(ctx)
log.Printf("goproxy: Server gracefully %s", sign)
} }
list := filepath.SplitList(env.GOPATH)
if len(list) == 0 || list[0] == "" {
log.Fatalf("missing $GOPATH")
}
downloadRoot = filepath.Join(list[0], "pkg/mod/cache/download")
if cacheDir != "" {
downloadRoot = filepath.Join(cacheDir, "pkg/mod/cache/download")
os.Setenv("GOPATH", cacheDir)
}
log.Fatal(http.ListenAndServe(listen, &logger{proxy.NewServer(new(ops))}))
}
// 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
}
func (r *responseLogger) WriteHeader(code int) {
r.code = code
r.ResponseWriter.WriteHeader(code)
}
func (l *logger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(os.Stderr, "------ --- %s\n", r.URL)
start := time.Now()
rl := &responseLogger{code: 200, ResponseWriter: w}
l.h.ServeHTTP(rl, r)
fmt.Fprintf(os.Stderr, "%.3fs %d %s\n", time.Since(start).Seconds(), rl.code, r.URL)
}
// An ops is a proxy.ServerOps implementation.
type ops struct{}
func (*ops) NewContext(r *http.Request) (context.Context, error) {
return context.Background(), nil
}
func (*ops) List(ctx context.Context, path string) (proxy.File, error) {
escMod, err := module.EscapePath(path)
if err != nil {
return nil, err
}
file := filepath.Join(downloadRoot, escMod+"/@v/listproxy")
if info, err := os.Stat(file); err == nil && time.Since(info.ModTime()) < listExpire {
return os.Open(file)
}
var list struct {
Path string
Versions []string
}
if err := goJSON(&list, "go", "list", "-m", "-json", "-versions", path+"@latest"); err != nil {
return nil, err
}
if list.Path != path {
return nil, fmt.Errorf("go list -m: asked for %s but got %s", path, list.Path)
}
data := []byte(strings.Join(list.Versions, "\n") + "\n")
if len(data) == 1 {
data = nil
}
ioutil.WriteFile(file, data, 0666)
return os.Open(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)
}
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)
}
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)
}
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())
} }

View File

@ -1,199 +0,0 @@
package proxy
import (
"fmt"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/goproxyio/goproxy/internal/cfg"
"github.com/goproxyio/goproxy/internal/modfetch"
"github.com/goproxyio/goproxy/internal/modfetch/codehost"
"github.com/goproxyio/goproxy/internal/modload"
"github.com/goproxyio/goproxy/internal/module"
)
var cacheDir string
var innerHandle http.Handler
type modInfo struct {
module.Version
suf string
}
func setupEnv(basedir string) {
modfetch.QuietLookup = true // just to hide modfetch/cache.go#127
modfetch.PkgMod = filepath.Join(basedir, "pkg", "mod")
codehost.WorkRoot = filepath.Join(modfetch.PkgMod, "cache", "vcs")
cfg.CmdName = "mod download" // just to hide modfetch/fetch.go#L87
}
func NewProxy(cache string) http.Handler {
setupEnv(cache)
cacheDir = filepath.Join(modfetch.PkgMod, "cache", "download")
innerHandle = http.FileServer(http.Dir(cacheDir))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("goproxy: %s request %s\n", r.RemoteAddr, r.URL.Path)
info, err := parseModInfoFromUrl(r.URL.Path)
if err != nil {
innerHandle.ServeHTTP(w, r)
return
}
switch suf := info.suf; suf {
case ".info", ".mod", ".zip":
{
if _, err := os.Stat(filepath.Join(cacheDir, r.URL.Path)); err == nil {
// cache files exist on disk
innerHandle.ServeHTTP(w, r)
return
}
realMod, err := getQuery(info.Version.Path, info.Version.Version)
if err != nil {
errLogger.Printf("goproxy: lookup %s@%s get err %s", info.Path, info.Version.Version, err)
ReturnNotFound(w, err)
return
}
if realMod.Path != info.Version.Path {
log.Printf("goproxy: mod %s@%s may have subpath, just return to make client recurse", info.Path, info.Version.Version)
ReturnSuccess(w, nil)
return
}
switch suf {
case ".info":
{
if revInfo, err := modfetch.Stat(realMod.Path, realMod.Version); err != nil {
// use Stat instead of InfoFile, because when query-version is master, no infoFile here, maybe bug of go
// TODO(hxzhao527): check whether InfoFile have a bug?
errLogger.Printf("goproxy: fetch info %s@%s get err %s", info.Path, info.Version.Version, err)
ReturnNotFound(w, err)
} else {
ReturnJsonData(w, revInfo)
}
}
case ".mod":
{
if modFile, err := modfetch.GoModFile(realMod.Path, realMod.Version); err != nil {
errLogger.Printf("goproxy: fetch modfile %s@%s get err %s", info.Path, info.Version.Version, err)
ReturnNotFound(w, err)
} else {
http.ServeFile(w, r, modFile)
}
}
case ".zip":
{
mod := module.Version{Path: realMod.Path, Version: realMod.Version}
if zipFile, err := modfetch.DownloadZip(mod); err != nil {
errLogger.Printf("goproxy: download zip %s@%s get err %s", info.Path, info.Version.Version, err)
ReturnNotFound(w, err)
} else {
http.ServeFile(w, r, zipFile)
}
}
}
return
}
case "/@v/list", "/@latest":
{
repo, err := modfetch.Lookup(info.Path)
if err != nil {
ReturnNotFound(w, err)
return
}
switch suf {
case "/@v/list":
modPath := strings.Trim(strings.TrimSuffix(r.URL.Path, "/@v/list"), "/")
modPath, err := module.DecodePath(modPath)
if err != nil {
ReturnNotFound(w, err)
return
}
modload.LoadBuildList()
mods := modload.ListModules([]string{modPath + "@latest"}, false, true)
data := []byte(strings.Join(mods[0].Versions, "\n") + "\n")
if len(data) == 1 {
data = nil
}
w.Write(data)
return
case "/@latest":
rev, err := repo.Stat("latest")
if err != nil {
ReturnNotFound(w, err)
return
}
ReturnJsonData(w, rev)
return
}
return
}
}
})
}
func parseModInfoFromUrl(url string) (*modInfo, error) {
var modPath, modVersion, suf string
var err error
switch {
case strings.HasSuffix(url, "/@v/list"):
// /golang.org/x/net/@v/list
suf = "/@v/list"
modVersion = ""
modPath = strings.Trim(strings.TrimSuffix(url, suf), "/")
case strings.HasSuffix(url, "/@latest"):
// /golang.org/x/@latest
suf = "/@latest"
modVersion = "latest"
modPath = strings.Trim(strings.TrimSuffix(url, suf), "/")
case strings.HasSuffix(url, ".info"), strings.HasSuffix(url, ".mod"), strings.HasSuffix(url, ".zip"):
// /golang.org/x/net/@v/v0.0.0-20181220203305-927f97764cc3.info
// /golang.org/x/net/@v/v0.0.0-20181220203305-927f97764cc3.mod
// /golang.org/x/net/@v/v0.0.0-20181220203305-927f97764cc3.zip
suf = path.Ext(url)
tmp := strings.Split(url, "/@v/")
if len(tmp) != 2 {
return nil, fmt.Errorf("bad module path:%s", url)
}
modPath = strings.Trim(tmp[0], "/")
modVersion = strings.TrimSuffix(tmp[1], suf)
modVersion, err = module.DecodeVersion(modVersion)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("bad module path:%s", url)
}
// decode path & version, next proxy and source need
modPath, err = module.DecodePath(modPath)
if err != nil {
return nil, err
}
return &modInfo{module.Version{Path: modPath, Version: modVersion}, suf}, nil
}
// getQuery evaluates the given package path, version pair
// to determine the underlying module version being requested.
// If forceModulePath is set, getQuery must interpret path
// as a module path.
func getQuery(path, vers string) (module.Version, error) {
// First choice is always to assume path is a module path.
// If that works out, we're done.
info, err := modload.Query(path, vers, modload.Allowed)
if err == nil {
return module.Version{Path: path, Version: info.Version}, nil
}
// Otherwise, try a package path.
m, _, err := modload.QueryPackage(path, vers, modload.Allowed)
return m, err
}

View File

@ -1,338 +0,0 @@
package proxy
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/goproxyio/goproxy/internal/modfetch"
"github.com/goproxyio/goproxy/internal/module"
"github.com/goproxyio/goproxy/internal/testenv"
)
var _handle http.Handler
func TestMain(m *testing.M) {
tmpdir, err := ioutil.TempDir("", "goproxy-test-")
if err != nil {
log.Fatalf("init tmpdir failed: %s", err)
}
defer os.RemoveAll(tmpdir)
_handle = NewProxy(tmpdir)
m.Run()
}
var _modInfoTests = []struct {
path string
query string // query
version string // want
latest bool
time time.Time
gomod string
zip []string
}{
{
path: "gopkg.in/check.v1",
query: "v0.0.0-20161208181325-20d25e280405",
version: "v0.0.0-20161208181325-20d25e280405",
time: time.Date(2016, 12, 8, 18, 13, 25, 0, time.UTC),
gomod: "module gopkg.in/check.v1\n",
zip: []string{
".gitignore",
".travis.yml",
"LICENSE",
"README.md",
"TODO",
"benchmark.go",
"benchmark_test.go",
"bootstrap_test.go",
"check.go",
"check_test.go",
"checkers.go",
"checkers_test.go",
"export_test.go",
"fixture_test.go",
"foundation_test.go",
"helpers.go",
"helpers_test.go",
"printer.go",
"printer_test.go",
"reporter.go",
"reporter_test.go",
"run.go",
"run_test.go",
},
},
{
path: "github.com/PuerkitoBio/goquery",
query: "v0.0.0-20181014175806-2af3d16e2bb8",
version: "v0.0.0-20181014175806-2af3d16e2bb8",
time: time.Date(2018, 10, 14, 17, 58, 6, 0, time.UTC),
gomod: "module github.com/PuerkitoBio/goquery\n",
zip: []string{
".gitattributes",
".gitignore",
".travis.yml",
"LICENSE",
"README.md",
"array.go",
"array_test.go",
"bench/v0.1.0",
"bench/v0.1.1",
"bench/v0.1.1-v0.2.1-go1.1rc1.svg",
"bench/v0.2.0",
"bench/v0.2.0-v0.2.1-go1.1rc1.svg",
"bench/v0.2.1-go1.1rc1",
"bench/v0.3.0",
"bench/v0.3.2-go1.2",
"bench/v0.3.2-go1.2-take2",
"bench/v0.3.2-go1.2rc1",
"bench/v1.0.0-go1.7",
"bench/v1.0.1a-go1.7",
"bench/v1.0.1b-go1.7",
"bench/v1.0.1c-go1.7",
"bench_array_test.go",
"bench_example_test.go",
"bench_expand_test.go",
"bench_filter_test.go",
"bench_iteration_test.go",
"bench_property_test.go",
"bench_query_test.go",
"bench_traversal_test.go",
"doc.go",
"doc/tips.md",
"example_test.go",
"expand.go",
"expand_test.go",
"filter.go",
"filter_test.go",
"iteration.go",
"iteration_test.go",
"manipulation.go",
"manipulation_test.go",
"misc/git/pre-commit",
"property.go",
"property_test.go",
"query.go",
"query_test.go",
"testdata/gotesting.html",
"testdata/gowiki.html",
"testdata/metalreview.html",
"testdata/page.html",
"testdata/page2.html",
"testdata/page3.html",
"traversal.go",
"traversal_test.go",
"type.go",
"type_test.go",
"utilities.go",
"utilities_test.go",
},
},
{
path: "github.com/rsc/vgotest1",
query: "v0.0.0-20180219223237-a08abb797a67",
version: "v0.0.0-20180219223237-a08abb797a67",
latest: true,
time: time.Date(2018, 02, 19, 22, 32, 37, 0, time.UTC),
},
{
path: "github.com/hxzhao527/legacytest",
query: "master",
version: "v2.0.1+incompatible",
time: time.Date(2018, 07, 17, 16, 42, 53, 0, time.UTC),
gomod: "module github.com/hxzhao527/legacytest\n",
zip: []string{
"x.go",
},
},
{
path: "github.com/micro/go-api/resolver",
query: "v0.5.0",
version: "v0.5.0",
gomod: "module github.com/micro/go-api\n",
},
}
var _modListTests = []struct {
path string
versions []string
}{
{
path: "github.com/rsc/vgotest1",
versions: []string{"v0.0.0", "v0.0.1", "v1.0.0", "v1.0.1", "v1.0.2", "v1.0.3", "v1.1.0", "v2.0.0+incompatible"},
},
}
func TestFetchInfo(t *testing.T) {
testenv.MustHaveExternalNetwork(t)
for _, mod := range _modInfoTests {
req := buildRequest(mod.path, mod.query, ".info")
rr, err := basicCheck(req)
if err != nil {
t.Error(err)
continue
}
// check return data
info := new(modfetch.RevInfo)
if err := json.Unmarshal(rr.Body.Bytes(), info); err != nil {
t.Errorf("package info is not recognized")
continue
}
if mod.version != info.Version {
t.Errorf("info.Version = %s, want %s", info.Version, mod.version)
}
if !mod.time.Equal(info.Time) {
t.Errorf("info.Time = %v, want %v", info.Time, mod.time)
}
}
}
func TestFetchModFile(t *testing.T) {
testenv.MustHaveExternalNetwork(t)
for _, mod := range _modInfoTests {
if len(mod.gomod) == 0 {
continue
}
req := buildRequest(mod.path, mod.query, ".mod")
rr, err := basicCheck(req)
if err != nil {
t.Error(err)
continue
}
if data := rr.Body.String(); data != mod.gomod {
t.Errorf("repo.GoMod(%q) = %q, want %q", mod.version, data, mod.gomod)
}
}
}
func TestFetchZip(t *testing.T) {
testenv.MustHaveExternalNetwork(t)
for _, mod := range _modInfoTests {
if len(mod.zip) == 0 {
continue
}
req := buildRequest(mod.path, mod.query, ".zip")
rr, err := basicCheck(req)
if err != nil {
t.Error(err)
continue
}
prefix := mod.path + "@" + mod.version + "/"
var names []string
data := rr.Body.Bytes() // ??
z, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
t.Errorf("open %s's zip failed: %v", mod.path, err)
continue
}
for _, file := range z.File {
if !strings.HasPrefix(file.Name, prefix) {
t.Errorf("zip entry %v does not start with prefix %v", file.Name, prefix)
continue
}
names = append(names, file.Name[len(prefix):])
}
if !reflect.DeepEqual(names, mod.zip) {
t.Errorf("zip = %v\nwant %v\n", names, mod.zip)
}
}
}
func TestLatest(t *testing.T) {
testenv.MustHaveExternalNetwork(t)
for _, mod := range _modInfoTests {
if !mod.latest {
continue
}
req := buildRequest(mod.path, "latest", "")
rr, err := basicCheck(req)
if err != nil {
t.Error(err)
continue
}
info := new(modfetch.RevInfo)
if err := json.Unmarshal(rr.Body.Bytes(), info); err != nil {
t.Errorf("package info is not recognized")
continue
}
if mod.version != info.Version {
t.Errorf("info.Version = %s, want %s", info.Version, mod.version)
}
if !mod.time.Equal(info.Time) {
t.Errorf("info.Time = %v, want %v", info.Time, mod.time)
}
}
}
func TestList(t *testing.T) {
for _, mod := range _modListTests {
req := buildRequest(mod.path, "", "")
rr, err := basicCheck(req)
if err != nil {
t.Error(err)
continue
}
modfetch.SortVersions(mod.versions)
if data := rr.Body.String(); strings.Join(mod.versions, "\n") != data {
t.Errorf("list not well,\n expected: %v\n, got: %v", mod.versions, strings.Split(data, "\n"))
}
}
}
func buildRequest(modPath, modVersion string, ext string) *http.Request {
modPath, _ = module.EncodePath(modPath)
modVersion, _ = module.EncodeVersion(modVersion)
url := "/" + modPath
switch modVersion {
case "":
url += "/@v/list"
case "latest":
url += "/@latest"
default:
url = url + "/@v/" + modVersion + ext
}
req, _ := http.NewRequest("GET", url, nil)
return req
}
func basicCheck(req *http.Request) (*httptest.ResponseRecorder, error) {
rr := httptest.NewRecorder()
_handle.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
return nil, fmt.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
return rr, nil
}

View File

@ -1,46 +0,0 @@
package proxy
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
var errLogger = log.New(os.Stderr, "", log.LstdFlags)
func ReturnInternalServerError(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusInternalServerError)
msg := fmt.Sprintf("%v", err)
errLogger.Printf("goproxy: %s\n", msg)
_, _ = w.Write([]byte(msg))
}
func ReturnBadRequest(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusBadRequest)
msg := fmt.Sprintf("%v", err)
errLogger.Printf("goproxy: %s\n", msg)
_, _ = w.Write([]byte(msg))
}
func ReturnNotFound(w http.ResponseWriter, err error) {
w.WriteHeader(http.StatusNotFound)
msg := fmt.Sprintf("%v", err)
errLogger.Printf("goproxy: %s\n", msg)
_, _ = w.Write([]byte(msg))
}
func ReturnSuccess(w http.ResponseWriter, data []byte) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write(data)
}
func ReturnJsonData(w http.ResponseWriter, data interface{}) {
js, err := json.Marshal(data)
if err != nil {
ReturnNotFound(w, err)
} else {
ReturnSuccess(w, js)
}
}

243
proxy/server.go Normal file
View File

@ -0,0 +1,243 @@
// 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.
// Package proxy implements the HTTP protocols for serving a Go module proxy.
package proxy
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"os"
"path"
"strings"
"time"
"golang.org/x/mod/module"
)
// A ServerOps provides the external operations
// (accessing module information and so on) needed by the Server.
type ServerOps interface {
// NewContext returns the context to use for the request r.
NewContext(r *http.Request) (context.Context, error)
// List, Latest, Info, GoMod, and Zip all return a File to be sent to a client.
// The File will be closed after its contents are sent.
// In the case of an error, if the error satisfies errors.Is(err, os.ErrNotFound),
// the server responds with an HTTP 404 error;
// otherwise it responds with an HTTP 500 error.
// List returns a list of tagged versions of the module identified by path.
// The versions should all be canonical semantic versions
// and formatted in a text listing, one per line.
// Pseudo-versions derived from untagged commits should be omitted.
// The go command exposes this list in 'go list -m -versions' output
// and also uses it to resolve wildcards like 'go get m@v1.2'.
List(ctx context.Context, path string) (File, error)
// Latest returns an info file for the latest known version of the module identified by path.
// The go command uses this for 'go get m' or 'go get m@latest'
// but only after finding no suitable version among the ones returned by List.
// Typically, Latest should return a pseudo-version for the latest known commit.
Latest(ctx context.Context, path string) (File, error)
// Info opens and returns the module version's info file.
// The requested version can be a canonical semantic version
// but can also be an arbitrary version reference, like "master".
//
// The metadata in the returned file should be a JSON object corresponding
// to the Go type
//
// type Info struct {
// Version string
// Time time.Time
// }
//
// where the version is the resolved canonical semantic version
// and the time is the commit or publication time of that version
// (for use with go list -m).
// The NewInfo function can be used to construct an info File.
//
// Proxies should obtain the module version information by
// executing 'go mod download -json' and caching the file
// listed in the Info field.
Info(ctx context.Context, m module.Version) (File, error)
// GoMod opens and returns the module's go.mod file.
// The requested version is a canonical semantic version.
//
// Proxies should obtain the module version information by
// executing 'go mod download -json' and caching the file
// listed in the GoMod field.
GoMod(ctx context.Context, m module.Version) (File, error)
// Zip opens and returns the module's zip file.
// The requested version is a canonical semantic version.
//
// Proxies should obtain the module version information by
// executing 'go mod download -json' and caching the file
// listed in the Zip field.
Zip(ctx context.Context, m module.Version) (File, error)
}
// A File is a file to be served, typically an *os.File or the result of calling MemFile or NewInfo.
// The modification time is the only necessary field in the Stat result.
type File interface {
io.Reader
io.Seeker
io.Closer
Stat() (os.FileInfo, error)
}
// A Server is the proxy HTTP server,
// which implements http.Handler and should be invoked
// to serve the paths listed in ServerPaths.
//
// The server assumes that the requests are made to the root of the URL space,
// so it should typically be registered using:
//
// srv := proxy.NewServer(ops)
// http.Handle("/", srv)
//
// To register a server at a subdirectory of the URL space, wrap the server in http.StripPrefix:
//
// srv := proxy.NewServer(ops)
// http.Handle("/proxy/", http.StripPrefix("/proxy", srv))
//
// All recognized requests to the server contain the substring "/@v/" in the URL.
// The server will respond with an http.StatusBadRequest (400) error to unrecognized requests.
type Server struct {
ops ServerOps
}
// NewServer returns a new Server using the given operations.
func NewServer(ops ServerOps) *Server {
return &Server{ops: ops}
}
// ServeHTTP is the server's implementation of http.Handler.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, err := s.ops.NewContext(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
i := strings.Index(r.URL.Path, "/@")
if i < 0 {
http.Error(w, "no such path", http.StatusNotFound)
return
}
modPath, err := module.UnescapePath(strings.TrimPrefix(r.URL.Path[:i], "/"))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
what := r.URL.Path[i+len("/@"):]
const (
contentTypeJSON = "application/json"
contentTypeText = "text/plain; charset=UTF-8"
contentTypeBinary = "application/octet-stream"
)
var ctype string
var f File
var openErr error
switch what {
case "latest":
ctype = contentTypeJSON
f, openErr = s.ops.Latest(ctx, modPath)
case "v/list":
ctype = contentTypeText
f, openErr = s.ops.List(ctx, modPath)
default:
what = strings.TrimPrefix(what, "v/")
ext := path.Ext(what)
vers, err := module.UnescapeVersion(strings.TrimSuffix(what, ext))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
m := module.Version{Path: modPath, Version: vers}
if vers == "latest" {
// The go command handles "go get m@latest" by fetching /m/@v/latest, not latest.info.
// We should never see requests for "latest.info" and so on, so avoid confusion
// by disallowing it early.
http.Error(w, "version latest is disallowed", http.StatusNotFound)
return
}
// All requests require canonical versions except for info,
// which accepts any revision identifier known to the underlying storage.
if ext != ".info" && vers != module.CanonicalVersion(vers) {
http.Error(w, "version "+vers+" is not in canonical form", http.StatusNotFound)
return
}
switch ext {
case ".info":
ctype = "application/json"
f, openErr = s.ops.Info(ctx, m)
case ".mod":
ctype = "text/plain; charset=UTF-8"
f, openErr = s.ops.GoMod(ctx, m)
case ".zip":
ctype = "application/octet-stream"
f, openErr = s.ops.Zip(ctx, m)
default:
http.Error(w, "request not recognized", http.StatusNotFound)
return
}
}
if openErr != nil {
code := http.StatusNotFound
http.Error(w, "not found", code)
return
}
defer f.Close()
info, err := f.Stat()
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if info.IsDir() {
http.Error(w, "unexpected directory", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", ctype)
http.ServeContent(w, r, what, info.ModTime(), f)
}
// MemFile returns an File containing the given in-memory content and modification time.
func MemFile(data []byte, t time.Time) File {
return &memFile{bytes.NewReader(data), memStat{t, int64(len(data))}}
}
type memFile struct {
*bytes.Reader
stat memStat
}
func (f *memFile) Close() error { return nil }
func (f *memFile) Stat() (os.FileInfo, error) { return &f.stat, nil }
func (f *memFile) Readdir(count int) ([]os.FileInfo, error) { return nil, os.ErrInvalid }
type memStat struct {
t time.Time
size int64
}
func (s *memStat) Name() string { return "memfile" }
func (s *memStat) Size() int64 { return s.size }
func (s *memStat) Mode() os.FileMode { return 0444 }
func (s *memStat) ModTime() time.Time { return s.t }
func (s *memStat) IsDir() bool { return false }
func (s *memStat) Sys() interface{} { return nil }
// NewInfo returns a formatted info file for the given version, time pair.
// The version should be a canonical semantic version.
func NewInfo(version string, t time.Time) File {
var info = struct {
Version string
Time time.Time
}{version, t}
js, err := json.Marshal(info)
if err != nil {
// json.Marshal only fails for bad types; there are no bad types in info.
panic("unexpected json.Marshal failure")
}
return MemFile(js, t)
}

View File

@ -4,3 +4,4 @@ github.com/micro/go-api/resolver@v0.5.0
cloud.google.com/go cloud.google.com/go
golang.org/x/tools/cmd/gopls golang.org/x/tools/cmd/gopls
golang.org/x/tools/cmd/guru@latest golang.org/x/tools/cmd/guru@latest
github.com/gorilla/mux@v1.7.3