mirror of https://github.com/goproxyio/goproxy
refactor handlers
parent
3d49ac1020
commit
b259869435
|
@ -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
|
||||||
|
|
|
@ -3,7 +3,3 @@ go_repos/*
|
||||||
cacheDir/*
|
cacheDir/*
|
||||||
.idea/*
|
.idea/*
|
||||||
bin/*
|
bin/*
|
||||||
|
|
||||||
internal/
|
|
||||||
pkg/*
|
|
||||||
!pkg/proxy
|
|
||||||
|
|
11
Makefile
11
Makefile
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
2
go.mod
|
@ -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
|
||||||
|
|
|
@ -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
224
main.go
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue