From 4dad509dc62e36484a9eae11e11ca37e9dfac353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A0Ilya=20Atamas?= Date: Tue, 16 Apr 2019 15:05:21 +0300 Subject: [PATCH] feat: initial commit --- .dockerignore | 4 +++ .gitignore | 18 +++++++++++++ Dockerfile | 27 +++++++++++++++++++ Makefile | 11 ++++++++ go.mod | 10 +++++++ go.sum | 36 +++++++++++++++++++++++++ main.go | 34 ++++++++++++++++++++++++ proxy/main.go | 24 +++++++++++++++++ proxy/metadata.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++ proxy/server.go | 65 ++++++++++++++++++++++++++++++++++++++++++++ readme.md | 10 +++++++ 11 files changed, 307 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 proxy/main.go create mode 100644 proxy/metadata.go create mode 100644 proxy/server.go create mode 100644 readme.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b6f8530 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +* +!*.go +!go.mod +!go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0e6fe8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Build +build/ + +# APFS cache +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ca9025a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# === BUILD STAGE === # +FROM golang:1.12-alpine as build + +ARG ACCESS_TOKEN + +RUN apk add --no-cache git + +WORKDIR /srv/app +ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN go test -v ./... +RUN go build -ldflags="-w -s" -o build + +# === RUN STAGE === # +FROM scratch as run + +WORKDIR /srv/app +COPY --from=build /srv/app/build /srv/app/build + +ENV LISTEN_ADDRESS 0.0.0.0:8080 +ENV GIN_MODE release + +CMD ["/srv/app/build"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d3bed69 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +.PHONY: run +run: + go run main.go + +.PHONY: build +build: + CGO_ENABLED=0 go build -ldflags="-w -s" -o build/router + +.PHONY: test +test: + go test ./... diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c6a1a28 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/emeralt/npm-cache-proxy + +go 1.12 + +require ( + github.com/gin-contrib/zap v0.0.0-20190405225521-7c4b822813e7 + github.com/gin-gonic/gin v1.3.0 + github.com/go-redis/redis v6.15.2+incompatible + go.uber.org/zap v1.9.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dbbf477 --- /dev/null +++ b/go.sum @@ -0,0 +1,36 @@ +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/zap v0.0.0-20190405225521-7c4b822813e7 h1:v6rNWmMnVDBVMc1pUUSCTobu2p4BukiPMwoj0pLqBhA= +github.com/gin-contrib/zap v0.0.0-20190405225521-7c4b822813e7/go.mod h1:pQKeeey3PeRN2SbZe1jWiIkTJkylO9hL1K0Hf4Wbtt4= +github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= +github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= +github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= +github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..40f7358 --- /dev/null +++ b/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "net/http" + "time" + + npmproxy "github.com/emeralt/npm-cache-proxy/proxy" + "github.com/go-redis/redis" +) + +func getOptions() (npmproxy.Options, error) { + return npmproxy.Options{ + RedisPrefix: "", + RedisExpireTimeout: 1 * time.Hour, + + UpstreamAddress: "http://registry.npmjs.org", + ReplaceAddress: "https://registry.npmjs.org", + StaticServerAddress: "http://localhost:8080", + }, nil +} + +func main() { + proxy := npmproxy.Proxy{ + RedisClient: redis.NewClient(&redis.Options{}), + HttpClient: &http.Client{ + Transport: http.DefaultTransport, + }, + GetOptions: getOptions, + } + + proxy.Server(npmproxy.ServerOptions{ + ListenAddress: "localhost:8080", + }).ListenAndServe() +} diff --git a/proxy/main.go b/proxy/main.go new file mode 100644 index 0000000..78a7493 --- /dev/null +++ b/proxy/main.go @@ -0,0 +1,24 @@ +package proxy + +import ( + "net/http" + "time" + + "github.com/go-redis/redis" +) + +type Proxy struct { + RedisClient *redis.Client + HttpClient *http.Client + + GetOptions func() (Options, error) +} + +type Options struct { + RedisPrefix string + RedisExpireTimeout time.Duration + + UpstreamAddress string + ReplaceAddress string + StaticServerAddress string +} diff --git a/proxy/metadata.go b/proxy/metadata.go new file mode 100644 index 0000000..3a76a18 --- /dev/null +++ b/proxy/metadata.go @@ -0,0 +1,68 @@ +package proxy + +import ( + "io/ioutil" + "net/http" + "strings" +) + +// GetMetadata returns NPM response for a given package path +func (proxy Proxy) GetMetadata(path string, header http.Header) ([]byte, error) { + options, err := proxy.GetOptions() + if err != nil { + return nil, err + } + + // get package from redis + pkg, err := proxy.RedisClient.Get(options.RedisPrefix + path).Result() + + // either package doesn't exist or there's some other problem + if err != nil { + + // check if error is caused by nonexistend package + // if no, return error + if err.Error() != "redis: nil" { + return nil, err + } + + // error is caused by nonexistent package + // fetch package + req, err := http.NewRequest("GET", options.UpstreamAddress+path, nil) + + // inherit headers from request + req.Header = header + if err != nil { + return nil, err + } + + res, err := proxy.HttpClient.Do(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + // convert body to string + pkg = string(body) + + // save to redis + _, err = proxy.RedisClient.Set( + options.RedisPrefix+path, + pkg, + options.RedisExpireTimeout, + ).Result() + if err != nil { + return nil, err + } + } + + // replace tarball urls + // FIXME: unmarshall and replace only necessary fields + convertedPkg := strings.ReplaceAll(string(pkg), options.ReplaceAddress, options.StaticServerAddress) + + return []byte(convertedPkg), nil +} diff --git a/proxy/server.go b/proxy/server.go new file mode 100644 index 0000000..89306b1 --- /dev/null +++ b/proxy/server.go @@ -0,0 +1,65 @@ +package proxy + +import ( + "net/http" + "strings" + "time" + + ginzap "github.com/gin-contrib/zap" + gin "github.com/gin-gonic/gin" + zap "go.uber.org/zap" +) + +type ServerOptions struct { + ListenAddress string +} + +func (proxy Proxy) Server(options ServerOptions) *http.Server { + router := gin.New() + + logger, _ := zap.NewProduction() + router.Use(ginzap.Ginzap(logger, time.RFC3339, true)) + + router.GET("/:scope/:name", proxy.GetPackageHandler) + router.GET("/:scope", proxy.GetPackageHandler) + router.NoRoute(proxy.NoRouteHandler) + + return &http.Server{ + Handler: router, + Addr: options.ListenAddress, + } +} + +func (proxy Proxy) GetPackageHandler(c *gin.Context) { + key := c.Request.URL.Path + + pkg, err := proxy.GetMetadata(key, c.Request.Header) + + if err != nil { + c.AbortWithError(500, err) + } else { + c.Data(200, "application/json", pkg) + } +} + +func (proxy Proxy) NoRouteHandler(c *gin.Context) { + if strings.Contains(c.Request.URL.Path, ".tgz") { + proxy.GetPackageHandler(c) + } else if c.Request.URL.Path == "/" { + _, err := proxy.RedisClient.Ping().Result() + + if err != nil { + c.AbortWithStatusJSON(503, err) + } else { + c.AbortWithStatusJSON(200, gin.H{"ok": true}) + } + } else { + options, err := proxy.GetOptions() + + if err != nil { + c.AbortWithStatusJSON(500, err) + } else { + c.Redirect(http.StatusTemporaryRedirect, options.UpstreamAddress+c.Request.URL.Path) + } + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..619ceb8 --- /dev/null +++ b/readme.md @@ -0,0 +1,10 @@ +# npm-cache-proxy + +## Installation +... + +## Configuration +... + +## Programmatic usage +...