refactor: pure go

pull/20/head
赵浩翔 2018-12-24 15:51:40 +08:00 committed by kun
parent 0ab15412ae
commit 18c8979b4c
13 changed files with 183 additions and 1076 deletions

View File

@ -10,6 +10,5 @@ jobs:
working_directory: /go/src/github.com/goproxyio/goproxy/
steps:
- checkout
# specify any bash command here prefixed with `run: `
- run: bash circle-test.sh

6
.gitignore vendored
View File

@ -1,4 +1,6 @@
goproxy
build/*
go_repos/*
.idea/*
.idea/*
pkg/*
!pkg/proxy

View File

@ -1,8 +1,3 @@
FROM golang:1.11
COPY ./ /goproxy
WORKDIR /goproxy
RUN go build
CMD ["/goproxy/goproxy","-listen=0.0.0.0:8080"]
FROM alpine:3.8
RUN apk add --no-cache git mercurial subversion bzr fossil
COPY bin/goproxy /usr/bin/goproxy

View File

@ -4,23 +4,24 @@
A global proxy for go modules. see: [https://goproxy.io](https://goproxy.io)
## Build
go generate
go build
## Started
./goproxy -listen=0.0.0.0:80
./goproxy -listen=0.0.0.0:80 -root=/ext
## Docker
docker run -it goproxyio/goproxy
docker run --name goproxy -d -p80:8081 goproxyio/goproxy
Use the -v flag to persisting the proxy module data (change ___go_repo___ to your own dir):
docker run -it -v go_repo:/go/pkg/mod/cache/download goproxyio/goproxy
docker run --name goproxy -d -p80:8081 -v go_repo:/ext goproxyio/goproxy
## Docker Compose
docker-compose up
## Appendix
1. set `$GOPROXY` to chain your proxy or disable the proxy

0
bin/.keep Normal file
View File

32
build/generate.sh Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
PKG=${PWD}/pkg/
cp -r ${GOROOT}/src/cmd/go/internal/base ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/cache ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/cfg ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/dirhash ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/get ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/load ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/modfetch ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/modfile ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/modinfo ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/module ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/search ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/par ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/semver ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/txtar ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/str ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/web ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/web2 ${PKG}
cp -r ${GOROOT}/src/cmd/go/internal/work ${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/internal/singleflight ${PKG}
find ${PWD}/pkg -type f -name '*.go' -exec sed -i 's/cmd\/go\/internal/github.com\/goproxyio\/goproxy\/pkg/g' {} +
find ${PWD}/pkg -type f -name '*.go' -exec sed -i 's/cmd\/internal/github.com\/goproxyio\/goproxy\/pkg/g' {} +
find ${PWD}/pkg -type f -name '*.go' -exec sed -i 's/internal/github.com\/goproxyio\/goproxy\/pkg/g' {} +

View File

@ -1,8 +1,7 @@
#!/bin/bash
export GO111MODULE=on
go mod download
go mod verify
go test -timeout 60s -v ./...
go generate
go mod tidy
# build
go build -v -mod readonly
go build -o bin/goproxy

View File

@ -1,10 +1,10 @@
version: "3"
version: "2"
services:
goproxy:
image: goproxyio/goproxy:latest
command: "goproxy -listen=0.0.0.0:8080 -root=/ext"
ports:
- "8080"
- "80:8080"
restart: always
volumes:
- ./go_repos:/go/pkg/mod/cache/download
- ./go_repos:/ext

143
main.go
View File

@ -1,144 +1,49 @@
//go:generate bash build/generate.sh
package main
import (
"flag"
"fmt"
"io/ioutil"
"github.com/goproxyio/goproxy/pkg/proxy"
"log"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"time"
"github.com/goproxyio/goproxy/module"
)
var cacheDir string
var listen string
var root string
func init() {
log.SetOutput(os.Stdout)
flag.StringVar(&root, "root", "/go", "root cache dir to save")
flag.StringVar(&listen, "listen", "0.0.0.0:8081", "service listen address")
flag.Parse()
if err := os.MkdirAll(root, os.ModePerm); err != nil {
log.Fatalf("goproxy: make root dir failed: %s", err)
}
}
func main() {
gpEnv := os.Getenv("GOPATH")
if gpEnv == "" {
panic("can not find $GOPATH")
}
fmt.Fprintf(os.Stdout, "goproxy: %s inited.\n", time.Now().Format("2006-01-02 15:04:05"))
gp := filepath.SplitList(gpEnv)
cacheDir = filepath.Join(gp[0], "pkg", "mod", "cache", "download")
// sigs := make(chan os.Signal)
// signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
log.Printf("goproxy: %s inited. listen on %s\n", time.Now().Format("2006-01-02 15:04:05"), listen)
cacheDir := filepath.Join(root, "pkg", "mod", "cache", "download")
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
fmt.Fprintf(os.Stdout, "goproxy: %s cache dir is not exist. %s\n", time.Now().Format("2006-01-02 15:04:05"), cacheDir)
os.MkdirAll(cacheDir, 0755)
log.Printf("goproxy: cache dir %s is not exist. To create\n", cacheDir)
if err := os.MkdirAll(cacheDir, 0755); err != nil {
log.Fatalf("make cache dir failed: %s", err)
}
}
http.Handle("/", mainHandler(http.FileServer(http.Dir(cacheDir))))
http.Handle("/", proxy.NewProxy(root))
// TODO: TLS, graceful shutdown
err := http.ListenAndServe(listen, nil)
if nil != err {
panic(err)
}
}
func mainHandler(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(os.Stdout, "goproxy: %s %s download %s\n", r.RemoteAddr, time.Now().Format("2006-01-02 15:04:05"), r.URL.Path)
if _, err := os.Stat(filepath.Join(cacheDir, r.URL.Path)); err != nil {
suffix := path.Ext(r.URL.Path)
if suffix == ".info" || suffix == ".mod" || suffix == ".zip" {
mod := strings.Split(r.URL.Path, "/@v/")
if len(mod) != 2 {
ReturnBadRequest(w, fmt.Errorf("bad module path:%s", r.URL.Path))
return
}
version := strings.TrimSuffix(mod[1], suffix)
version, err = module.DecodeVersion(version)
if err != nil {
ReturnServerError(w, err)
return
}
path := strings.TrimPrefix(mod[0], "/")
path, err := module.DecodePath(path)
if err != nil {
ReturnServerError(w, err)
return
}
// ignore the error, incorrect tag may be given
// forward to inner.ServeHTTP
goGet(path, version, suffix, w, r)
}
// fetch latest version
if strings.HasSuffix(r.URL.Path, "/@latest") {
path := strings.TrimSuffix(r.URL.Path, "/@latest")
path = strings.TrimPrefix(path, "/")
path, err := module.DecodePath(path)
if err != nil {
ReturnServerError(w, err)
return
}
goGet(path, "latest", "", w, r)
}
if strings.HasSuffix(r.URL.Path, "/@v/list") {
w.Write([]byte(""))
return
}
}
inner.ServeHTTP(w, r)
})
}
func goGet(path, version, suffix string, w http.ResponseWriter, r *http.Request) error {
cmd := exec.Command("go", "get", "-d", path+"@"+version)
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
bytesErr, err := ioutil.ReadAll(stderr)
if err != nil {
return err
}
_, err = ioutil.ReadAll(stdout)
if err != nil {
return err
}
if err := cmd.Wait(); err != nil {
fmt.Fprintf(os.Stderr, "goproxy: download %s stderr:\n%s", path, string(bytesErr))
return err
}
out := fmt.Sprintf("%s", bytesErr)
for _, line := range strings.Split(out, "\n") {
f := strings.Fields(line)
if len(f) != 4 {
continue
}
if f[1] == "downloading" && f[2] == path && f[3] != version && suffix != "" {
h := r.Host
mod := strings.Split(r.URL.Path, "/@v/")
p := fmt.Sprintf("%s/@v/%s%s", mod[0], f[3], suffix)
scheme := "http:"
if r.TLS != nil {
scheme = "https:"
}
url := fmt.Sprintf("%s//%s/%s", scheme, h, p)
http.Redirect(w, r, url, 302)
}
}
return nil
}

View File

@ -1,540 +0,0 @@
// Copyright 2018 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 module defines the module.Version type
// along with support code.
package module
// IMPORTANT NOTE
//
// This file essentially defines the set of valid import paths for the go command.
// There are many subtle considerations, including Unicode ambiguity,
// security, network, and file system representations.
//
// This file also defines the set of valid module path and version combinations,
// another topic with many subtle considerations.
//
// Changes to the semantics in this file require approval from rsc.
import (
"fmt"
"sort"
"strings"
"unicode"
"unicode/utf8"
"github.com/goproxyio/goproxy/semver"
)
// A Version is defined by a module path and version pair.
type Version struct {
Path string
// Version is usually a semantic version in canonical form.
// There are two exceptions to this general rule.
// First, the top-level target of a build has no specific version
// and uses Version = "".
// Second, during MVS calculations the version "none" is used
// to represent the decision to take no version of a given module.
Version string `json:",omitempty"`
}
// Check checks that a given module path, version pair is valid.
// In addition to the path being a valid module path
// and the version being a valid semantic version,
// the two must correspond.
// For example, the path "yaml/v2" only corresponds to
// semantic versions beginning with "v2.".
func Check(path, version string) error {
if err := CheckPath(path); err != nil {
return err
}
if !semver.IsValid(version) {
return fmt.Errorf("malformed semantic version %v", version)
}
_, pathMajor, _ := SplitPathVersion(path)
if !MatchPathMajor(version, pathMajor) {
if pathMajor == "" {
pathMajor = "v0 or v1"
}
if pathMajor[0] == '.' { // .v1
pathMajor = pathMajor[1:]
}
return fmt.Errorf("mismatched module path %v and version %v (want %v)", path, version, pathMajor)
}
return nil
}
// firstPathOK reports whether r can appear in the first element of a module path.
// The first element of the path must be an LDH domain name, at least for now.
// To avoid case ambiguity, the domain name must be entirely lower case.
func firstPathOK(r rune) bool {
return r == '-' || r == '.' ||
'0' <= r && r <= '9' ||
'a' <= r && r <= 'z'
}
// pathOK reports whether r can appear in an import path element.
// Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: + - . _ and ~.
// This matches what "go get" has historically recognized in import paths.
// TODO(rsc): We would like to allow Unicode letters, but that requires additional
// care in the safe encoding (see note below).
func pathOK(r rune) bool {
if r < utf8.RuneSelf {
return r == '+' || r == '-' || r == '.' || r == '_' || r == '~' ||
'0' <= r && r <= '9' ||
'A' <= r && r <= 'Z' ||
'a' <= r && r <= 'z'
}
return false
}
// fileNameOK reports whether r can appear in a file name.
// For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters.
// If we expand the set of allowed characters here, we have to
// work harder at detecting potential case-folding and normalization collisions.
// See note about "safe encoding" below.
func fileNameOK(r rune) bool {
if r < utf8.RuneSelf {
// Entire set of ASCII punctuation, from which we remove characters:
// ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
// We disallow some shell special characters: " ' * < > ? ` |
// (Note that some of those are disallowed by the Windows file system as well.)
// We also disallow path separators / : and \ (fileNameOK is only called on path element characters).
// We allow spaces (U+0020) in file names.
const allowed = "!#$%&()+,-.=@[]^_{}~ "
if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' {
return true
}
for i := 0; i < len(allowed); i++ {
if rune(allowed[i]) == r {
return true
}
}
return false
}
// It may be OK to add more ASCII punctuation here, but only carefully.
// For example Windows disallows < > \, and macOS disallows :, so we must not allow those.
return unicode.IsLetter(r)
}
// CheckPath checks that a module path is valid.
func CheckPath(path string) error {
if err := checkPath(path, false); err != nil {
return fmt.Errorf("malformed module path %q: %v", path, err)
}
i := strings.Index(path, "/")
if i < 0 {
i = len(path)
}
if i == 0 {
return fmt.Errorf("malformed module path %q: leading slash", path)
}
if !strings.Contains(path[:i], ".") {
return fmt.Errorf("malformed module path %q: missing dot in first path element", path)
}
if path[0] == '-' {
return fmt.Errorf("malformed module path %q: leading dash in first path element", path)
}
for _, r := range path[:i] {
if !firstPathOK(r) {
return fmt.Errorf("malformed module path %q: invalid char %q in first path element", path, r)
}
}
if _, _, ok := SplitPathVersion(path); !ok {
return fmt.Errorf("malformed module path %q: invalid version", path)
}
return nil
}
// CheckImportPath checks that an import path is valid.
func CheckImportPath(path string) error {
if err := checkPath(path, false); err != nil {
return fmt.Errorf("malformed import path %q: %v", path, err)
}
return nil
}
// checkPath checks that a general path is valid.
// It returns an error describing why but not mentioning path.
// Because these checks apply to both module paths and import paths,
// the caller is expected to add the "malformed ___ path %q: " prefix.
// fileName indicates whether the final element of the path is a file name
// (as opposed to a directory name).
func checkPath(path string, fileName bool) error {
if !utf8.ValidString(path) {
return fmt.Errorf("invalid UTF-8")
}
if path == "" {
return fmt.Errorf("empty string")
}
if strings.Contains(path, "..") {
return fmt.Errorf("double dot")
}
if strings.Contains(path, "//") {
return fmt.Errorf("double slash")
}
if path[len(path)-1] == '/' {
return fmt.Errorf("trailing slash")
}
elemStart := 0
for i, r := range path {
if r == '/' {
if err := checkElem(path[elemStart:i], fileName); err != nil {
return err
}
elemStart = i + 1
}
}
if err := checkElem(path[elemStart:], fileName); err != nil {
return err
}
return nil
}
// checkElem checks whether an individual path element is valid.
// fileName indicates whether the element is a file name (not a directory name).
func checkElem(elem string, fileName bool) error {
if elem == "" {
return fmt.Errorf("empty path element")
}
if strings.Count(elem, ".") == len(elem) {
return fmt.Errorf("invalid path element %q", elem)
}
if elem[0] == '.' && !fileName {
return fmt.Errorf("leading dot in path element")
}
if elem[len(elem)-1] == '.' {
return fmt.Errorf("trailing dot in path element")
}
charOK := pathOK
if fileName {
charOK = fileNameOK
}
for _, r := range elem {
if !charOK(r) {
return fmt.Errorf("invalid char %q", r)
}
}
// Windows disallows a bunch of path elements, sadly.
// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
short := elem
if i := strings.Index(short, "."); i >= 0 {
short = short[:i]
}
for _, bad := range badWindowsNames {
if strings.EqualFold(bad, short) {
return fmt.Errorf("disallowed path element %q", elem)
}
}
return nil
}
// CheckFilePath checks whether a slash-separated file path is valid.
func CheckFilePath(path string) error {
if err := checkPath(path, true); err != nil {
return fmt.Errorf("malformed file path %q: %v", path, err)
}
return nil
}
// badWindowsNames are the reserved file path elements on Windows.
// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
var badWindowsNames = []string{
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
}
// SplitPathVersion returns prefix and major version such that prefix+pathMajor == path
// and version is either empty or "/vN" for N >= 2.
// As a special case, gopkg.in paths are recognized directly;
// they require ".vN" instead of "/vN", and for all N, not just N >= 2.
func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) {
if strings.HasPrefix(path, "gopkg.in/") {
return splitGopkgIn(path)
}
i := len(path)
dot := false
for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') {
if path[i-1] == '.' {
dot = true
}
i--
}
if i <= 1 || path[i-1] != 'v' || path[i-2] != '/' {
return path, "", true
}
prefix, pathMajor = path[:i-2], path[i-2:]
if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" {
return path, "", false
}
return prefix, pathMajor, true
}
// splitGopkgIn is like SplitPathVersion but only for gopkg.in paths.
func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) {
if !strings.HasPrefix(path, "gopkg.in/") {
return path, "", false
}
i := len(path)
if strings.HasSuffix(path, "-unstable") {
i -= len("-unstable")
}
for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') {
i--
}
if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' {
// All gopkg.in paths must end in vN for some N.
return path, "", false
}
prefix, pathMajor = path[:i-2], path[i-2:]
if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" {
return path, "", false
}
return prefix, pathMajor, true
}
// MatchPathMajor reports whether the semantic version v
// matches the path major version pathMajor.
func MatchPathMajor(v, pathMajor string) bool {
if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
}
if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" {
// Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1.
// For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405.
return true
}
m := semver.Major(v)
if pathMajor == "" {
return m == "v0" || m == "v1" || semver.Build(v) == "+incompatible"
}
return (pathMajor[0] == '/' || pathMajor[0] == '.') && m == pathMajor[1:]
}
// CanonicalVersion returns the canonical form of the version string v.
// It is the same as semver.Canonical(v) except that it preserves the special build suffix "+incompatible".
func CanonicalVersion(v string) string {
cv := semver.Canonical(v)
if semver.Build(v) == "+incompatible" {
cv += "+incompatible"
}
return cv
}
// Sort sorts the list by Path, breaking ties by comparing Versions.
func Sort(list []Version) {
sort.Slice(list, func(i, j int) bool {
mi := list[i]
mj := list[j]
if mi.Path != mj.Path {
return mi.Path < mj.Path
}
// To help go.sum formatting, allow version/file.
// Compare semver prefix by semver rules,
// file by string order.
vi := mi.Version
vj := mj.Version
var fi, fj string
if k := strings.Index(vi, "/"); k >= 0 {
vi, fi = vi[:k], vi[k:]
}
if k := strings.Index(vj, "/"); k >= 0 {
vj, fj = vj[:k], vj[k:]
}
if vi != vj {
return semver.Compare(vi, vj) < 0
}
return fi < fj
})
}
// Safe encodings
//
// Module paths appear as substrings of file system paths
// (in the download cache) and of web server URLs in the proxy protocol.
// In general we cannot rely on file systems to be case-sensitive,
// nor can we rely on web servers, since they read from file systems.
// That is, we cannot rely on the file system to keep rsc.io/QUOTE
// and rsc.io/quote separate. Windows and macOS don't.
// Instead, we must never require two different casings of a file path.
// Because we want the download cache to match the proxy protocol,
// and because we want the proxy protocol to be possible to serve
// from a tree of static files (which might be stored on a case-insensitive
// file system), the proxy protocol must never require two different casings
// of a URL path either.
//
// One possibility would be to make the safe encoding be the lowercase
// hexadecimal encoding of the actual path bytes. This would avoid ever
// needing different casings of a file path, but it would be fairly illegible
// to most programmers when those paths appeared in the file system
// (including in file paths in compiler errors and stack traces)
// in web server logs, and so on. Instead, we want a safe encoding that
// leaves most paths unaltered.
//
// The safe encoding is this:
// replace every uppercase letter with an exclamation mark
// followed by the letter's lowercase equivalent.
//
// For example,
// github.com/Azure/azure-sdk-for-go -> github.com/!azure/azure-sdk-for-go.
// github.com/GoogleCloudPlatform/cloudsql-proxy -> github.com/!google!cloud!platform/cloudsql-proxy
// github.com/Sirupsen/logrus -> github.com/!sirupsen/logrus.
//
// Import paths that avoid upper-case letters are left unchanged.
// Note that because import paths are ASCII-only and avoid various
// problematic punctuation (like : < and >), the safe encoding is also ASCII-only
// and avoids the same problematic punctuation.
//
// Import paths have never allowed exclamation marks, so there is no
// need to define how to encode a literal !.
//
// Although paths are disallowed from using Unicode (see pathOK above),
// the eventual plan is to allow Unicode letters as well, to assume that
// file systems and URLs are Unicode-safe (storing UTF-8), and apply
// the !-for-uppercase convention. Note however that not all runes that
// are different but case-fold equivalent are an upper/lower pair.
// For example, U+004B ('K'), U+006B ('k'), and U+212A ('' for Kelvin)
// are considered to case-fold to each other. When we do add Unicode
// letters, we must not assume that upper/lower are the only case-equivalent pairs.
// Perhaps the Kelvin symbol would be disallowed entirely, for example.
// Or perhaps it would encode as "!!k", or perhaps as "(212A)".
//
// Also, it would be nice to allow Unicode marks as well as letters,
// but marks include combining marks, and then we must deal not
// only with case folding but also normalization: both U+00E9 ('é')
// and U+0065 U+0301 ('e' followed by combining acute accent)
// look the same on the page and are treated by some file systems
// as the same path. If we do allow Unicode marks in paths, there
// must be some kind of normalization to allow only one canonical
// encoding of any character used in an import path.
// EncodePath returns the safe encoding of the given module path.
// It fails if the module path is invalid.
func EncodePath(path string) (encoding string, err error) {
if err := CheckPath(path); err != nil {
return "", err
}
return encodeString(path)
}
// EncodeVersion returns the safe encoding of the given module version.
// Versions are allowed to be in non-semver form but must be valid file names
// and not contain exclamation marks.
func EncodeVersion(v string) (encoding string, err error) {
if err := checkElem(v, true); err != nil || strings.Contains(v, "!") {
return "", fmt.Errorf("disallowed version string %q", v)
}
return encodeString(v)
}
func encodeString(s string) (encoding string, err error) {
haveUpper := false
for _, r := range s {
if r == '!' || r >= utf8.RuneSelf {
// This should be disallowed by CheckPath, but diagnose anyway.
// The correctness of the encoding loop below depends on it.
return "", fmt.Errorf("internal error: inconsistency in EncodePath")
}
if 'A' <= r && r <= 'Z' {
haveUpper = true
}
}
if !haveUpper {
return s, nil
}
var buf []byte
for _, r := range s {
if 'A' <= r && r <= 'Z' {
buf = append(buf, '!', byte(r+'a'-'A'))
} else {
buf = append(buf, byte(r))
}
}
return string(buf), nil
}
// DecodePath returns the module path of the given safe encoding.
// It fails if the encoding is invalid or encodes an invalid path.
func DecodePath(encoding string) (path string, err error) {
path, ok := decodeString(encoding)
if !ok {
return "", fmt.Errorf("invalid module path encoding %q", encoding)
}
if err := CheckPath(path); err != nil {
return "", fmt.Errorf("invalid module path encoding %q: %v", encoding, err)
}
return path, nil
}
// DecodeVersion returns the version string for the given safe encoding.
// It fails if the encoding is invalid or encodes an invalid version.
// Versions are allowed to be in non-semver form but must be valid file names
// and not contain exclamation marks.
func DecodeVersion(encoding string) (v string, err error) {
v, ok := decodeString(encoding)
if !ok {
return "", fmt.Errorf("invalid version encoding %q", encoding)
}
if err := checkElem(v, true); err != nil {
return "", fmt.Errorf("disallowed version string %q", v)
}
return v, nil
}
func decodeString(encoding string) (string, bool) {
var buf []byte
bang := false
for _, r := range encoding {
if r >= utf8.RuneSelf {
return "", false
}
if bang {
bang = false
if r < 'a' || 'z' < r {
return "", false
}
buf = append(buf, byte(r+'A'-'a'))
continue
}
if r == '!' {
bang = true
continue
}
if 'A' <= r && r <= 'Z' {
return "", false
}
buf = append(buf, byte(r))
}
if bang {
return "", false
}
return string(buf), true
}

99
pkg/proxy/proxy.go Normal file
View File

@ -0,0 +1,99 @@
package proxy
import (
"fmt"
"github.com/goproxyio/goproxy/pkg/modfetch"
"github.com/goproxyio/goproxy/pkg/modfetch/codehost"
"github.com/goproxyio/goproxy/pkg/module"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
)
var cacheDir string
var innerHandle http.Handler
func NewProxy(cache string) http.Handler {
modfetch.PkgMod = filepath.Join(cache, "pkg/mod")
codehost.WorkRoot = filepath.Join(modfetch.PkgMod, "cache/vcs")
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 download %s\n", r.RemoteAddr, r.URL.Path)
if _, err := os.Stat(filepath.Join(cacheDir, r.URL.Path)); err != nil {
suffix := path.Ext(r.URL.Path)
if suffix == ".info" || suffix == ".mod" || suffix == ".zip" {
mod := strings.Split(r.URL.Path, "/@v/")
if len(mod) != 2 {
ReturnBadRequest(w, fmt.Errorf("bad module path:%s", r.URL.Path))
return
}
version := strings.TrimSuffix(mod[1], suffix)
version, err = module.DecodeVersion(version)
if err != nil {
ReturnServerError(w, err)
return
}
modPath := strings.TrimPrefix(mod[0], "/")
modPath, err := module.DecodePath(modPath)
if err != nil {
ReturnServerError(w, err)
return
}
// ignore the error, incorrect tag may be given
// forward to inner.ServeHTTP
if err := downloadMod(modPath, version); err != nil {
errLogger.Printf("download get err %s", err)
}
}
// fetch latest version
if strings.HasSuffix(r.URL.Path, "/@latest") {
modPath := strings.TrimSuffix(r.URL.Path, "/@latest")
modPath = strings.TrimPrefix(modPath, "/")
modPath, err := module.DecodePath(modPath)
if err != nil {
ReturnServerError(w, err)
return
}
if err := downloadMod(modPath, "latest"); err != nil {
errLogger.Printf("download get err %s", err)
}
}
if strings.HasSuffix(r.URL.Path, "/@v/list") {
// TODO
_, _ = w.Write([]byte(""))
return
}
}
innerHandle.ServeHTTP(w, r)
})
}
func downloadMod(modPath, version string) error {
if _, err := modfetch.InfoFile(modPath, version); err != nil {
return err
}
if _, err := modfetch.GoModFile(modPath, version); err != nil {
return err
}
if _, err := modfetch.GoModSum(modPath, version); err != nil {
return err
}
mod := module.Version{Path: modPath, Version: version}
if _, err := modfetch.DownloadZip(mod); err != nil {
return err
}
if a, err := modfetch.Download(mod); err != nil {
return err
} else {
log.Printf("goproxy: download %s@%s to dir %s\n", modPath, version, a)
}
return nil
}

View File

@ -1,21 +1,24 @@
package main
package proxy
import (
"fmt"
"log"
"net/http"
"os"
)
var errLogger = log.New(os.Stderr, "", log.LstdFlags)
func ReturnServerError(w http.ResponseWriter, err error) {
w.WriteHeader(500)
msg := fmt.Sprintf("%v", err)
fmt.Fprintf(os.Stderr, "goproxy: %s\n", msg)
w.Write([]byte(msg))
errLogger.Printf("goproxy: %s\n", msg)
_, _ = w.Write([]byte(msg))
}
func ReturnBadRequest(w http.ResponseWriter, err error) {
w.WriteHeader(400)
msg := fmt.Sprintf("%v", err)
fmt.Fprintf(os.Stderr, "goproxy: %s\n", msg)
w.Write([]byte(msg))
errLogger.Printf("goproxy: %s\n", msg)
_, _ = w.Write([]byte(msg))
}

View File

@ -1,388 +0,0 @@
// Copyright 2018 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 semver implements comparison of semantic version strings.
// In this package, semantic version strings must begin with a leading "v",
// as in "v1.0.0".
//
// The general form of a semantic version string accepted by this package is
//
// vMAJOR[.MINOR[.PATCH[-PRERELEASE][+BUILD]]]
//
// where square brackets indicate optional parts of the syntax;
// MAJOR, MINOR, and PATCH are decimal integers without extra leading zeros;
// PRERELEASE and BUILD are each a series of non-empty dot-separated identifiers
// using only alphanumeric characters and hyphens; and
// all-numeric PRERELEASE identifiers must not have leading zeros.
//
// This package follows Semantic Versioning 2.0.0 (see semver.org)
// with two exceptions. First, it requires the "v" prefix. Second, it recognizes
// vMAJOR and vMAJOR.MINOR (with no prerelease or build suffixes)
// as shorthands for vMAJOR.0.0 and vMAJOR.MINOR.0.
package semver
// parsed returns the parsed form of a semantic version string.
type parsed struct {
major string
minor string
patch string
short string
prerelease string
build string
err string
}
// IsValid reports whether v is a valid semantic version string.
func IsValid(v string) bool {
_, ok := parse(v)
return ok
}
// Canonical returns the canonical formatting of the semantic version v.
// It fills in any missing .MINOR or .PATCH and discards build metadata.
// Two semantic versions compare equal only if their canonical formattings
// are identical strings.
// The canonical invalid semantic version is the empty string.
func Canonical(v string) string {
p, ok := parse(v)
if !ok {
return ""
}
if p.build != "" {
return v[:len(v)-len(p.build)]
}
if p.short != "" {
return v + p.short
}
return v
}
// Major returns the major version prefix of the semantic version v.
// For example, Major("v2.1.0") == "v2".
// If v is an invalid semantic version string, Major returns the empty string.
func Major(v string) string {
pv, ok := parse(v)
if !ok {
return ""
}
return v[:1+len(pv.major)]
}
// MajorMinor returns the major.minor version prefix of the semantic version v.
// For example, MajorMinor("v2.1.0") == "v2.1".
// If v is an invalid semantic version string, MajorMinor returns the empty string.
func MajorMinor(v string) string {
pv, ok := parse(v)
if !ok {
return ""
}
i := 1 + len(pv.major)
if j := i + 1 + len(pv.minor); j <= len(v) && v[i] == '.' && v[i+1:j] == pv.minor {
return v[:j]
}
return v[:i] + "." + pv.minor
}
// Prerelease returns the prerelease suffix of the semantic version v.
// For example, Prerelease("v2.1.0-pre+meta") == "-pre".
// If v is an invalid semantic version string, Prerelease returns the empty string.
func Prerelease(v string) string {
pv, ok := parse(v)
if !ok {
return ""
}
return pv.prerelease
}
// Build returns the build suffix of the semantic version v.
// For example, Build("v2.1.0+meta") == "+meta".
// If v is an invalid semantic version string, Build returns the empty string.
func Build(v string) string {
pv, ok := parse(v)
if !ok {
return ""
}
return pv.build
}
// Compare returns an integer comparing two versions according to
// according to semantic version precedence.
// The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
//
// An invalid semantic version string is considered less than a valid one.
// All invalid semantic version strings compare equal to each other.
func Compare(v, w string) int {
pv, ok1 := parse(v)
pw, ok2 := parse(w)
if !ok1 && !ok2 {
return 0
}
if !ok1 {
return -1
}
if !ok2 {
return +1
}
if c := compareInt(pv.major, pw.major); c != 0 {
return c
}
if c := compareInt(pv.minor, pw.minor); c != 0 {
return c
}
if c := compareInt(pv.patch, pw.patch); c != 0 {
return c
}
return comparePrerelease(pv.prerelease, pw.prerelease)
}
// Max canonicalizes its arguments and then returns the version string
// that compares greater.
func Max(v, w string) string {
v = Canonical(v)
w = Canonical(w)
if Compare(v, w) > 0 {
return v
}
return w
}
func parse(v string) (p parsed, ok bool) {
if v == "" || v[0] != 'v' {
p.err = "missing v prefix"
return
}
p.major, v, ok = parseInt(v[1:])
if !ok {
p.err = "bad major version"
return
}
if v == "" {
p.minor = "0"
p.patch = "0"
p.short = ".0.0"
return
}
if v[0] != '.' {
p.err = "bad minor prefix"
ok = false
return
}
p.minor, v, ok = parseInt(v[1:])
if !ok {
p.err = "bad minor version"
return
}
if v == "" {
p.patch = "0"
p.short = ".0"
return
}
if v[0] != '.' {
p.err = "bad patch prefix"
ok = false
return
}
p.patch, v, ok = parseInt(v[1:])
if !ok {
p.err = "bad patch version"
return
}
if len(v) > 0 && v[0] == '-' {
p.prerelease, v, ok = parsePrerelease(v)
if !ok {
p.err = "bad prerelease"
return
}
}
if len(v) > 0 && v[0] == '+' {
p.build, v, ok = parseBuild(v)
if !ok {
p.err = "bad build"
return
}
}
if v != "" {
p.err = "junk on end"
ok = false
return
}
ok = true
return
}
func parseInt(v string) (t, rest string, ok bool) {
if v == "" {
return
}
if v[0] < '0' || '9' < v[0] {
return
}
i := 1
for i < len(v) && '0' <= v[i] && v[i] <= '9' {
i++
}
if v[0] == '0' && i != 1 {
return
}
return v[:i], v[i:], true
}
func parsePrerelease(v string) (t, rest string, ok bool) {
// "A pre-release version MAY be denoted by appending a hyphen and
// a series of dot separated identifiers immediately following the patch version.
// Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
// Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes."
if v == "" || v[0] != '-' {
return
}
i := 1
start := 1
for i < len(v) && v[i] != '+' {
if !isIdentChar(v[i]) && v[i] != '.' {
return
}
if v[i] == '.' {
if start == i || isBadNum(v[start:i]) {
return
}
start = i + 1
}
i++
}
if start == i || isBadNum(v[start:i]) {
return
}
return v[:i], v[i:], true
}
func parseBuild(v string) (t, rest string, ok bool) {
if v == "" || v[0] != '+' {
return
}
i := 1
start := 1
for i < len(v) {
if !isIdentChar(v[i]) {
return
}
if v[i] == '.' {
if start == i {
return
}
start = i + 1
}
i++
}
if start == i {
return
}
return v[:i], v[i:], true
}
func isIdentChar(c byte) bool {
return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '-'
}
func isBadNum(v string) bool {
i := 0
for i < len(v) && '0' <= v[i] && v[i] <= '9' {
i++
}
return i == len(v) && i > 1 && v[0] == '0'
}
func isNum(v string) bool {
i := 0
for i < len(v) && '0' <= v[i] && v[i] <= '9' {
i++
}
return i == len(v)
}
func compareInt(x, y string) int {
if x == y {
return 0
}
if len(x) < len(y) {
return -1
}
if len(x) > len(y) {
return +1
}
if x < y {
return -1
} else {
return +1
}
}
func comparePrerelease(x, y string) int {
// "When major, minor, and patch are equal, a pre-release version has
// lower precedence than a normal version.
// Example: 1.0.0-alpha < 1.0.0.
// Precedence for two pre-release versions with the same major, minor,
// and patch version MUST be determined by comparing each dot separated
// identifier from left to right until a difference is found as follows:
// identifiers consisting of only digits are compared numerically and
// identifiers with letters or hyphens are compared lexically in ASCII
// sort order. Numeric identifiers always have lower precedence than
// non-numeric identifiers. A larger set of pre-release fields has a
// higher precedence than a smaller set, if all of the preceding
// identifiers are equal.
// Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta <
// 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0."
if x == y {
return 0
}
if x == "" {
return +1
}
if y == "" {
return -1
}
for x != "" && y != "" {
x = x[1:] // skip - or .
y = y[1:] // skip - or .
var dx, dy string
dx, x = nextIdent(x)
dy, y = nextIdent(y)
if dx != dy {
ix := isNum(dx)
iy := isNum(dy)
if ix != iy {
if ix {
return -1
} else {
return +1
}
}
if ix {
if len(dx) < len(dy) {
return -1
}
if len(dx) > len(dy) {
return +1
}
}
if dx < dy {
return -1
} else {
return +1
}
}
}
if x == "" {
return -1
} else {
return +1
}
}
func nextIdent(x string) (dx, rest string) {
i := 0
for i < len(x) && x[i] != '.' {
i++
}
return x[:i], x[i:]
}