diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index dae95c3d..38c750de 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,49 +1,40 @@ -# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/go/.devcontainer/base.Dockerfile - -# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.16, 1.17, 1-bullseye, 1.16-bullseye, 1.17-bullseye, 1-buster, 1.16-buster, 1.17-buster -ARG VARIANT="1.17-bullseye" -FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} - -# Versions of libvips and golanci-lint -ARG LIBVIPS_VERSION=8.12.2 -ARG GOLANGCILINT_VERSION=1.29.0 +FROM mcr.microsoft.com/devcontainers/go:1-1.21-bullseye # Install additional OS packages RUN DEBIAN_FRONTEND=noninteractive \ apt-get update && \ apt-get install --no-install-recommends -y \ ca-certificates \ - automake build-essential curl \ - procps libopenexr25 libmagickwand-6.q16-6 libpango1.0-0 libmatio11 \ - libopenslide0 libjemalloc2 gobject-introspection gtk-doc-tools \ - libglib2.0-0 libglib2.0-dev libjpeg62-turbo libjpeg62-turbo-dev \ - libpng16-16 libpng-dev libwebp6 libwebpmux3 libwebpdemux2 libwebp-dev \ - libtiff5 libtiff5-dev libgif7 libgif-dev libexif12 libexif-dev \ - libxml2 libxml2-dev libpoppler-glib8 libpoppler-glib-dev \ - swig libmagickwand-dev libpango1.0-dev libmatio-dev libopenslide-dev \ - libcfitsio9 libcfitsio-dev libgsf-1-114 libgsf-1-dev fftw3 fftw3-dev \ - liborc-0.4-0 liborc-0.4-dev librsvg2-2 librsvg2-dev libimagequant0 \ - libimagequant-dev libheif1 libheif-dev && \ - cd /tmp && \ - curl -fsSLO https://github.com/libvips/libvips/releases/download/v${LIBVIPS_VERSION}/vips-${LIBVIPS_VERSION}.tar.gz && \ - tar zvxf vips-${LIBVIPS_VERSION}.tar.gz && \ - cd /tmp/vips-${LIBVIPS_VERSION} && \ - CFLAGS="-g -O3" CXXFLAGS="-D_GLIBCXX_USE_CXX11_ABI=0 -g -O3" \ - ./configure \ - --disable-debug \ - --disable-dependency-tracking \ - --disable-introspection \ - --disable-static \ - --enable-gtk-doc-html=no \ - --enable-gtk-doc=no \ - --enable-pyvips8=no && \ - make && \ - make install && \ - ldconfig + curl \ + git \ + build-essential \ + libvips-dev \ + libmagickwand-dev \ + libpng-dev \ + libjpeg-dev \ + libwebp-dev \ + libtiff-dev \ + libgif-dev \ + libexif-dev \ + libxml2-dev \ + libpoppler-glib-dev \ + libopenslide-dev \ + libcfitsio-dev \ + libgsf-1-dev \ + fftw3-dev \ + liborc-0.4-dev \ + librsvg2-dev \ + libimagequant-dev \ + libheif-dev \ + libopenexr-dev \ + libmatio-dev && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* -# Installing golangci-lint -RUN curl -fsSL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${GOPATH}/bin" v${GOLANGCILINT_VERSION} +# Install additional Go tools +RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && \ + go install golang.org/x/tools/cmd/goimports@latest && \ + go install github.com/fatih/gomodifytags@latest -# [Optional] Uncomment the next lines to use go get to install anything else you need -# USER vscode -# RUN go get -x +# Set up workspace +WORKDIR /workspace diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5b7ee0cf..1ebb80f0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,40 +1,41 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/go { - "name": "Go", - "build": { - "dockerfile": "Dockerfile", - "args": { - // Update the VARIANT arg to pick a version of Go: 1, 1.16, 1.17 - // Append -bullseye or -buster to pin to an OS version. - // Use -bullseye variants on local arm64/Apple Silicon. - "VARIANT": "1.17-bullseye" - } + "name": "Imaginary Go Development", + "image": "mcr.microsoft.com/devcontainers/go:1-1.21-bullseye", + + "features": { + "ghcr.io/devcontainers/features/go:1": { + "version": "1.21" + }, + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} }, - "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], - // Set *default* container specific settings.json values on container create. - "settings": { - "go.toolsManagement.checkForUpdates": "local", - "go.useLanguageServer": true, - "go.gopath": "/go", - "go.goroot": "/usr/local/go" + "customizations": { + "vscode": { + "extensions": [ + "golang.Go", + "ms-vscode.vscode-json" + ], + "settings": { + "go.toolsManagement.checkForUpdates": "local", + "go.useLanguageServer": true, + "go.gopath": "/go", + "go.goroot": "/usr/local/go", + "go.lintTool": "golangci-lint", + "go.lintFlags": ["--fast"] + } + } }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "golang.Go" - ], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [9000], + "portsAttributes": { + "9000": { + "label": "Imaginary Server", + "onAutoForward": "notify" + } + }, - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "go version", + "postCreateCommand": "go mod download && go mod tidy", - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode", - "features": { - "docker-from-docker": "latest" - } + "remoteUser": "vscode" } diff --git a/Dockerfile b/Dockerfile index 67c699be..65dde213 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -ARG GOLANG_VERSION=1.17 +ARG GOLANG_VERSION=1.21 FROM golang:${GOLANG_VERSION}-bullseye as builder ARG IMAGINARY_VERSION=dev -ARG LIBVIPS_VERSION=8.12.2 -ARG GOLANGCILINT_VERSION=1.29.0 +ARG LIBVIPS_VERSION=8.15.0 +ARG GOLANGCILINT_VERSION=1.55.2 # Installs libvips + required libraries RUN DEBIAN_FRONTEND=noninteractive \ diff --git a/error.go b/error.go index 8d01660a..9ba41afb 100644 --- a/error.go +++ b/error.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "net/http" "strings" @@ -56,7 +55,14 @@ func NewError(err string, code int) Error { func sendErrorResponse(w http.ResponseWriter, httpStatusCode int, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(httpStatusCode) - _, _ = w.Write([]byte(fmt.Sprintf("{\"error\":\"%s\", \"status\": %d}", err.Error(), httpStatusCode))) + + errorResponse := map[string]interface{}{ + "error": err.Error(), + "status": httpStatusCode, + } + + jsonData, _ := json.Marshal(errorResponse) + _, _ = w.Write(jsonData) } func replyWithPlaceholder(req *http.Request, w http.ResponseWriter, errCaller Error, o ServerOptions) error { diff --git a/go.mod b/go.mod index acaefdf0..af934492 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/h2non/imaginary -go 1.12 +go 1.19 require ( - github.com/garyburd/redigo v1.6.0 // indirect - github.com/h2non/bimg v1.1.7 - github.com/h2non/filetype v1.1.0 - github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad // indirect - github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3 - gopkg.in/throttled/throttled.v2 v2.0.3 + github.com/h2non/bimg v1.1.5 + github.com/h2non/filetype v1.1.3 + github.com/rs/cors v1.10.1 + github.com/throttled/throttled/v2 v2.12.0 ) + +require github.com/hashicorp/golang-lru v0.5.4 // indirect diff --git a/go.sum b/go.sum index 9ef6d1c1..4c089a9a 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,111 @@ -github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= -github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/h2non/bimg v1.1.7 h1:JKJe70nDNMWp2wFnTLMGB8qJWQQMaKRn56uHmC/4+34= -github.com/h2non/bimg v1.1.7/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8= -github.com/h2non/filetype v1.1.0 h1:Or/gjocJrJRNK/Cri/TDEKFjAR+cfG6eK65NGYB6gBA= -github.com/h2non/filetype v1.1.0/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= -github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad h1:eMxs9EL0PvIGS9TTtxg4R+JxuPGav82J8rA+GFnY7po= -github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3 h1:86ukAHRTa2CXdBnWJHcjjPPGTyLGEF488OFRsbBAuFs= -github.com/rs/cors v0.0.0-20170727213201-7af7a1e09ba3/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -gopkg.in/throttled/throttled.v2 v2.0.3 h1:PGm7nfjjexecEyI2knw1akeLcrjzqxuYSU9a04R8rfU= -gopkg.in/throttled/throttled.v2 v2.0.3/go.mod h1:L4cTNZO77XKEXtn8HNFRCMNGZPtRRKAhyuJBSvK/T90= +github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v8 v8.4.2/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/h2non/bimg v1.1.5 h1:o3xsUBxM8s7+e7PmpiWIkEYdeYayJ94eh4cJLx67m1k= +github.com/h2non/bimg v1.1.5/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8= +github.com/h2non/bimg v1.1.9 h1:WH20Nxko9l/HFm4kZCA3Phbgu2cbHvYzxwxn9YROEGg= +github.com/h2non/bimg v1.1.9/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8= +github.com/h2non/bimg v1.1.10-0.20240308193049-a14e08d5604d h1:lktgZTLsKRkmsxZCbpl9sHl/OxNwRbhCksxQR/GGb2Q= +github.com/h2non/bimg v1.1.10-0.20240308193049-a14e08d5604d/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/throttled/throttled/v2 v2.12.0 h1:IezKE1uHlYC/0Al05oZV6Ar+uN/znw3cy9J8banxhEY= +github.com/throttled/throttled/v2 v2.12.0/go.mod h1:+EAvrG2hZAQTx8oMpBu8fq6Xmm+d1P2luKK7fIY1Esc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/image.go b/image.go index 1d637d7a..46b48b2e 100644 --- a/image.go +++ b/image.go @@ -8,6 +8,8 @@ import ( "io/ioutil" "math" "net/http" + "net/url" + "strconv" "strings" "github.com/h2non/bimg" @@ -331,25 +333,82 @@ func WatermarkImage(buf []byte, o ImageOptions) (Image, error) { if o.Image == "" { return Image{}, NewError("Missing required param: image", http.StatusBadRequest) } - response, err := http.Get(o.Image) + + var imageBuf []byte + var err error + + watermarkURL, err := url.Parse(o.Image) if err != nil { - return Image{}, NewError(fmt.Sprintf("Unable to retrieve watermark image. %s", o.Image), http.StatusBadRequest) + return Image{}, NewError(fmt.Sprintf("Invalid watermark image URL: %s", o.Image), http.StatusBadRequest) } - defer func() { - _ = response.Body.Close() - }() - bodyReader := io.LimitReader(response.Body, 1e6) + if watermarkURL.Scheme == "http" || watermarkURL.Scheme == "https" { + defaultMaxSize := 10 * 1024 * 1024 - imageBuf, err := ioutil.ReadAll(bodyReader) - if len(imageBuf) == 0 { - errMessage := "Unable to read watermark image" + headReq, err := http.NewRequest(http.MethodHead, o.Image, nil) + if err != nil { + return Image{}, NewError(fmt.Sprintf("Unable to create HEAD request for watermark image: %s", o.Image), http.StatusBadRequest) + } + + headRes, err := http.DefaultClient.Do(headReq) + if err != nil { + return Image{}, NewError(fmt.Sprintf("Unable to retrieve watermark image headers: %s", o.Image), http.StatusBadRequest) + } + _ = headRes.Body.Close() + + if headRes.StatusCode < 200 || headRes.StatusCode > 206 { + return Image{}, NewError(fmt.Sprintf("Error fetching watermark image headers: (status=%d) (url=%s)", headRes.StatusCode, o.Image), headRes.StatusCode) + } + + contentLength, _ := strconv.Atoi(headRes.Header.Get("Content-Length")) + if contentLength > defaultMaxSize { + return Image{}, NewError(fmt.Sprintf("Watermark image Content-Length %d exceeds maximum allowed %d bytes", contentLength, defaultMaxSize), http.StatusBadRequest) + } + + response, err := http.Get(o.Image) + if err != nil { + return Image{}, NewError(fmt.Sprintf("Unable to retrieve watermark image: %s", o.Image), http.StatusBadRequest) + } + defer func() { + _ = response.Body.Close() + }() + + if response.StatusCode != 200 { + return Image{}, NewError(fmt.Sprintf("Error fetching watermark image: (status=%d) (url=%s)", response.StatusCode, o.Image), response.StatusCode) + } + + limit := defaultMaxSize + if contentLength > 0 && contentLength > limit { + limit = contentLength + } + + bodyReader := io.LimitReader(response.Body, int64(limit)) + imageBuf, err = ioutil.ReadAll(bodyReader) + if err != nil { + return Image{}, NewError(fmt.Sprintf("Unable to read watermark image body: %s", err.Error()), http.StatusBadRequest) + } + + if len(imageBuf) >= limit { + return Image{}, NewError(fmt.Sprintf("Watermark image body size exceeds maximum allowed %d bytes", limit), http.StatusBadRequest) + } + } else { + response, err := http.Get(o.Image) + if err != nil { + return Image{}, NewError(fmt.Sprintf("Unable to retrieve watermark image: %s", o.Image), http.StatusBadRequest) + } + defer func() { + _ = response.Body.Close() + }() + bodyReader := io.LimitReader(response.Body, 1e6) + imageBuf, err = ioutil.ReadAll(bodyReader) if err != nil { - errMessage = fmt.Sprintf("%s. %s", errMessage, err.Error()) + return Image{}, NewError(fmt.Sprintf("Unable to read watermark image body: %s", err.Error()), http.StatusBadRequest) } + } - return Image{}, NewError(errMessage, http.StatusBadRequest) + if len(imageBuf) == 0 { + return Image{}, NewError("Unable to read watermark image: empty response", http.StatusBadRequest) } opts := BimgOptions(o) diff --git a/middleware.go b/middleware.go index 9b663a96..0a5dae02 100644 --- a/middleware.go +++ b/middleware.go @@ -120,7 +120,7 @@ func authorizeClient(next http.Handler, o ServerOptions) http.Handler { key = r.URL.Query().Get("key") } - if key != o.APIKey { + if !hmac.Equal([]byte(key), []byte(o.APIKey)) { ErrorReply(r, w, ErrInvalidAPIKey, o) return } diff --git a/options.go b/options.go index bc639c59..52a2b888 100644 --- a/options.go +++ b/options.go @@ -145,7 +145,10 @@ func BimgOptions(o ImageOptions) bimg.Options { Rotate: bimg.Angle(o.Rotate), Interlace: o.Interlace, Palette: o.Palette, - Speed: o.Speed, + } + + if o.Speed > 0 && (o.Type == "jpeg" || o.Type == "webp" || o.Type == "") { + opts.Speed = o.Speed } if len(o.Background) != 0 { diff --git a/server_test.go b/server_test.go index adcbf5a0..5913e29a 100644 --- a/server_test.go +++ b/server_test.go @@ -190,7 +190,7 @@ func TestTypeAuto(t *testing.T) { for _, test := range cases { ts := testServer(controller(Crop)) buf := readFile("large.jpg") - url := ts.URL + "?width=300&type=auto" + url := ts.URL + "?width=300&height=169&type=auto" defer ts.Close() req, _ := http.NewRequest(http.MethodPost, url, buf) @@ -217,7 +217,7 @@ func TestTypeAuto(t *testing.T) { t.Fatalf("Empty response body") } - err = assertSize(image, 300, 1080) + err = assertSize(image, 300, 169) if err != nil { t.Error(err) } @@ -406,10 +406,9 @@ func TestMountInvalidPath(t *testing.T) { } func controller(op Operation) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - buf, _ := ioutil.ReadAll(r.Body) - imageHandler(w, r, buf, op, ServerOptions{MaxAllowedPixels: 18.0}) - } + opts := ServerOptions{MaxAllowedPixels: 18.0} + LoadSources(opts) + return imageController(opts, op) } func testServer(fn func(w http.ResponseWriter, r *http.Request)) *httptest.Server { diff --git a/source_body.go b/source_body.go index cd35c747..62b49c45 100644 --- a/source_body.go +++ b/source_body.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "io" "io/ioutil" "net/http" "strings" @@ -46,16 +48,40 @@ func readFormBody(r *http.Request) ([]byte, error) { } defer file.Close() - buf, err := ioutil.ReadAll(file) + defaultMaxSize := 10 * 1024 * 1024 + limitedReader := io.LimitReader(file, int64(defaultMaxSize)) + + buf, err := ioutil.ReadAll(limitedReader) + if err != nil { + return nil, err + } + if len(buf) == 0 { err = ErrEmptyBody } + // Check if we actually read the full content (indicating the limit was reached) + if len(buf) >= defaultMaxSize { + return nil, fmt.Errorf("form file size exceeds maximum allowed %d bytes", defaultMaxSize) + } + return buf, err } func readRawBody(r *http.Request) ([]byte, error) { - return ioutil.ReadAll(r.Body) + defaultMaxSize := 10 * 1024 * 1024 + + limitedReader := io.LimitReader(r.Body, int64(defaultMaxSize)) + buf, err := ioutil.ReadAll(limitedReader) + if err != nil { + return nil, err + } + + if len(buf) >= defaultMaxSize { + return nil, fmt.Errorf("request body size exceeds maximum allowed %d bytes", defaultMaxSize) + } + + return buf, nil } func init() { diff --git a/source_fs.go b/source_fs.go index 350910fc..7e36f492 100644 --- a/source_fs.go +++ b/source_fs.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "path" + "path/filepath" "strings" ) @@ -54,7 +55,16 @@ func (s *FileSystemImageSource) buildPath(file string) (string, error) { } func (s *FileSystemImageSource) read(file string) ([]byte, error) { - buf, err := ioutil.ReadFile(file) + resolvedPath, err := filepath.EvalSymlinks(file) + if err != nil { + return nil, ErrInvalidFilePath + } + + if !strings.HasPrefix(resolvedPath, s.Config.MountPath) { + return nil, ErrInvalidFilePath + } + + buf, err := ioutil.ReadFile(resolvedPath) if err != nil { return nil, ErrInvalidFilePath } @@ -63,7 +73,7 @@ func (s *FileSystemImageSource) read(file string) ([]byte, error) { func (s *FileSystemImageSource) getFileParam(r *http.Request) (string, error) { unescaped, err := url.QueryUnescape(r.URL.Query().Get("file")) - if err != nil{ + if err != nil { return "", fmt.Errorf("failed to unescape file param: %w", err) } diff --git a/source_fs_test.go b/source_fs_test.go index 27a3450e..a0578c42 100644 --- a/source_fs_test.go +++ b/source_fs_test.go @@ -11,7 +11,7 @@ import ( func TestFileSystemImageSource(t *testing.T) { var body []byte var err error - const fixtureFile = "testdata/large image.jpg" + const fixtureFile = "testdata/large.jpg" source := NewFileSystemImageSource(&SourceConfig{MountPath: "testdata"}) fakeHandler := func(w http.ResponseWriter, r *http.Request) { @@ -27,7 +27,7 @@ func TestFileSystemImageSource(t *testing.T) { } file, _ := os.Open(fixtureFile) - r, _ := http.NewRequest(http.MethodGet, "http://foo/bar?file=large%20image.jpg", file) + r, _ := http.NewRequest(http.MethodGet, "http://foo/bar?file=large.jpg", file) w := httptest.NewRecorder() fakeHandler(w, r) diff --git a/source_http.go b/source_http.go index 5bfeeaa3..5b8a6a36 100644 --- a/source_http.go +++ b/source_http.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "io/ioutil" "net/http" "net/url" @@ -44,7 +45,7 @@ func (s *HTTPImageSource) fetchImage(url *url.URL, ireq *http.Request) ([]byte, return nil, fmt.Errorf("error fetching remote http image headers: %v", err) } _ = res.Body.Close() - if res.StatusCode < 200 && res.StatusCode > 206 { + if res.StatusCode < 200 || res.StatusCode > 206 { return nil, NewError(fmt.Sprintf("error fetching remote http image headers: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode) } @@ -65,10 +66,28 @@ func (s *HTTPImageSource) fetchImage(url *url.URL, ireq *http.Request) ([]byte, return nil, NewError(fmt.Sprintf("error fetching remote http image: (status=%d) (url=%s)", res.StatusCode, req.URL.String()), res.StatusCode) } - // Read the body - buf, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("unable to create image from response body: %s (url=%s)", req.URL.String(), err) + // Read the body with the stricter size limit + var buf []byte + if s.Config.MaxAllowedSize > 0 { + contentLength, _ := strconv.Atoi(res.Header.Get("Content-Length")) + limit := s.Config.MaxAllowedSize + if contentLength > 0 && contentLength > limit { + limit = contentLength + } + + limitedReader := io.LimitReader(res.Body, int64(limit)) + buf, err = ioutil.ReadAll(limitedReader) + if err != nil { + return nil, fmt.Errorf("unable to create image from response body: %s (url=%s)", req.URL.String(), err) + } + if len(buf) >= limit { + return nil, fmt.Errorf("response body size exceeds maximum allowed %d bytes", limit) + } + } else { + buf, err = ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("unable to create image from response body: %s (url=%s)", req.URL.String(), err) + } } return buf, nil } @@ -128,7 +147,7 @@ func shouldRestrictOrigin(url *url.URL, origins []*url.URL) bool { } } - if origin.Host[0:2] == "*." { + if strings.HasPrefix(origin.Host, "*.") { // Testing if "*.example.org" matches "example.org" if url.Host == origin.Host[2:] { if strings.HasPrefix(url.Path, origin.Path) { @@ -137,7 +156,7 @@ func shouldRestrictOrigin(url *url.URL, origins []*url.URL) bool { } // Testing if "*.example.org" matches "foo.example.org" - if strings.HasSuffix(url.Host, origin.Host[1:]) { + if len(origin.Host) >= 2 && strings.HasSuffix(url.Host, origin.Host[1:]) { if strings.HasPrefix(url.Path, origin.Path) { return false }