├── .gitignore ├── .docker.json.enc ├── Dockerfile ├── .travis.yml ├── cli └── stable │ ├── main.go │ ├── logger.go │ └── cmd_server.go ├── fetcher_test.go ├── LICENSE ├── etc └── cloud-config.yml ├── fetcher.go ├── server.go ├── common_test.go ├── common.go ├── Makefile ├── proxy_test.go ├── proxy.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | cli/gop.kg/gop.kg 3 | coverage.txt 4 | -------------------------------------------------------------------------------- /.docker.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcuadros/go-stable/HEAD/.docker.json.enc -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | MAINTAINER Máximo Cuadros 3 | 4 | RUN apk --update upgrade && \ 5 | apk add curl ca-certificates && \ 6 | update-ca-certificates && \ 7 | rm -rf /var/cache/apk/* 8 | 9 | ADD cli/stable/stable /usr/local/bin/ 10 | CMD ["stable"] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | services: 6 | - docker 7 | 8 | go: 9 | - 1.7 10 | 11 | script: 12 | - make test-coverage 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) 16 | 17 | before_deploy: 18 | - mkdir -p $HOME/.docker/ 19 | - openssl aes-256-cbc -K $encrypted_55ba33ebd2bf_key -iv $encrypted_55ba33ebd2bf_iv -in .docker.json.enc -out $HOME/.docker/config.json -d 20 | - make push 21 | - make packages 22 | 23 | deploy: 24 | provider: releases 25 | api_key: $GITHUB_TOKEN 26 | file: 27 | - build/gopkg_${TRAVIS_TAG}_darwin_amd64.tar.gz 28 | - build/gopkg_${TRAVIS_TAG}_linux_amd64.tar.gz 29 | skip_cleanup: true 30 | on: 31 | tags: true 32 | -------------------------------------------------------------------------------- /cli/stable/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | 8 | "github.com/jessevdk/go-flags" 9 | ) 10 | 11 | var ( 12 | commit string 13 | version string 14 | build string 15 | ) 16 | 17 | func main() { 18 | runtime.GOMAXPROCS(runtime.NumCPU()) 19 | parser := flags.NewParser(nil, flags.Default) 20 | parser.AddCommand("server", "", "", &ServerCommand{}) 21 | 22 | if _, err := parser.Parse(); err != nil { 23 | if err, ok := err.(*flags.Error); ok { 24 | if err.Type == flags.ErrHelp { 25 | os.Exit(0) 26 | } 27 | 28 | parser.WriteHelp(os.Stdout) 29 | fmt.Printf( 30 | "\nBuild information\n version: %s\n build: %s\n commit: %s\n", 31 | version, build, commit, 32 | ) 33 | } 34 | 35 | os.Exit(1) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /fetcher_test.go: -------------------------------------------------------------------------------- 1 | package stable 2 | 3 | import ( 4 | "bytes" 5 | 6 | . "gopkg.in/check.v1" 7 | "gopkg.in/src-d/go-git.v4/plumbing" 8 | "gopkg.in/src-d/go-git.v4/plumbing/transport" 9 | ) 10 | 11 | type FetcherSuite struct{} 12 | 13 | var _ = Suite(&FetcherSuite{}) 14 | 15 | func (s *FetcherSuite) TestVersions(c *C) { 16 | pkg := &Package{} 17 | pkg.Repository, _ = transport.NewEndpoint("https://github.com/git-fixtures/basic") 18 | 19 | f := NewFetcher(pkg, nil) 20 | versions, err := f.Versions() 21 | c.Assert(err, IsNil) 22 | c.Assert(versions, HasLen, 2) 23 | } 24 | 25 | func (s *FetcherSuite) TestFetch(c *C) { 26 | pkg := &Package{} 27 | pkg.Repository, _ = transport.NewEndpoint("https://github.com/git-fixtures/basic") 28 | 29 | f := NewFetcher(pkg, nil) 30 | 31 | ref := plumbing.NewReferenceFromStrings("foo", "6ecf0ef2c2dffb796033e5a02219af86ec6584e5") 32 | 33 | buf := bytes.NewBuffer(nil) 34 | n, err := f.Fetch(buf, ref) 35 | c.Assert(err, IsNil) 36 | c.Assert(n, Equals, int64(85374)) 37 | c.Assert(buf.Len(), Equals, 85374) 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Máximo Cuadros 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /etc/cloud-config.yml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | coreos: 4 | units: 5 | - name: go-stable.service 6 | command: start 7 | content: | 8 | [Unit] 9 | Description=go-stable service 10 | After=docker.service 11 | Requires=docker.service 12 | 13 | [Service] 14 | Environment=DOMAIN= 15 | Environment=ORGANIZATION= 16 | Environment=CERTIFICATES_FOLDER=/data/certificates 17 | Environment=VERSION=v1.0.2 18 | Environment=CONTAINER_NAME=go-stable 19 | 20 | TimeoutStartSec=20m 21 | ExecStartPre=-/usr/bin/docker kill ${CONTAINER_NAME} 22 | ExecStartPre=-/usr/bin/docker rm ${CONTAINER_NAME} 23 | ExecStart=/usr/bin/docker run --name ${CONTAINER_NAME} \ 24 | -v ${CERTIFICATES_FOLDER}:/certificates \ 25 | -p 443:443 -p 80:80 \ 26 | mcuadros/go-stable:${VERSION} \ 27 | stable server \ 28 | --host ${DOMAIN} \ 29 | --addr :443 \ 30 | --redirect-addr :80 \ 31 | --organization ${ORGANIZATION} 32 | ExecStop=/usr/bin/docker stop ${CONTAINER_NAME} 33 | 34 | [Install] 35 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /fetcher.go: -------------------------------------------------------------------------------- 1 | package stable 2 | 3 | import ( 4 | "io" 5 | 6 | "gopkg.in/src-d/go-git.v4/plumbing" 7 | "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" 8 | "gopkg.in/src-d/go-git.v4/plumbing/transport" 9 | "gopkg.in/src-d/go-git.v4/plumbing/transport/http" 10 | ) 11 | 12 | type Fetcher struct { 13 | pkg *Package 14 | service transport.UploadPackSession 15 | auth transport.AuthMethod 16 | } 17 | 18 | func NewFetcher(p *Package, auth transport.AuthMethod) *Fetcher { 19 | 20 | s, _ := http.DefaultClient.NewUploadPackSession(p.Repository, auth) 21 | 22 | return &Fetcher{pkg: p, service: s} 23 | } 24 | 25 | func (f *Fetcher) Versions() (Versions, error) { 26 | info, err := f.service.AdvertisedReferences() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | refs, err := info.AllReferences() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return NewVersions(refs), nil 37 | } 38 | 39 | func (f *Fetcher) Fetch(w io.Writer, ref *plumbing.Reference) (written int64, err error) { 40 | req := packp.NewUploadPackRequest() 41 | req.Wants = []plumbing.Hash{ref.Hash()} 42 | 43 | r, err := f.service.UploadPack(req) 44 | if err != nil { 45 | return 0, err 46 | } 47 | 48 | return io.Copy(w, r) 49 | } 50 | -------------------------------------------------------------------------------- /cli/stable/logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/Sirupsen/logrus" 8 | "github.com/urfave/negroni" 9 | ) 10 | 11 | type Logger struct { 12 | Logger *logrus.Logger 13 | } 14 | 15 | func NewLogger(level logrus.Level, formatter logrus.Formatter) *Logger { 16 | log := logrus.New() 17 | log.Level = level 18 | log.Formatter = formatter 19 | 20 | return &Logger{Logger: log} 21 | } 22 | 23 | func (m *Logger) ServeHTTP(w http.ResponseWriter, r *http.Request, n http.HandlerFunc) { 24 | remoteAddr := r.RemoteAddr 25 | if realIP := r.Header.Get("X-Real-IP"); realIP != "" { 26 | remoteAddr = realIP 27 | } 28 | 29 | f := logrus.Fields{} 30 | f["request"] = r.URL.Path 31 | f["method"] = r.Method 32 | f["remote"] = remoteAddr 33 | logrus.NewEntry(m.Logger).WithFields(f).Debug("new request accepted") 34 | 35 | start := time.Now() 36 | n(w, r) 37 | 38 | status := w.(negroni.ResponseWriter).Status() 39 | 40 | f["status"] = status 41 | f["elapsed"] = time.Since(start) 42 | 43 | entry := logrus.NewEntry(m.Logger).WithFields(f) 44 | if status != 200 { 45 | entry.Warning("completed handling request, with errors") 46 | return 47 | } 48 | 49 | entry.Info("completed handling request") 50 | } 51 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package stable 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | const DefaultBaseRoute = "/{org:[a-z0-9-]+}/{repository:[a-z0-9-/]+}.{version:v[0-9.]+}" 11 | 12 | type Server struct { 13 | http.Server 14 | r *mux.Router 15 | 16 | BaseRoute string 17 | Host string 18 | Default struct { 19 | Server string 20 | Organization string 21 | Repository string 22 | } 23 | } 24 | 25 | func NewDefaultServer(host string) *Server { 26 | s := NewServer(DefaultBaseRoute, host) 27 | s.Default.Server = "github.com" 28 | 29 | return s 30 | } 31 | 32 | func NewServer(base, host string) *Server { 33 | s := &Server{ 34 | BaseRoute: base, 35 | Host: host, 36 | } 37 | 38 | s.buildRouter() 39 | return s 40 | } 41 | 42 | func (s *Server) ListenAndServe() error { 43 | panic("ListenAndServer, is not supported try ListenAndServeTLS") 44 | } 45 | 46 | func (s *Server) ListenAndServeTLS(certFile, keyFile string) error { 47 | return s.Server.ListenAndServeTLS(certFile, keyFile) 48 | } 49 | 50 | func (s *Server) buildRouter() { 51 | s.r = mux.NewRouter() 52 | s.r.HandleFunc("/", s.doRootRedirect).Methods("GET").Name("base") 53 | s.r.HandleFunc(path.Join(s.BaseRoute, "/info/refs"), s.doUploadPackInfoResponse).Methods("GET") 54 | s.r.HandleFunc(path.Join(s.BaseRoute, "/git-upload-pack"), s.doUploadPackResponse).Methods("POST") 55 | s.r.HandleFunc(path.Join(s.BaseRoute, "/{subpkg:.+}"), s.doMetaImportResponse).Methods("GET").Queries("go-get", "1") 56 | s.r.HandleFunc(path.Join(s.BaseRoute, "/{subpkg:.+}"), s.doPackageRedirect).Methods("GET") 57 | s.r.HandleFunc(s.BaseRoute, s.doMetaImportResponse).Methods("GET").Queries("go-get", "1") 58 | s.r.HandleFunc(s.BaseRoute, s.doPackageRedirect).Methods("GET").Name("base") 59 | s.Handler = s.r 60 | } 61 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package stable 2 | 3 | import ( 4 | "testing" 5 | 6 | . "gopkg.in/check.v1" 7 | "gopkg.in/src-d/go-git.v4/plumbing" 8 | "gopkg.in/src-d/go-git.v4/storage/memory" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type SuiteCommon struct{} 14 | 15 | var _ = Suite(&SuiteCommon{}) 16 | 17 | func (s *SuiteCommon) TestNewVersions(c *C) { 18 | refs := make(memory.ReferenceStorage, 0) 19 | refs.SetReference(plumbing.NewHashReference("refs/heads/master", plumbing.NewHash(""))) 20 | refs.SetReference(plumbing.NewHashReference("refs/tags/v1.0.0", plumbing.NewHash(""))) 21 | refs.SetReference(plumbing.NewHashReference("refs/tags/1.1.2", plumbing.NewHash(""))) 22 | refs.SetReference(plumbing.NewHashReference("refs/tags/1.1.3", plumbing.NewHash(""))) 23 | refs.SetReference(plumbing.NewHashReference("refs/tags/v1.0.3", plumbing.NewHash(""))) 24 | refs.SetReference(plumbing.NewHashReference("refs/tags/v2.0.3", plumbing.NewHash(""))) 25 | refs.SetReference(plumbing.NewHashReference("refs/tags/v4.0.0-rc1", plumbing.NewHash(""))) 26 | 27 | v := NewVersions(refs) 28 | c.Assert(v.BestMatch("v0").Name().String(), Equals, "refs/heads/master") 29 | c.Assert(v.BestMatch("v1.1").Name().String(), Equals, "refs/tags/1.1.3") 30 | c.Assert(v.BestMatch("1.1").Name().String(), Equals, "refs/tags/1.1.3") 31 | c.Assert(v.BestMatch("1.1.2").Name().String(), Equals, "refs/tags/1.1.2") 32 | c.Assert(v.BestMatch("2").Name().String(), Equals, "refs/tags/v2.0.3") 33 | c.Assert(v.BestMatch("4").Name().String(), Equals, "refs/tags/v4.0.0-rc1") 34 | c.Assert(v.BestMatch("master").Name().String(), Equals, "refs/heads/master") 35 | c.Assert(v.BestMatch("foo"), IsNil) 36 | 37 | refs.SetReference(plumbing.NewHashReference("refs/tags/v0.0.0", plumbing.NewHash(""))) 38 | 39 | v = NewVersions(refs) 40 | c.Assert(v.BestMatch("v0").Name().String(), Equals, "refs/tags/v0.0.0") 41 | } 42 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package stable 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mcuadros/go-version" 7 | "gopkg.in/src-d/go-git.v4/plumbing" 8 | "gopkg.in/src-d/go-git.v4/plumbing/transport" 9 | "gopkg.in/src-d/go-git.v4/storage/memory" 10 | ) 11 | 12 | // Package represent a golang package 13 | type Package struct { 14 | Name string 15 | Repository transport.Endpoint 16 | Constrain string 17 | Versions Versions 18 | } 19 | 20 | type Versions map[string]*plumbing.Reference 21 | 22 | func NewVersions(refs memory.ReferenceStorage) Versions { 23 | versions := make(Versions, 0) 24 | for _, ref := range refs { 25 | if !ref.IsTag() && !ref.IsBranch() { 26 | continue 27 | } 28 | 29 | versions[ref.Name().Short()] = ref 30 | } 31 | 32 | return versions 33 | } 34 | 35 | func (v Versions) Match(needed string) []*plumbing.Reference { 36 | c := newConstrain(needed) 37 | 38 | var names []string 39 | for _, ref := range v { 40 | name := ref.Name().Short() 41 | if c.Match(version.Normalize(name)) { 42 | names = append(names, name) 43 | } 44 | } 45 | 46 | version.Sort(names) 47 | var matched []*plumbing.Reference 48 | for n := len(names) - 1; n >= 0; n-- { 49 | matched = append(matched, v[names[n]]) 50 | } 51 | 52 | return matched 53 | } 54 | 55 | func (v Versions) BestMatch(needed string) *plumbing.Reference { 56 | if version, ok := v[needed]; ok { 57 | return version 58 | } 59 | 60 | matched := v.Match(needed) 61 | if len(matched) != 0 { 62 | return matched[0] 63 | } 64 | 65 | if needed == "v0" { 66 | return v.handleV0() 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (v Versions) handleV0() *plumbing.Reference { 73 | return v.BestMatch("master") 74 | } 75 | 76 | func (v Versions) Mayor() map[string]*plumbing.Reference { 77 | output := make(map[string]*plumbing.Reference, 0) 78 | for i := 0; i < 100; i++ { 79 | mayor := fmt.Sprintf("v%d", i) 80 | if m := v.BestMatch(mayor); m != nil { 81 | output[mayor] = m 82 | } 83 | } 84 | 85 | return output 86 | } 87 | 88 | func newConstrain(needed string) *version.ConstraintGroup { 89 | if needed[0] == 'v' && needed[1] >= 28 && needed[1] <= 57 { 90 | needed = needed[1:] 91 | } 92 | 93 | return version.NewConstrainGroupFromString(needed + ".*") 94 | } 95 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Package configuration 2 | PROJECT = go-stable 3 | COMMANDS = cli/stable 4 | DEPENDENCIES = golang.org/x/tools/cmd/cover 5 | PACKAGES = . 6 | ORGANIZATION = mcuadros 7 | 8 | # Environment 9 | BASE_PATH := $(shell pwd) 10 | BUILD_PATH := $(BASE_PATH)/build 11 | BUILD ?= $(shell date +"%m-%d-%Y_%H_%M_%S") 12 | COMMIT ?= $(shell git log --format='%H' -n 1 | cut -c1-10) 13 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 14 | 15 | # Packages content 16 | PKG_OS = darwin linux 17 | PKG_ARCH = amd64 18 | PKG_CONTENT = LICENSE 19 | PKG_TAG = latest 20 | 21 | # Go parameters 22 | GOCMD = go 23 | GOBUILD = $(GOCMD) build 24 | GOCLEAN = $(GOCMD) clean 25 | GOGET = $(GOCMD) get -v 26 | GOTEST = $(GOCMD) test -v 27 | GHRELEASE = github-release 28 | 29 | # Coverage 30 | COVERAGE_REPORT = coverage.txt 31 | COVERAGE_MODE = atomic 32 | 33 | # Docker 34 | DOCKERCMD = docker 35 | 36 | ifneq ($(origin TRAVIS_TAG), undefined) 37 | BRANCH := $(TRAVIS_TAG) 38 | endif 39 | 40 | # Rules 41 | all: clean upload 42 | 43 | dependencies: 44 | $(GOGET) -t ./... 45 | for i in $(DEPENDENCIES); do $(GOGET) $$i; done 46 | 47 | test: dependencies 48 | for p in $(PACKAGES); do \ 49 | $(GOTEST) $${p}; \ 50 | done; 51 | 52 | test-coverage: dependencies 53 | echo "mode: $(COVERAGE_MODE)" > $(COVERAGE_REPORT); \ 54 | for p in $(PACKAGES); do \ 55 | $(GOTEST) $${p} -coverprofile=tmp_$(COVERAGE_REPORT) -covermode=$(COVERAGE_MODE); \ 56 | cat tmp_$(COVERAGE_REPORT) | grep -v "mode: $(COVERAGE_MODE)" >> $(COVERAGE_REPORT); \ 57 | rm tmp_$(COVERAGE_REPORT); \ 58 | done; 59 | 60 | packages: dependencies 61 | for os in $(PKG_OS); do \ 62 | for arch in $(PKG_ARCH); do \ 63 | cd $(BASE_PATH); \ 64 | mkdir -p $(BUILD_PATH)/$(PROJECT)_$${os}_$${arch}; \ 65 | for cmd in $(COMMANDS); do \ 66 | cd $${cmd} && GOOS=$${os} GOARCH=$${arch} $(GOCMD) build -ldflags "-X main.version=$(BRANCH) -X main.build=$(BUILD) -X main.commit=$(COMMIT)" -o $(BUILD_PATH)/$(PROJECT)_$${os}_$${arch}/$$(basename $${cmd}) .; \ 67 | cd $(BASE_PATH); \ 68 | done; \ 69 | for content in $(PKG_CONTENT); do \ 70 | cp -rf $${content} $(BUILD_PATH)/$(PROJECT)_$${os}_$${arch}/; \ 71 | done; \ 72 | cd $(BUILD_PATH) && tar -cvzf $(BUILD_PATH)/$(PROJECT)_$${os}_$${arch}.tar.gz $(PROJECT)_$${os}_$${arch}/; \ 73 | done; \ 74 | done; 75 | 76 | push: 77 | for cmd in $(COMMANDS); do \ 78 | cd $${cmd} && CGO_ENABLED=0 $(GOCMD) build -ldflags "-X main.version=$(BRANCH) -X main.build=$(BUILD) -X main.commit=$(COMMIT)" .; \ 79 | cd $(BASE_PATH); \ 80 | done; 81 | $(DOCKERCMD) build -t $(ORGANIZATION)/$(PROJECT):$(BRANCH) . 82 | $(DOCKERCMD) push $(ORGANIZATION)/$(PROJECT):$(BRANCH) 83 | 84 | clean: 85 | rm -rf $(BUILD_PATH) 86 | $(GOCLEAN) . 87 | -------------------------------------------------------------------------------- /cli/stable/cmd_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "path/filepath" 9 | 10 | "github.com/Sirupsen/logrus" 11 | "github.com/dkumor/acmewrapper" 12 | "github.com/mcuadros/go-stable" 13 | "github.com/urfave/negroni" 14 | ) 15 | 16 | const ( 17 | BaseRoute = "/{repository:[a-z0-9-/]+}.{version:v[0-9.]+}" 18 | BaseRouteOrg = "/{org:[a-z0-9-]+}/{repository:[a-z0-9-/]+}.{version:v[0-9.]+}" 19 | BaseRouteSrvOrg = "/{srv:[a-z0-9-.]+}/{org:[a-z0-9-]+}/{repository:[a-z0-9-/]+}.{version:v[0-9.]+}" 20 | ) 21 | 22 | type ServerCommand struct { 23 | Host string `long:"host" description:"host of the server"` 24 | Server string `long:"server" default:"github.com" description:"repository git server"` 25 | Organization string `long:"organization" description:"repository organization"` 26 | Repository string `long:"repository" default:"github.com" description:"repository name"` 27 | BaseRoute string `long:"base-route" description:"base gorilla/mux route"` 28 | 29 | Addr string `long:"addr" default:":443" description:"http server addr"` 30 | RedirectAddr string `long:"redirect-addr" description:"http to https redirect server addr"` 31 | CertFolder string `long:"certs" default:"/certificates" description:"TLS certificate folder"` 32 | 33 | LogLevel string `long:"log-level" default:"info" description:"log level, values: debug, info, warn or panic"` 34 | LogFormat string `long:"log-format" default:"text" description:"log format, values: text or json"` 35 | 36 | s *stable.Server 37 | redirect *http.Server 38 | } 39 | 40 | func (c *ServerCommand) Execute(args []string) error { 41 | if err := c.buildServer(); err != nil { 42 | return err 43 | } 44 | 45 | if err := c.buildMiddleware(); err != nil { 46 | return err 47 | } 48 | 49 | c.buildRedirectHTTP() 50 | return c.listen() 51 | } 52 | 53 | func (c *ServerCommand) buildServer() error { 54 | if c.Host == "" { 55 | return fmt.Errorf("missing host name, please set `--host`") 56 | } 57 | 58 | if c.BaseRoute == "" { 59 | c.BaseRoute = c.getBaseRoute() 60 | } 61 | 62 | c.s = stable.NewServer(c.BaseRoute, c.Host) 63 | c.s.Addr = c.Addr 64 | c.s.Default.Server = c.Server 65 | c.s.Default.Organization = c.Organization 66 | c.s.Default.Repository = c.Repository 67 | 68 | return nil 69 | } 70 | 71 | func (c *ServerCommand) getBaseRoute() string { 72 | if c.BaseRoute != "" { 73 | return c.BaseRoute 74 | } 75 | 76 | if c.Server == "" { 77 | return BaseRouteSrvOrg 78 | } 79 | 80 | if c.Organization == "" { 81 | return BaseRouteOrg 82 | } 83 | 84 | return BaseRoute 85 | } 86 | 87 | func (c *ServerCommand) buildMiddleware() error { 88 | logger, err := c.getLogrusMiddleware() 89 | if err != nil { 90 | return err 91 | } 92 | 93 | n := negroni.New() 94 | n.Use(negroni.NewRecovery()) 95 | n.Use(logger) 96 | n.UseHandler(c.s.Handler) 97 | 98 | c.s.Handler = n 99 | 100 | return nil 101 | } 102 | 103 | func (c *ServerCommand) getLogrusMiddleware() (negroni.Handler, error) { 104 | level, err := c.getLogLevel() 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | format, err := c.getLogFormat() 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return NewLogger(level, format), nil 115 | } 116 | 117 | func (c *ServerCommand) getLogLevel() (level logrus.Level, err error) { 118 | switch c.LogLevel { 119 | case "debug": 120 | level = logrus.DebugLevel 121 | case "info": 122 | level = logrus.InfoLevel 123 | case "warn": 124 | level = logrus.WarnLevel 125 | case "panic": 126 | level = logrus.PanicLevel 127 | default: 128 | err = fmt.Errorf("invalid log-level, %q", c.LogLevel) 129 | } 130 | 131 | return 132 | } 133 | 134 | func (c *ServerCommand) getLogFormat() (format logrus.Formatter, err error) { 135 | switch c.LogFormat { 136 | case "text": 137 | format = &logrus.TextFormatter{} 138 | case "json": 139 | format = &logrus.JSONFormatter{} 140 | default: 141 | err = fmt.Errorf("invalid log-format, %q", c.LogLevel) 142 | } 143 | 144 | return 145 | } 146 | 147 | func (c *ServerCommand) listen() error { 148 | acme, err := c.getACME() 149 | if err != nil { 150 | return err 151 | } 152 | 153 | c.s.TLSConfig = acme.TLSConfig() 154 | 155 | listener, err := tls.Listen("tcp", c.Addr, c.s.TLSConfig) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | go c.listenRedirectHTTP() 161 | return c.s.Serve(listener) 162 | } 163 | 164 | func (c *ServerCommand) getACME() (*acmewrapper.AcmeWrapper, error) { 165 | return acmewrapper.New(acmewrapper.Config{ 166 | Domains: []string{c.Host}, 167 | Address: c.Addr, 168 | TLSCertFile: filepath.Join(c.CertFolder, "cert.pem"), 169 | TLSKeyFile: filepath.Join(c.CertFolder, "key.pem"), 170 | RegistrationFile: filepath.Join(c.CertFolder, "user.reg"), 171 | PrivateKeyFile: filepath.Join(c.CertFolder, "private.pem"), 172 | TOSCallback: acmewrapper.TOSAgree, 173 | }) 174 | } 175 | 176 | func (c *ServerCommand) listenRedirectHTTP() { 177 | if c.redirect == nil { 178 | return 179 | } 180 | 181 | c.redirect.ListenAndServe() 182 | } 183 | 184 | func (c *ServerCommand) buildRedirectHTTP() { 185 | if c.RedirectAddr == "" { 186 | return 187 | } 188 | 189 | m := http.NewServeMux() 190 | m.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 191 | url := req.URL 192 | url.Scheme = "https" 193 | url.Host = c.Host 194 | 195 | _, port, _ := net.SplitHostPort(c.Addr) 196 | if port != "443" { 197 | url.Host += ":" + port 198 | } 199 | 200 | http.Redirect(w, req, url.String(), http.StatusMovedPermanently) 201 | }) 202 | 203 | c.redirect = &http.Server{ 204 | Addr: c.RedirectAddr, 205 | Handler: m, 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /proxy_test.go: -------------------------------------------------------------------------------- 1 | package stable 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | import "net/http/httptest" 11 | 12 | type ProxySuite struct{} 13 | 14 | var _ = Suite(&ProxySuite{}) 15 | 16 | func (s *ProxySuite) TestDoMetaImportResponse(c *C) { 17 | r, _ := http.NewRequest("GET", "http://foo.bar/git-fixtures/releases.v1?go-get=1", nil) 18 | s.doTestDoMetaImportResponse(c, r) 19 | } 20 | 21 | func (s *ProxySuite) TestDoMetaImportResponseSubpackage(c *C) { 22 | r, _ := http.NewRequest("GET", "http://foo.bar/git-fixtures/releases.v1/subpackage?go-get=1", nil) 23 | s.doTestDoMetaImportResponse(c, r) 24 | } 25 | 26 | func (s *ProxySuite) TestDoMetaImportResponseSubSubpackage(c *C) { 27 | r, _ := http.NewRequest("GET", "http://foo.bar/git-fixtures/releases.v1/subpackage/subsubpackage?go-get=1", nil) 28 | s.doTestDoMetaImportResponse(c, r) 29 | } 30 | 31 | func (s *ProxySuite) doTestDoMetaImportResponse(c *C, r *http.Request) { 32 | w := httptest.NewRecorder() 33 | 34 | server := NewDefaultServer("foo.bar") 35 | server.buildRouter() 36 | server.Handler.ServeHTTP(w, r) 37 | 38 | response := w.Result() 39 | body, err := ioutil.ReadAll(response.Body) 40 | c.Assert(err, IsNil) 41 | c.Assert(response.StatusCode, Equals, 200) 42 | 43 | c.Assert(string(body), Equals, ""+ 44 | "\n"+ 45 | "\t\t\n"+ 46 | "\t\t\t\n"+ 47 | "\t\t\n"+ 48 | "\t\t\n"+ 49 | "\t", 50 | ) 51 | 52 | c.Assert(response.Header.Get("Content-Type"), Equals, "text/html") 53 | } 54 | 55 | func (s *ProxySuite) TestDoUploadPackInfoResponse(c *C) { 56 | r, _ := http.NewRequest("GET", "http://foo.bar/git-fixtures/releases.v1/info/refs", nil) 57 | w := httptest.NewRecorder() 58 | 59 | server := NewDefaultServer("foo.bar") 60 | server.buildRouter() 61 | server.Handler.ServeHTTP(w, r) 62 | 63 | response := w.Result() 64 | body, err := ioutil.ReadAll(response.Body) 65 | c.Assert(err, IsNil) 66 | c.Assert(string(body), Equals, ""+ 67 | "001e# service=git-upload-pack\n"+ 68 | "0000006696f2c336f6aec28963719fb42513b88dfd709d09 HEAD\x00symref=HEAD:refs/heads/v1 symref=HEAD:refs/heads/v1\n"+ 69 | "003f96f2c336f6aec28963719fb42513b88dfd709d09 refs/heads/master\n"+ 70 | "003b96f2c336f6aec28963719fb42513b88dfd709d09 refs/heads/v1\n"+ 71 | "0000", 72 | ) 73 | 74 | c.Assert(response.StatusCode, Equals, 200) 75 | c.Assert(response.Header.Get("Content-Type"), Equals, "application/x-git-upload-pack-advertisement") 76 | } 77 | 78 | func (s *ProxySuite) TestDoUploadPackInfoResponsePrivate(c *C) { 79 | r, _ := http.NewRequest("GET", "http://foo.bar/git-fixtures/private.v1/info/refs", nil) 80 | w := httptest.NewRecorder() 81 | 82 | server := NewDefaultServer("foo.bar") 83 | server.buildRouter() 84 | server.Handler.ServeHTTP(w, r) 85 | 86 | response := w.Result() 87 | c.Assert(response.StatusCode, Equals, 401) 88 | } 89 | 90 | func (s *ProxySuite) TestDoUploadPackResponse(c *C) { 91 | r, _ := http.NewRequest("POST", "http://foo.bar/git-fixtures/releases.v1/git-upload-pack", nil) 92 | w := httptest.NewRecorder() 93 | 94 | server := NewDefaultServer("foo.bar") 95 | server.buildRouter() 96 | server.Handler.ServeHTTP(w, r) 97 | 98 | response := w.Result() 99 | body, err := ioutil.ReadAll(response.Body) 100 | c.Assert(err, IsNil) 101 | c.Assert(len(body), Equals, 1152) 102 | 103 | c.Assert(response.StatusCode, Equals, http.StatusOK) 104 | c.Assert(response.Header.Get("Content-Type"), Equals, "application/x-git-upload-pack-result") 105 | } 106 | 107 | func (s *ProxySuite) TestDoRootRedirect(c *C) { 108 | r, _ := http.NewRequest("GET", "http://foo.bar/", nil) 109 | w := httptest.NewRecorder() 110 | 111 | server := NewDefaultServer("foo.bar") 112 | server.Default.Server = "qux.baz" 113 | server.Default.Organization = "foo" 114 | 115 | server.buildRouter() 116 | server.Handler.ServeHTTP(w, r) 117 | 118 | response := w.Result() 119 | body, err := ioutil.ReadAll(response.Body) 120 | c.Assert(err, IsNil) 121 | c.Assert(len(body), Equals, 42) 122 | 123 | c.Assert(response.StatusCode, Equals, http.StatusFound) 124 | c.Assert(response.Header.Get("Location"), Equals, "https://qux.baz/foo") 125 | } 126 | 127 | func (s *ProxySuite) TestDoPackageRedirect(c *C) { 128 | r, _ := http.NewRequest("GET", "http://foo.bar/org/repository.v1", nil) 129 | w := httptest.NewRecorder() 130 | 131 | server := NewDefaultServer("foo.bar") 132 | server.buildRouter() 133 | server.Handler.ServeHTTP(w, r) 134 | 135 | response := w.Result() 136 | body, err := ioutil.ReadAll(response.Body) 137 | c.Assert(err, IsNil) 138 | c.Assert(len(body), Equals, 56) 139 | 140 | c.Assert(response.StatusCode, Equals, http.StatusFound) 141 | c.Assert(response.Header.Get("Location"), Equals, "https://github.com/org/repository") 142 | } 143 | 144 | func (s *ProxySuite) TestDoPackageRedirectSubpackage(c *C) { 145 | r, _ := http.NewRequest("GET", "http://foo.bar/org/repository.v1/subpackage", nil) 146 | w := httptest.NewRecorder() 147 | 148 | server := NewDefaultServer("foo.bar") 149 | server.buildRouter() 150 | server.Handler.ServeHTTP(w, r) 151 | 152 | response := w.Result() 153 | body, err := ioutil.ReadAll(response.Body) 154 | c.Assert(err, IsNil) 155 | c.Assert(len(body), Equals, 56) 156 | 157 | c.Assert(response.StatusCode, Equals, http.StatusFound) 158 | c.Assert(response.Header.Get("Location"), Equals, "https://github.com/org/repository") 159 | } 160 | 161 | func (s *ProxySuite) TestDoPackageRedirectSubSubpackage(c *C) { 162 | r, _ := http.NewRequest("GET", "http://foo.bar/org/repository.v1/subpackage/subsubpackage", nil) 163 | w := httptest.NewRecorder() 164 | 165 | server := NewDefaultServer("foo.bar") 166 | server.buildRouter() 167 | server.Handler.ServeHTTP(w, r) 168 | 169 | response := w.Result() 170 | body, err := ioutil.ReadAll(response.Body) 171 | c.Assert(err, IsNil) 172 | c.Assert(len(body), Equals, 56) 173 | 174 | c.Assert(response.StatusCode, Equals, http.StatusFound) 175 | c.Assert(response.Header.Get("Location"), Equals, "https://github.com/org/repository") 176 | } 177 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package stable 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "path" 9 | 10 | "strings" 11 | 12 | "github.com/gorilla/mux" 13 | "gopkg.in/src-d/go-git.v4/plumbing" 14 | "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" 15 | "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp" 16 | "gopkg.in/src-d/go-git.v4/plumbing/transport" 17 | githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" 18 | ) 19 | 20 | var ( 21 | ErrVersionNotFound = errors.New("version not found") 22 | ) 23 | 24 | const ( 25 | ServerKey = "server" 26 | OrganizationKey = "org" 27 | RepositoryKey = "repository" 28 | ConstraintKey = "version" 29 | ) 30 | 31 | func (s *Server) doRootRedirect(w http.ResponseWriter, r *http.Request) { 32 | // the redirect to the orgnization only happends with a default organization 33 | // and a default server 34 | if s.Default.Server == "" || s.Default.Organization == "" { 35 | return 36 | } 37 | 38 | url := "https://" + path.Join(s.Default.Server, s.Default.Organization) 39 | http.Redirect(w, r, url, http.StatusFound) 40 | } 41 | 42 | func (s *Server) doPackageRedirect(w http.ResponseWriter, r *http.Request) { 43 | pkg := s.buildPackage(r) 44 | http.Redirect(w, r, pkg.Repository.String(), http.StatusFound) 45 | } 46 | 47 | func (s *Server) doMetaImportResponse(w http.ResponseWriter, r *http.Request) { 48 | pkg := s.buildPackage(r) 49 | w.Header().Set("Content-Type", "text/html") 50 | fmt.Fprintf(w, metaImportTemplate, pkg.Name) 51 | } 52 | 53 | func (s *Server) doUploadPackInfoResponse(w http.ResponseWriter, r *http.Request) { 54 | pkg := s.buildPackage(r) 55 | fetcher := NewFetcher(pkg, getAuth(r)) 56 | ref, err := s.getVersion(fetcher, pkg) 57 | if err != nil { 58 | s.handleError(w, r, err) 59 | return 60 | } 61 | 62 | ref = s.mutateTagToBranch(ref, pkg.Constrain) 63 | info := s.buildGitUploadPackInfo(ref) 64 | 65 | w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement") 66 | 67 | e := pktline.NewEncoder(w) 68 | e.Encode([]byte("# service=git-upload-pack\n")) 69 | e.Flush() 70 | 71 | info.Encode(w) 72 | } 73 | 74 | func (s *Server) getVersion(f *Fetcher, pkg *Package) (*plumbing.Reference, error) { 75 | versions, err := f.Versions() 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | v := versions.BestMatch(pkg.Constrain) 81 | if v == nil { 82 | return nil, ErrVersionNotFound 83 | } 84 | 85 | return v, nil 86 | } 87 | 88 | // we mutate the tag into a branch to avoid detached branches 89 | func (s *Server) mutateTagToBranch(ref *plumbing.Reference, constraint string) *plumbing.Reference { 90 | branch := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", constraint)) 91 | return plumbing.NewHashReference(branch, ref.Hash()) 92 | } 93 | 94 | func (s *Server) buildGitUploadPackInfo(ref *plumbing.Reference) *packp.AdvRefs { 95 | h := ref.Hash() 96 | 97 | info := packp.NewAdvRefs() 98 | info.Head = &h 99 | info.AddReference(ref) 100 | info.AddReference(plumbing.NewSymbolicReference(plumbing.HEAD, ref.Name())) 101 | info.Capabilities.Set("symref", "HEAD:"+ref.Name().String()) 102 | 103 | // temporal fix due to https://github.com/golang/gddo/issues/464 104 | info.AddReference(plumbing.NewHashReference("refs/heads/master", ref.Hash())) 105 | return info 106 | } 107 | 108 | func (s *Server) buildPackage(r *http.Request) *Package { 109 | params := mux.Vars(r) 110 | server := getOrDefault(params, ServerKey, s.Default.Server) 111 | organization := getOrDefault(params, OrganizationKey, s.Default.Organization) 112 | repository := getOrDefault(params, RepositoryKey, s.Default.Repository) 113 | 114 | name, err := s.r.Get("base").URL( 115 | "server", server, 116 | "org", organization, 117 | "repository", removeSubpackage(repository), 118 | "version", params[ConstraintKey], 119 | ) 120 | 121 | if err != nil { 122 | panic(fmt.Sprintf("unreachable: %s [%s]", err.Error(), params)) 123 | } 124 | 125 | return &Package{ 126 | Name: path.Join(s.Host, name.String()), 127 | Repository: s.buildEndpoint(server, organization, repository), 128 | Constrain: params[ConstraintKey], 129 | } 130 | } 131 | 132 | func (s *Server) buildEndpoint(server, orgnization, repository string) transport.Endpoint { 133 | e, err := transport.NewEndpoint(fmt.Sprintf( 134 | "https://%s/%s/%s", server, orgnization, repository, 135 | )) 136 | 137 | if err != nil { 138 | panic(fmt.Sprintf("unreachable: %s", err.Error())) 139 | } 140 | 141 | return e 142 | } 143 | 144 | func (s *Server) doUploadPackResponse(w http.ResponseWriter, r *http.Request) { 145 | pkg := s.buildPackage(r) 146 | fetcher := NewFetcher(pkg, getAuth(r)) 147 | ref, err := s.getVersion(fetcher, pkg) 148 | if err != nil { 149 | s.handleError(w, r, err) 150 | return 151 | } 152 | 153 | w.Header().Set("Content-Type", "application/x-git-upload-pack-result") 154 | 155 | pkt := pktline.NewEncoder(w) 156 | if err := pkt.EncodeString("NAK\n"); err != nil { 157 | s.handleError(w, r, err) 158 | return 159 | } 160 | 161 | _, err = fetcher.Fetch(w, ref) 162 | if err != nil { 163 | s.handleError(w, r, err) 164 | return 165 | } 166 | } 167 | 168 | func (s *Server) handleError(w http.ResponseWriter, r *http.Request, err error) { 169 | switch err { 170 | case transport.ErrAuthorizationRequired: 171 | s.requireAuth(w, r) 172 | return 173 | case ErrVersionNotFound: 174 | w.WriteHeader(http.StatusNotFound) 175 | return 176 | } 177 | 178 | w.WriteHeader(http.StatusInternalServerError) 179 | fmt.Fprintf(os.Stderr, "error handling request: %s\n", err.Error()) 180 | } 181 | 182 | func (s *Server) requireAuth(w http.ResponseWriter, r *http.Request) { 183 | if _, _, ok := r.BasicAuth(); ok { 184 | w.WriteHeader(http.StatusNotFound) 185 | return 186 | } 187 | 188 | w.Header().Set("WWW-Authenticate", `Basic realm="go-stable"`) 189 | w.WriteHeader(http.StatusUnauthorized) 190 | } 191 | 192 | func getOrDefault(m map[string]string, key, def string) string { 193 | if v, ok := m[key]; ok { 194 | return v 195 | } 196 | 197 | return def 198 | } 199 | 200 | func getAuth(r *http.Request) *githttp.BasicAuth { 201 | username, password, _ := r.BasicAuth() 202 | 203 | return githttp.NewBasicAuth(username, password) 204 | } 205 | 206 | func removeSubpackage(pkg string) string { 207 | p := strings.Split(pkg, "/") 208 | return p[0] 209 | } 210 | 211 | var metaImportTemplate = "" + 212 | ` 213 | 214 | 215 | 216 | 217 | ` 218 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-stable [![Build Status](https://travis-ci.org/mcuadros/go-stable.svg?branch=master)](https://travis-ci.org/mcuadros/go-stable) [![codecov.io](https://codecov.io/github/mcuadros/go-stable/coverage.svg?branch=master)](https://codecov.io/github/mcuadros/go-stable?branch=master) [![GitHub release](https://img.shields.io/github/release/mcuadros/go-stable.svg)](https://github.com/mcuadros/go-stable) [![Docker Stars](https://img.shields.io/docker/pulls/mcuadros/go-stable.svg)](https://hub.docker.com/r/mcuadros/go-stable/tags/) 2 | 3 | 4 | *go-stable* is a **self-hosted** service, that provides **versioned URLs** for any **Go package**, allowing to have fixed versions for your dependencies. *go-stable* is heavily inspired by [gopkg.in](http://labix.org/gopkg.in). 5 | 6 | _How it works?_ Is a **proxy** between a git server, such as `github.com` or `bitbucket.com` and your git client, base on the requested URL (eg.: `example.com/repository.v1`) the most suitable available **tag is match** and used as **default branch**. 7 | 8 | The key features are: 9 | - [Self-hosted service](#self-hosted) 10 | - [Semantic Versioning](#semantic) 11 | - [Private repositories support](#private) 12 | - [Custom URLs](#url) 13 | 14 | 15 | 16 | ## Deploying your private go-stable 17 | 18 | *go-stable* is a self-hosted server so **you need your own domain** and server to run the service. 19 | 20 | The easiest way to deploy a *go-stable* is using Docker. Every time a new version is released our *CI* builds a new docker image, you can check all the available releases at [Docker Hub](https://hub.docker.com/r/mcuadros/go-stable/tags/) 21 | 22 | This is the bare minimum command to run a `go-stable` server: 23 | 24 | ```sh 25 | docker run -d \ 26 | -v :/certificates -p 443 27 | mcuadros/go-stable: \ 28 | stable server --host 29 | ``` 30 | 31 | Just run `stable server --help` to read all the available configuration options. 32 | 33 | Since *go-stable* runs always under a TLS server, a trusted key and certificate is required to run it. 34 | 35 | By default a new certificate is issued to your domain at [*Let's Encrypt*](https://letsencrypt.org/) using [acmewrapper](https://github.com/dkumor/acmewrapper). In order to perform the domain validation a `go-stable server` running in the port `443` is required. After the first execution you can use another port, but this is not recommended, because the _auto-renovation_ happens every two weeks. 36 | 37 | If you want to use a custom TLS key/certificate pair, maybe because your are in a private network or because you have already a valid certificated, you can place the files at the `` with the names `cert.pem` and `key.pem` 38 | 39 | ## Semantic Versioning 40 | _Semantic Versioning_ is fully supported. The `version` variable from a URL as *example.com/org/repository*.**v1** is translated to a [`go-version`](https://github.com/mcuadros/go-version) constrain, like `v1.*` 41 | 42 | This means that for example: if the repository has the following tags: `1.0`, `1.0rc1`. `1.10-dev`, the `1.0` will be chosen, but if the tags were: `1.0rc1`. `1.10-dev` the result is `1.0rc1`. 43 | 44 | *go-stable* is not very strict with the tag format, you can use `v1.0` or just `1.0`. 45 | 46 | **v0** contains more magic than expected, if none tag nor branch match, the *master* branch is returned. 47 | 48 | ## Using go-stable with private repositories 49 | 50 | *go-stable* supports private repositories, since is based on HTTP protocol. The auth is done by [basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). 51 | 52 | That means that a _user_ and _password_ should be provided, you can use the GitHub user and password (if you are not using 2FA) but we really **recommend** using a [*personal token*](https://help.github.com/articles/creating-an-access-token-for-command-line-use/) that should be used as user. This methods allows you to not disclose your password, and invalidate the token whenever you want, revoking the access to the private repos. 53 | 54 | When a package is installed through `go get` using a URL provided by *go-stable*, the terminal prompts are disabled. To provide the token (or the user and password...), you need to use a [`.netrc`](https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) file. 55 | 56 | The line to add to ` ~/.netrc` (Linux or MacOS) or `%HOME%/_netrc` (Windows) is: 57 | ``` 58 | machine login 59 | ``` 60 | 61 | If you are a bit paranoid, you can [encrypt](http://bryanwweber.com/writing/personal/2016/01/01/how-to-set-up-an-encrypted-.netrc-file-with-gpg-for-github-2fa-access/) your token or password using GPG. 62 | 63 | ## URL configuration 64 | 65 | The URL router is based on [`gorilla/mux`](https://github.com/gorilla/mux), this enables `go-stable` with extremely flexible URLs patterns. By default, a couple of routes are configured, depending on the different flags provided. 66 | 67 | ### Same git provider and user/organization (recommended setup) 68 | 69 | If all the packages are owned by the **same developer or organization** using the same provider (like *github.com*), you can specify the values for bot using the `--server` (for the provider part of the URL) and `--organization` flags. 70 | 71 | In this case, the pattern *go-stable* uses is `/{repository:[a-z0-9-/]+}.{version:v[0-9.]+}` (eg.: `example.com/repository.v1`). If we used the flag `--server github.com` and `--organization mcuadros`, the previous example will look for a version matching `v1` in the repo `github.com/mcuadros/repository`. 72 | 73 | ### If you need several organizations ... 74 | 75 | Leaving empty or not passing an `--organization` value will require the user to add a first segment in the URL to specify the developer or organization. 76 | 77 | The pattern used in this case is `/{org:[a-z0-9-]+}/{repository:[a-z0-9-/]+}.{version:v[0-9.]+}`. If the flag `--server` was configured as `github.com`, `example.com/serabe/repository.v1` would look for a version matching `v1` in `github.com/serabe`. 78 | 79 | ### Multiple providers 80 | 81 | Optionally, you can leave the `--server` empty too. In this case, a new segment would be needed at the beginning of the path specifying the provider. `example.com/github.com/mcuadros/go-stable.v1` will look for a version of `github.com/mcuadros/go-stable`. 82 | 83 | The pattern used is `/{srv:[a-z0-9-.]+}/{org:[a-z0-9-]+}/{repository:[a-z0-9-/]+}.{version:v[0-9.]+}`. 84 | 85 | ### DIY 86 | 87 | You can use any other pattern as long as you provide the router four variables: `srv`, `org`, `repository` and `version`. This feature is configured via the `--base-route` flag and the format for the pattern is specified by [`gorilla/mux`](https://github.com/gorilla/mux). 88 | 89 | 90 | License 91 | ------- 92 | 93 | MIT, see [LICENSE](LICENSE) 94 | --------------------------------------------------------------------------------