diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 43906a8..edf6ee2 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -17,24 +17,21 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Cache Docker layers + - name: Cache Docker layers uses: actions/cache@v4 with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ github.sha }} restore-keys: | ${{ runner.os }}-buildx- - - - name: Build a dev image + - name: Build a dev image id: docker_build_dev uses: docker/build-push-action@v5 with: @@ -45,16 +42,24 @@ jobs: tags: transformimgs-dev cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache - - - name: Run tests + - name: Gosec + id: gosec run: | - docker run --entrypoint=/go/src/github.com/Pixboost/transformimgs/test.sh -v $(pwd):/go/src/github.com/Pixboost/transformimgs transformimgs-dev - - - name: Code coverage + CWD=$(pwd) + cd /tmp + curl -L https://github.com/securego/gosec/releases/download/v2.22.0/gosec_2.22.0_linux_amd64.tar.gz | tar zx + cd $CWD + /tmp/gosec -exclude-dir illustration ./... + cd illustration + /tmp/gosec ./... + - name: Run tests run: | - bash <(curl -s https://codecov.io/bash) - - - name: Build a prod image + docker run --entrypoint=/go/src/github.com/Pixboost/transformimgs/test.sh -v $(pwd):/go/src/github.com/Pixboost/transformimgs transformimgs-dev + - id: codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + - name: Build a prod image id: docker_build_prod uses: docker/build-push-action@v5 with: @@ -67,17 +72,14 @@ jobs: "BRANCH=${{ github.ref_name }}" cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache - - - name: Run the service + - name: Run the service run: | docker run -p 8080:8080 -d transformimgs sleep 5 - - - name: Smoketest + - name: Smoketest run: | curl -o /dev/null -f http://localhost:8080/img/https://pixboost.com/img/homepage/hero.jpg/resize?size=x600 - - - name: Publish image + - name: Publish image if: ${{ github.event_name == 'release' }} uses: docker/build-push-action@v5 with: diff --git a/cmd/main.go b/cmd/main.go index aefa105..0bd3c79 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "runtime" + "time" ) func main() { @@ -46,7 +47,13 @@ func main() { router.HandleFunc("/health", health.Health) img.Log.Printf("Running the application on port 8080...\n") - err = http.ListenAndServe(":8080", router) + server := http.Server{ + Addr: ":8080", + Handler: router, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + err = server.ListenAndServe() if err != nil { img.Log.Errorf("Error while stopping application: %+v", err) diff --git a/illustration/main.go b/illustration/main.go index 39697ff..3f77d4c 100644 --- a/illustration/main.go +++ b/illustration/main.go @@ -63,9 +63,9 @@ func main() { count uint currColor *imagick.PixelWand pixelsCount = uint(0) - totalPixelsCount = float32(mw.GetImageHeight() * mw.GetImageWidth()) - tenPercent = uint(totalPixelsCount * 0.1) - fiftyPercent = uint(totalPixelsCount * 0.5) + totalPixelsCount = mw.GetImageHeight() * mw.GetImageWidth() + tenPercent = totalPixelsCount / 10 + fiftyPercent = totalPixelsCount / 2 isBackground = false lastBackgroundColor *imagick.PixelWand colorsInBackground = uint(0) @@ -108,7 +108,7 @@ func main() { pixelsInBackground = 0 } else { pixelsCount += count - fiftyPercent = uint((totalPixelsCount - float32(pixelsInBackground)) * 0.5) + fiftyPercent = (totalPixelsCount - pixelsInBackground) / 2 } } default: @@ -116,6 +116,9 @@ func main() { } } + if colorIdx < 0 { + log.Fatal("colorIdx < 0") + } colorsCntIn50Pct := uint(colorIdx) - colorsInBackground fmt.Print(colorsCntIn50Pct < 10 || (float32(colorsCntIn50Pct)/float32(colorsCnt)) <= 0.02) diff --git a/img/loader/http.go b/img/loader/http.go index 5ac4147..ded6d8d 100644 --- a/img/loader/http.go +++ b/img/loader/http.go @@ -33,7 +33,7 @@ var client = &http.Client{ }, } -func (r *Http) Load(url string, _ context.Context) (*img.Image, error) { +func (r *Http) Load(url string, ctx context.Context) (*img.Image, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err @@ -44,6 +44,14 @@ func (r *Http) Load(url string, _ context.Context) (*img.Image, error) { } } + if moreHeaders, ok := img.HeaderFromContext(ctx); ok { + for k, v := range *moreHeaders { + for _, headerVal := range v { + req.Header.Add(k, headerVal) + } + } + } + resp, err := client.Do(req) if err != nil { return nil, err @@ -58,6 +66,7 @@ func (r *Http) Load(url string, _ context.Context) (*img.Image, error) { } contentType := resp.Header.Get("Content-Type") + contentEncoding := resp.Header.Get("Content-Encoding") result, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -65,8 +74,9 @@ func (r *Http) Load(url string, _ context.Context) (*img.Image, error) { } return &img.Image{ - Id: url, - Data: result, - MimeType: contentType, + Id: url, + Data: result, + MimeType: contentType, + ContentEncoding: contentEncoding, }, nil } diff --git a/img/loader/http_test.go b/img/loader/http_test.go index 971714a..2074add 100644 --- a/img/loader/http_test.go +++ b/img/loader/http_test.go @@ -2,6 +2,7 @@ package loader_test import ( "context" + "github.com/Pixboost/transformimgs/v8/img" "github.com/Pixboost/transformimgs/v8/img/loader" "github.com/dooman87/kolibri/test" "net/http" @@ -72,7 +73,7 @@ func TestHttp_LoadImgErrorResponseStatus(t *testing.T) { ) } -func TestHttp_LoadCustomHeaders(t *testing.T) { +func TestHttp_LoadCustomGlobalHeaders(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("this-is-header") != "wow" { w.WriteHeader(http.StatusInternalServerError) @@ -100,7 +101,7 @@ func TestHttp_LoadCustomHeaders(t *testing.T) { ) } -func FuzzHttp_LoadCustomHeaders(f *testing.F) { +func FuzzHttp_LoadCustomGlobalHeaders(f *testing.F) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "cool/stuff") w.Write([]byte("123")) @@ -137,3 +138,27 @@ func FuzzHttp_LoadCustomHeaders(f *testing.F) { } }) } + +func TestHttp_LoadCustomContextHeaders(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("this-is-header") != "wow" { + w.WriteHeader(http.StatusInternalServerError) + } else { + w.Header().Add("Content-Type", "cool/stuff") + w.Write([]byte("123")) + } + })) + defer server.Close() + + httpLoader := &loader.Http{} + + image, err := httpLoader.Load(server.URL, img.NewContextWithHeaders(context.Background(), &http.Header{ + "This-Is-Header": []string{"wow"}, + })) + + test.Error(t, + test.Nil(err, "error"), + test.Equal("cool/stuff", image.MimeType, "content type"), + test.Equal("123", string(image.Data), "resulted image"), + ) +} diff --git a/img/processor/imagemagick.go b/img/processor/imagemagick.go index ef6962d..9031a7b 100644 --- a/img/processor/imagemagick.go +++ b/img/processor/imagemagick.go @@ -252,7 +252,7 @@ func (p *ImageMagick) Optimise(config *img.TransformationConfig) (*img.Image, er func (p *ImageMagick) execImagemagick(in *bytes.Reader, args []string, imgId string) ([]byte, error) { var out, cmderr bytes.Buffer - cmd := exec.Command(p.convertCmd) + cmd := exec.Command(p.convertCmd) // #nosec G204 - sanitizing before assigning cmd.Args = append(cmd.Args, args...) @@ -295,7 +295,7 @@ func (p *ImageMagick) LoadImageInfo(src *img.Image) (*img.Info, error) { var out, cmderr bytes.Buffer imgId := src.Id in := bytes.NewReader(src.Data) - cmd := exec.Command(p.identifyCmd) + cmd := exec.Command(p.identifyCmd) // #nosec G204 - sanitizing before assigning cmd.Args = append(cmd.Args, "-format", "%m %Q %[opaque] %w %h", "-") cmd.Stdin = in diff --git a/img/service.go b/img/service.go index 6313218..2beda35 100644 --- a/img/service.go +++ b/img/service.go @@ -192,17 +192,23 @@ func (r *Service) AsIs(resp http.ResponseWriter, req *http.Request) { Log.Printf("Requested image %s as is\n", imgUrl) - result, err := r.Loader.Load(imgUrl, req.Context()) + var proxyHeaders = make(http.Header) + accept := req.Header.Get("Accept") + if len(accept) > 0 { + proxyHeaders.Add("Accept", accept) + } + acceptEncoding := req.Header.Get("Accept-Encoding") + if len(acceptEncoding) > 0 { + proxyHeaders.Add("Accept-Encoding", acceptEncoding) + } + + result, err := r.Loader.Load(imgUrl, NewContextWithHeaders(req.Context(), &proxyHeaders)) if err != nil { sendError(resp, err) return } - if len(result.MimeType) > 0 { - resp.Header().Add("Content-Type", result.MimeType) - } - r.execOp(&Command{ Config: &TransformationConfig{ Src: &Image{ @@ -239,11 +245,15 @@ func (r *Service) getQueue() *Queue { // Adds Content-Length and Cache-Control headers func addHeaders(resp http.ResponseWriter, image *Image) { + headers := resp.Header() if len(image.MimeType) != 0 { - resp.Header().Add("Content-Type", image.MimeType) + headers.Add("Content-Type", image.MimeType) + } + if len(image.ContentEncoding) != 0 { + headers.Add("Content-Encoding", image.ContentEncoding) } - resp.Header().Add("Content-Length", strconv.Itoa(len(image.Data))) - resp.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%d", CacheTTL)) + headers.Add("Content-Length", strconv.Itoa(len(image.Data))) + headers.Add("Cache-Control", fmt.Sprintf("public, max-age=%d", CacheTTL)) } func getQueryParam(url *url.URL, name string) (string, bool) { @@ -391,3 +401,18 @@ func sendError(resp http.ResponseWriter, err error) { } } } + +type headersKey int + +func NewContextWithHeaders(ctx context.Context, headers *http.Header) context.Context { + return context.WithValue(ctx, headersKey(0), headers) +} + +func HeaderFromContext(ctx context.Context) (*http.Header, bool) { + if ctx == nil { + return nil, false + } + + header, ok := ctx.Value(headersKey(0)).(*http.Header) + return header, ok +} diff --git a/img/service_test.go b/img/service_test.go index fe52c3f..2b8a058 100644 --- a/img/service_test.go +++ b/img/service_test.go @@ -24,6 +24,7 @@ const ( ImgLowQualityOut = "12" ImgLowerQualityOut = "1" ImgBorderTrimmed = "777" + ImgGzipSvg = "888" EmptyGifBase64Out = "R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" ) @@ -122,24 +123,38 @@ func (r *resizerMock) resultImage(config *img.TransformationConfig) *img.Image { type loaderMock struct{} -func (l *loaderMock) Load(url string, _ context.Context) (*img.Image, error) { - if url == "http://site.com/img.png" { +func (l *loaderMock) Load(url string, ctx context.Context) (*img.Image, error) { + switch url { + case "http://site.com/img.png": return &img.Image{ Data: []byte(ImgSrc), MimeType: "image/png", Id: url, }, nil - } - if url == "http://site.com/img2.png" { + case "http://site.com/img2.png": return &img.Image{ Data: []byte(NoContentTypeImgSrc), MimeType: "image/png", Id: url, }, nil - } - if url == "http://site.com/custom_error.png" { + case "http://site.com/custom_error.png": return nil, img.NewHttpError(http.StatusTeapot, "Uh oh :(") + + case "http://site.com/img.svg": + if headers, ok := img.HeaderFromContext(ctx); ok { + accept := headers.Get("Accept") + acceptEncoding := headers.Get("Accept-Encoding") + if accept == "svg" && acceptEncoding == "gzip" { + return &img.Image{ + Data: []byte(ImgSrc), + MimeType: "svg/xml", + ContentEncoding: "gzip", + Id: url, + }, nil + } + } } + return nil, errors.New("read_error") } @@ -575,6 +590,26 @@ func TestService_AsIs(t *testing.T) { ) }, }, + { + Description: "Success with custom accept encoding", + Request: &http.Request{ + Method: "GET", + URL: parseUrl("http://localhost/img/http%3A%2F%2Fsite.com/img.svg/asis", t), + Header: map[string][]string{ + "Accept-Encoding": {"gzip"}, + "Accept": {"svg"}, + }, + }, + Handler: func(w *httptest.ResponseRecorder, t *testing.T) { + test.Error(t, + test.Equal("public, max-age=86400", w.Header().Get("Cache-Control"), "Cache-Control header"), + test.Equal("3", w.Header().Get("Content-Length"), "Content-Length header"), + test.Equal("svg/xml", w.Header().Get("Content-Type"), "Content-Type header"), + test.Equal("gzip", w.Header().Get("Content-Encoding"), "Content-Encoding header"), + test.Equal("", w.Header().Get("Vary"), "No Vary header"), + ) + }, + }, { Request: &http.Request{ Method: "GET", @@ -659,6 +694,26 @@ func FuzzService_ResizeUrl(f *testing.F) { }) } +func TestHeaderFromContext_NoHeader(t *testing.T) { + header, ok := img.HeaderFromContext(context.Background()) + if header != nil { + t.Errorf("expected nil header") + } + if ok { + t.Errorf("expected ok to be false") + } +} + +func TestHeaderFromContext_NoContext(t *testing.T) { + header, ok := img.HeaderFromContext(nil) + if header != nil { + t.Errorf("expected nil header") + } + if ok { + t.Errorf("expected ok to be false") + } +} + func createService(t *testing.T) *img.Service { img.CacheTTL = 86400 s, err := img.NewService(&loaderMock{}, &resizerMock{}, 1) diff --git a/img/types.go b/img/types.go index da376ff..8c7e240 100644 --- a/img/types.go +++ b/img/types.go @@ -6,6 +6,9 @@ type Image struct { Id string Data []byte MimeType string + // Content encoding is a literally Content-Encoding header from the response + // because of the #32 (https://github.com/Pixboost/transformimgs/issues/32) + ContentEncoding string } // Info holds basic information about an image. diff --git a/run.sh b/run.sh index 0763c2b..2053400 100644 --- a/run.sh +++ b/run.sh @@ -2,11 +2,4 @@ set -e -# Fetching and installing all dependencies -# and running server on port 8080. Using this -# script to run the application inside docker -# container. - -cd cmd/ -echo 'Running Application' -go run main.go -imConvert=/usr/local/bin/convert -imIdentify=/usr/local/bin/identify $@ \ No newline at end of file +go run cmd/main.go -imConvert=/usr/local/bin/convert -imIdentify=/usr/local/bin/identify $@ diff --git a/test.sh b/test.sh index bf88069..4e498be 100755 --- a/test.sh +++ b/test.sh @@ -10,6 +10,7 @@ go test $(go list -buildvcs=false ./... | grep -v '/vendor/') -v -bench . -bench go test -fuzz=FuzzCalculateTargetSizeForResize -fuzztime 30s ./img/processor/internal/ go test -fuzz=FuzzCalculateTargetSizeForFit -fuzztime 30s ./img/processor/internal/ go test -fuzz=FuzzHttp_LoadImg -fuzztime 30s ./img/loader/ +go test -fuzz=FuzzHttp_LoadCustomGlobalHeaders -fuzztime 30s ./img/loader/ go test -fuzz=FuzzService_ResizeUrl -fuzztime 30s ./img/ echo 'Running go vet'