diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7fdd10a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.gitignore +.drone.yml +.env +README.md +docker-compose.yml diff --git a/.drone.yml b/.drone.yml index aeb4a18..9fd573b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,13 +11,13 @@ steps: - go test ./... - name: build and push docker image - image: docker:24 - volumes: - - name: dockersock - path: /var/run/docker.sock - commands: - - docker build -t localhost:5000/link-shortener:latest -f dockerfile . - - docker push localhost:5000/link-shortener:latest + image: plugins/docker + settings: + repo: localhost:5000/link-shortener + tags: latest + dockerfile: dockerfile + registry: localhost:5000 + insecure: true - name: deploy with compose image: docker:24 @@ -35,7 +35,10 @@ steps: LETSENCRYPT_EMAIL: from_secret: LETSENCRYPT_EMAIL SERVER_PORT: 8080 - REDIS_URL: redis://redis:6379 + REDIS_PASSWORD: + from_secret: REDIS_PASSWORD + REDIS_URL: + from_secret: REDIS_URL commands: - apk add --no-cache docker-cli-compose - cp -r . /opt/app @@ -49,8 +52,8 @@ volumes: path: /var/run/docker.sock - name: appdir host: - path: /tmp/drone-app-deploy + path: /opt/drone/link-shortener trigger: branch: - - main \ No newline at end of file + - main diff --git a/.gitignore b/.gitignore index e69de29..2eea525 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 0a2ad5f..bab00ba 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,7 +35,10 @@ func run() error { address := fmt.Sprintf("0.0.0.0:%s", cfg.ServerPort) repo := repositories.NewLinksRepository(database) - service := services.NewLinksService(repo) + service, err := services.NewLinksService(repo) + if err != nil { + log.Fatalf("failed to initialize links service: %v", err) + } handler := handlers.NewLinksHandler(service, cfg.ServerHost, cfg.ServerPort) server := httpserver.NewServer(address, handler) diff --git a/docker-compose.yml b/docker-compose.yml index 294343d..17764fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,14 @@ services: - SERVER_PORT=${SERVER_PORT} - REDIS_URL=${REDIS_URL} restart: unless-stopped + read_only: true + security_opt: + - no-new-privileges:true + deploy: + resources: + limits: + memory: 256M + cpus: '0.50' healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:8080/ping"] interval: 5s @@ -27,15 +35,25 @@ services: start_period: 10s redis: - image: redis:latest + image: redis:7.4-alpine container_name: link-shortener-redis networks: [isolated] restart: unless-stopped + read_only: true + security_opt: + - no-new-privileges:true + deploy: + resources: + limits: + memory: 256M + cpus: '0.50' volumes: - redis_data:/data - command: ["redis-server", "--appendonly", "yes"] + tmpfs: + - /tmp + command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 5s timeout: 3s retries: 5 @@ -49,4 +67,4 @@ networks: isolated: driver: bridge proxy: - external: true \ No newline at end of file + external: true diff --git a/dockerfile b/dockerfile index 37acbe5..934a605 100644 --- a/dockerfile +++ b/dockerfile @@ -7,12 +7,19 @@ RUN go mod download && go mod verify COPY . ./ -RUN go build -o bin/application ./cmd +RUN CGO_ENABLED=0 go build -o bin/application ./cmd FROM alpine:3.21 AS runner +RUN addgroup -S app && adduser -S app -G app + WORKDIR /opt +COPY --from=builder --chown=app:app /opt/bin/application ./ + +USER app + +CMD ["./application"] COPY --from=builder /opt/bin/application ./ CMD ["./application"] \ No newline at end of file diff --git a/go.mod b/go.mod index a35c6b4..5f95c6c 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,14 @@ module example.com/m go 1.25.3 +require ( + github.com/gin-gonic/gin v1.11.0 + github.com/joho/godotenv v1.5.1 + github.com/redis/go-redis/v9 v9.17.3 + github.com/sqids/sqids-go v0.4.1 + golang.org/x/time v0.14.0 +) + require ( github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect @@ -11,13 +19,11 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-gonic/gin v1.11.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -27,18 +33,13 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect - github.com/redis/go-redis/v9 v9.17.3 // indirect - github.com/sqids/sqids-go v0.4.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.uber.org/mock v0.6.0 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/crypto v0.47.0 // indirect - golang.org/x/mod v0.32.0 // indirect golang.org/x/net v0.49.0 // indirect - golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.41.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index a770ee4..829ba43 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= @@ -9,6 +13,7 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= @@ -18,6 +23,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -28,6 +35,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= @@ -46,6 +55,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= @@ -64,6 +74,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= @@ -74,21 +86,18 @@ golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/db/redis.go b/internal/db/redis.go index cf25e7a..d91fe0c 100644 --- a/internal/db/redis.go +++ b/internal/db/redis.go @@ -2,32 +2,27 @@ package db import ( "context" - "log" + "fmt" "time" "example.com/m/internal/config" "github.com/redis/go-redis/v9" ) -var Redis *redis.Client - func InitRedis(cfg *config.Config) (*redis.Client, error) { opt, err := redis.ParseURL(cfg.RedisURL) - if err != nil { - log.Fatalf("impossible to parse redis url: %v", err) - return nil, err + return nil, fmt.Errorf("failed to parse redis url: %w", err) } - Redis = redis.NewClient(opt) + client := redis.NewClient(opt) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if err := Redis.Ping(ctx).Err(); err != nil { - log.Fatalf("impossible to conenct to redis db: %v", err) - return nil, err + if err := client.Ping(ctx).Err(); err != nil { + return nil, fmt.Errorf("failed to connect to redis: %w", err) } - return Redis, nil + return client, nil } diff --git a/internal/handlers/links.go b/internal/handlers/links.go index d5f93ae..52cda7e 100644 --- a/internal/handlers/links.go +++ b/internal/handlers/links.go @@ -3,6 +3,7 @@ package handlers import ( "errors" "fmt" + "net" "net/http" "net/url" "strings" @@ -47,7 +48,7 @@ func (h *LinksHandler) CreateLink(c *gin.Context) { return } - address := fmt.Sprintf("http://%v:%s/r/%v", h.host, h.port, id) + address := fmt.Sprintf("https://%s/r/%s", h.host, id) response := CreateLinkResponse{ Status: "success", @@ -89,5 +90,44 @@ func NormalizeURL(raw string) (string, error) { return "", errors.New("invalid host in URL") } + host := u.Hostname() + if isPrivateHost(host) { + return "", errors.New("URLs pointing to private/internal addresses are not allowed") + } + return u.String(), nil } + +func isPrivateHost(host string) bool { + ip := net.ParseIP(host) + if ip == nil { + addrs, err := net.LookupHost(host) + if err != nil || len(addrs) == 0 { + return false + } + ip = net.ParseIP(addrs[0]) + if ip == nil { + return false + } + } + + privateRanges := []string{ + "127.0.0.0/8", + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + "169.254.0.0/16", + "0.0.0.0/8", + "::1/128", + "fc00::/7", + "fe80::/10", + } + + for _, cidr := range privateRanges { + _, network, _ := net.ParseCIDR(cidr) + if network.Contains(ip) { + return true + } + } + return false +} diff --git a/internal/http/server.go b/internal/http/server.go index 5fd9027..7f692e4 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -2,10 +2,10 @@ package http import ( "fmt" - "log" "net/http" "example.com/m/internal/handlers" + "example.com/m/internal/middleware" "github.com/gin-gonic/gin" ) @@ -20,7 +20,12 @@ type Server struct { func NewServer(address string, lh *handlers.LinksHandler) *Server { engine := gin.New() + engine.Use(gin.Logger()) engine.Use(gin.Recovery()) + engine.Use(middleware.SecurityHeaders()) + + rl := middleware.NewRateLimiter(10, 20) + engine.Use(rl.Middleware()) return &Server{ address: address, @@ -32,11 +37,11 @@ func NewServer(address string, lh *handlers.LinksHandler) *Server { func (s *Server) Start() error { s.routes() + fmt.Printf("starting server on %s\n", s.address) + if err := s.engine.Run(s.address); err != nil && err != http.ErrServerClosed { - log.Fatalf("error occured while starting the server: %v", err) - return err + return fmt.Errorf("error occurred while starting the server: %w", err) } - fmt.Println("server started: &v", s.address) return nil } diff --git a/internal/middleware/ratelimit.go b/internal/middleware/ratelimit.go new file mode 100644 index 0000000..9be1731 --- /dev/null +++ b/internal/middleware/ratelimit.go @@ -0,0 +1,74 @@ +package middleware + +import ( + "net/http" + "sync" + "time" + + "github.com/gin-gonic/gin" + "golang.org/x/time/rate" +) + +type client struct { + limiter *rate.Limiter + lastSeen time.Time +} + +type RateLimiter struct { + clients map[string]*client + mu sync.Mutex + rps rate.Limit + burst int +} + +func NewRateLimiter(requestsPerSecond float64, burst int) *RateLimiter { + rl := &RateLimiter{ + clients: make(map[string]*client), + rps: rate.Limit(requestsPerSecond), + burst: burst, + } + go rl.cleanup() + return rl +} + +func (rl *RateLimiter) getClient(ip string) *rate.Limiter { + rl.mu.Lock() + defer rl.mu.Unlock() + + if c, exists := rl.clients[ip]; exists { + c.lastSeen = time.Now() + return c.limiter + } + + limiter := rate.NewLimiter(rl.rps, rl.burst) + rl.clients[ip] = &client{limiter: limiter, lastSeen: time.Now()} + return limiter +} + +func (rl *RateLimiter) cleanup() { + for { + time.Sleep(time.Minute) + rl.mu.Lock() + for ip, c := range rl.clients { + if time.Since(c.lastSeen) > 3*time.Minute { + delete(rl.clients, ip) + } + } + rl.mu.Unlock() + } +} + +func (rl *RateLimiter) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + ip := c.ClientIP() + limiter := rl.getClient(ip) + if !limiter.Allow() { + c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ + "error": "failed", + "message": "rate limit exceeded", + }) + return + } + c.Next() + } +} diff --git a/internal/middleware/security.go b/internal/middleware/security.go new file mode 100644 index 0000000..43fbba8 --- /dev/null +++ b/internal/middleware/security.go @@ -0,0 +1,14 @@ +package middleware + +import "github.com/gin-gonic/gin" + +func SecurityHeaders() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("X-Content-Type-Options", "nosniff") + c.Header("X-Frame-Options", "DENY") + c.Header("X-XSS-Protection", "1; mode=block") + c.Header("Referrer-Policy", "strict-origin-when-cross-origin") + c.Header("Content-Security-Policy", "default-src 'none'") + c.Next() + } +} diff --git a/internal/services/links.go b/internal/services/links.go index b3111b5..5fdaebc 100644 --- a/internal/services/links.go +++ b/internal/services/links.go @@ -1,6 +1,7 @@ package services import ( + "fmt" "math/rand" "example.com/m/internal/repositories" @@ -12,17 +13,23 @@ type LinksService struct { sqid *sqids.Sqids } -func NewLinksService(repo *repositories.LinksRepository) *LinksService { - s, _ := sqids.New() +func NewLinksService(repo *repositories.LinksRepository) (*LinksService, error) { + s, err := sqids.New() + if err != nil { + return nil, fmt.Errorf("failed to initialize sqids: %w", err) + } return &LinksService{ repo: repo, sqid: s, - } + }, nil } func (s *LinksService) CreateLink(original string) (string, error) { - id, _ := s.sqid.Encode([]uint64{rand.Uint64()}) - err := s.repo.CreateLink(id, original) + id, err := s.sqid.Encode([]uint64{rand.Uint64()}) + if err != nil { + return "", fmt.Errorf("failed to encode link id: %w", err) + } + err = s.repo.CreateLink(id, original) return id, err }