This commit is contained in:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.gitignore
|
||||
.drone.yml
|
||||
.env
|
||||
README.md
|
||||
docker-compose.yml
|
||||
21
.drone.yml
21
.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,7 +52,7 @@ volumes:
|
||||
path: /var/run/docker.sock
|
||||
- name: appdir
|
||||
host:
|
||||
path: /tmp/drone-app-deploy
|
||||
path: /opt/drone/link-shortener
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -0,0 +1 @@
|
||||
.env
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
15
go.mod
15
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
|
||||
)
|
||||
|
||||
21
go.sum
21
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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
74
internal/middleware/ratelimit.go
Normal file
74
internal/middleware/ratelimit.go
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
14
internal/middleware/security.go
Normal file
14
internal/middleware/security.go
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user