commit 4dad509dc62e36484a9eae11e11ca37e9dfac353 Author:  Ilya Atamas Date: Tue Apr 16 15:05:21 2019 +0300 feat: initial commit 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 +...