Import existing project
This commit is contained in:
parent
7887817595
commit
80b0cc4939
125 changed files with 16980 additions and 0 deletions
BIN
checkpoint_service/data/GeoLite2-ASN.mmdb
Normal file
BIN
checkpoint_service/data/GeoLite2-ASN.mmdb
Normal file
Binary file not shown.
BIN
checkpoint_service/data/GeoLite2-Country.mmdb
Normal file
BIN
checkpoint_service/data/GeoLite2-Country.mmdb
Normal file
Binary file not shown.
1
checkpoint_service/data/checkpoint_secret.json
Normal file
1
checkpoint_service/data/checkpoint_secret.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"hmac_secret":"HJ9EY5oN0pO71AUN/2faoYnJhZyTr875iWbw6Nl8rc8=","created_at":"2025-04-28T18:28:59.6204724-05:00","updated_at":"2025-04-28T18:33:55.3259956-05:00"}
|
||||
BIN
checkpoint_service/data/checkpoint_tokendb/000001.sst
Normal file
BIN
checkpoint_service/data/checkpoint_tokendb/000001.sst
Normal file
Binary file not shown.
BIN
checkpoint_service/data/checkpoint_tokendb/000002.sst
Normal file
BIN
checkpoint_service/data/checkpoint_tokendb/000002.sst
Normal file
Binary file not shown.
BIN
checkpoint_service/data/checkpoint_tokendb/000002.vlog
Normal file
BIN
checkpoint_service/data/checkpoint_tokendb/000002.vlog
Normal file
Binary file not shown.
BIN
checkpoint_service/data/checkpoint_tokendb/000003.sst
Normal file
BIN
checkpoint_service/data/checkpoint_tokendb/000003.sst
Normal file
Binary file not shown.
BIN
checkpoint_service/data/checkpoint_tokendb/000003.vlog
Normal file
BIN
checkpoint_service/data/checkpoint_tokendb/000003.vlog
Normal file
Binary file not shown.
BIN
checkpoint_service/data/checkpoint_tokendb/DISCARD
Normal file
BIN
checkpoint_service/data/checkpoint_tokendb/DISCARD
Normal file
Binary file not shown.
1
checkpoint_service/data/checkpoint_tokendb/KEYREGISTRY
Normal file
1
checkpoint_service/data/checkpoint_tokendb/KEYREGISTRY
Normal file
|
|
@ -0,0 +1 @@
|
|||
~t(ðæ4¥
ø2³Dî•æHello Badger
|
||||
BIN
checkpoint_service/data/checkpoint_tokendb/MANIFEST
Normal file
BIN
checkpoint_service/data/checkpoint_tokendb/MANIFEST
Normal file
Binary file not shown.
BIN
checkpoint_service/data/data/GeoLite2-ASN.mmdb
Normal file
BIN
checkpoint_service/data/data/GeoLite2-ASN.mmdb
Normal file
Binary file not shown.
BIN
checkpoint_service/data/data/GeoLite2-Country.mmdb
Normal file
BIN
checkpoint_service/data/data/GeoLite2-Country.mmdb
Normal file
Binary file not shown.
1
checkpoint_service/data/data/checkpoint_secret.json
Normal file
1
checkpoint_service/data/data/checkpoint_secret.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"hmac_secret":"HJ9EY5oN0pO71AUN/2faoYnJhZyTr875iWbw6Nl8rc8=","created_at":"2025-04-28T18:28:59.6204724-05:00","updated_at":"2025-04-28T18:33:55.3259956-05:00"}
|
||||
BIN
checkpoint_service/data/data/checkpoint_tokendb/000001.sst
Normal file
BIN
checkpoint_service/data/data/checkpoint_tokendb/000001.sst
Normal file
Binary file not shown.
BIN
checkpoint_service/data/data/checkpoint_tokendb/000002.sst
Normal file
BIN
checkpoint_service/data/data/checkpoint_tokendb/000002.sst
Normal file
Binary file not shown.
BIN
checkpoint_service/data/data/checkpoint_tokendb/000002.vlog
Normal file
BIN
checkpoint_service/data/data/checkpoint_tokendb/000002.vlog
Normal file
Binary file not shown.
BIN
checkpoint_service/data/data/checkpoint_tokendb/000003.sst
Normal file
BIN
checkpoint_service/data/data/checkpoint_tokendb/000003.sst
Normal file
Binary file not shown.
BIN
checkpoint_service/data/data/checkpoint_tokendb/000003.vlog
Normal file
BIN
checkpoint_service/data/data/checkpoint_tokendb/000003.vlog
Normal file
Binary file not shown.
BIN
checkpoint_service/data/data/checkpoint_tokendb/DISCARD
Normal file
BIN
checkpoint_service/data/data/checkpoint_tokendb/DISCARD
Normal file
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
~t(ðæ4¥
ø2³Dî•æHello Badger
|
||||
BIN
checkpoint_service/data/data/checkpoint_tokendb/MANIFEST
Normal file
BIN
checkpoint_service/data/data/checkpoint_tokendb/MANIFEST
Normal file
Binary file not shown.
39
checkpoint_service/go.mod
Normal file
39
checkpoint_service/go.mod
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
module checkpoint_service
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/cloudflare/ahocorasick v0.0.0-20240916140611-054963ec9396
|
||||
github.com/dgraph-io/badger/v4 v4.7.0
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/mileusna/useragent v1.3.5
|
||||
github.com/oschwald/geoip2-golang v1.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/flatbuffers v25.2.10+incompatible // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
76
checkpoint_service/go.sum
Normal file
76
checkpoint_service/go.sum
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/ahocorasick v0.0.0-20240916140611-054963ec9396 h1:W2HK1IdCnCGuLUeyizSCkwvBjdj0ZL7mxnJYQ3poyzI=
|
||||
github.com/cloudflare/ahocorasick v0.0.0-20240916140611-054963ec9396/go.mod h1:tGWUZLZp9ajsxUOnHmFFLnqnlKXsCn6GReG4jAD59H0=
|
||||
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/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y=
|
||||
github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA=
|
||||
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
|
||||
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
|
||||
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
|
||||
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
||||
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
|
||||
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
66
checkpoint_service/main.go
Normal file
66
checkpoint_service/main.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"checkpoint_service/middleware"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse command-line flags
|
||||
port := flag.String("port", "8080", "Port to listen on")
|
||||
skipCheckpoint := flag.Bool("skip-checkpoint", false, "Skip the checkpoint middleware")
|
||||
flag.Parse()
|
||||
|
||||
// Create Fiber app
|
||||
app := fiber.New()
|
||||
|
||||
// Request logging
|
||||
app.Use(logger.New())
|
||||
|
||||
// Response compression
|
||||
app.Use(compress.New())
|
||||
|
||||
// Load and apply middleware plugins
|
||||
for _, handler := range middleware.LoadPlugins(*skipCheckpoint) {
|
||||
app.Use(handler)
|
||||
}
|
||||
log.Println("Loaded middleware plugins")
|
||||
|
||||
// API group for proof-of-work endpoints
|
||||
api := app.Group("/api")
|
||||
api.Get("/pow/challenge", middleware.GetCheckpointChallengeHandler)
|
||||
api.Post("/pow/verify", middleware.VerifyCheckpointHandler)
|
||||
api.Get("/verify", middleware.VerifyCheckpointHandler)
|
||||
|
||||
// Start the server in a goroutine
|
||||
go func() {
|
||||
addr := ":" + *port
|
||||
log.Printf("Checkpoint service starting on %s", addr)
|
||||
if err := app.Listen(addr); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Graceful shutdown on SIGINT/SIGTERM
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := app.ShutdownWithContext(ctx); err != nil {
|
||||
log.Fatalf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
log.Println("Server exiting")
|
||||
}
|
||||
1482
checkpoint_service/middleware/checkpoint.go
Normal file
1482
checkpoint_service/middleware/checkpoint.go
Normal file
File diff suppressed because it is too large
Load diff
77
checkpoint_service/middleware/config/checkpoint.toml
Normal file
77
checkpoint_service/middleware/config/checkpoint.toml
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# Checkpoint Middleware Configuration (checkpoint.toml)
|
||||
#
|
||||
# All durations are parsed via time.ParseDuration (e.g. "24h").
|
||||
# Arrays and tables map directly to the Config struct fields.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# === GENERAL SETTINGS ===
|
||||
# Number of leading zeros required in PoW hash
|
||||
Difficulty = 4
|
||||
# Validity period for issued tokens
|
||||
TokenExpiration = "24h"
|
||||
# Name of the cookie used to store the checkpoint token
|
||||
CookieName = "checkpoint_token"
|
||||
# Domain attribute for the cookie; empty = host-only (localhost)
|
||||
CookieDomain = ""
|
||||
# Length of the random salt in bytes for challenges
|
||||
SaltLength = 16
|
||||
|
||||
# === RATE LIMITING & EXPIRATION ===
|
||||
# Max PoW verification attempts per IP per hour
|
||||
MaxAttemptsPerHour = 10
|
||||
# Max age for used nonces before cleanup
|
||||
MaxNonceAge = "24h"
|
||||
# Time allowed for solving a challenge
|
||||
ChallengeExpiration = "5m"
|
||||
|
||||
# === PERSISTENCE PATHS ===
|
||||
# File where HMAC secret is stored
|
||||
SecretConfigPath = "./data/checkpoint_secret.json"
|
||||
# Directory for BadgerDB token store
|
||||
TokenStoreDBPath = "./data/checkpoint_tokendb"
|
||||
# Ordered fallback paths for interstitial HTML
|
||||
InterstitialPaths = [
|
||||
"./public/static/pow-interstitial.html",
|
||||
"./develop/static/pow-interstitial.html"
|
||||
]
|
||||
|
||||
# === SECURITY SETTINGS ===
|
||||
# Enable Proof-of-Space-Time consistency checks
|
||||
CheckPoSTimes = true
|
||||
# Allowed ratio between slowest and fastest PoS runs
|
||||
PoSTimeConsistencyRatio = 1.35
|
||||
|
||||
# === HTML CHECKPOINT EXCLUSIONS ===
|
||||
# Path prefixes to skip PoW interstitial
|
||||
HTMLCheckpointExclusions = ["/api"]
|
||||
# File extensions to skip PoW check
|
||||
HTMLCheckpointExcludedExtensions = { ".jpg" = true, ".jpeg" = true, ".png" = true, ".gif" = true, ".svg" = true, ".webp" = true, ".ico" = true, ".bmp" = true, ".tif" = true, ".tiff" = true, ".mp4" = true, ".webm" = true, ".css" = true, ".js" = true, ".mjs" = true, ".woff" = true, ".woff2" = true, ".ttf" = true, ".otf" = true, ".eot" = true, ".json" = true, ".xml" = true, ".txt" = true, ".pdf" = true, ".map" = true, ".wasm" = true }
|
||||
|
||||
# === QUERY SANITIZATION ===
|
||||
# Regex patterns (case-insensitive) to block in query strings
|
||||
DangerousQueryPatterns = [
|
||||
"(?i)union\\s+select",
|
||||
"(?i)drop\\s+table",
|
||||
"(?i)insert\\s+into",
|
||||
"(?i)<script",
|
||||
"(?i)javascript:",
|
||||
"(?i)onerror=",
|
||||
]
|
||||
# Block queries containing ';', '`', or '\\'
|
||||
BlockDangerousPathChars = true
|
||||
|
||||
# === USER-AGENT VALIDATION ===
|
||||
# Path prefixes to skip UA validation
|
||||
UserAgentValidationExclusions = ["/api"]
|
||||
# Required UA prefix per path prefix
|
||||
[UserAgentRequiredPrefixes]
|
||||
"/demo1" = "Dart/"
|
||||
|
||||
# === REVERSE PROXY MAPPINGS ===
|
||||
# Hostname-to-backend URL map
|
||||
[ReverseProxyMappings]
|
||||
"jellyfin.caileb.com" = "http://192.168.0.2:8096"
|
||||
"archive.caileb.com" = "http://192.168.0.2:7461"
|
||||
"music.caileb.com" = "http://192.168.0.2:4533"
|
||||
"gallery.caileb.com" = "http://192.168.0.2:2283"
|
||||
39
checkpoint_service/middleware/config/ipfilter.toml
Normal file
39
checkpoint_service/middleware/config/ipfilter.toml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# IPFilter Configuration
|
||||
|
||||
# Page shown when a request is blocked
|
||||
defaultBlockPage = "default-block.html"
|
||||
# Cache block decisions (seconds)
|
||||
ipBlockCacheTTLSec = 300
|
||||
|
||||
# Country codes to block
|
||||
blockedCountryCodes = [
|
||||
"IN", "BH", "AE", "OM", "QA", "KW", "SA", "YE", "IR", "IQ",
|
||||
"LB", "PS", "CY", "TR", "AZ", "AM", "TM", "UZ", "KZ", "KG",
|
||||
"TJ", "KE", "ET", "SO", "SD", "SS", "KP", "UA", "IL"
|
||||
]
|
||||
|
||||
# === CONTINENT-BASED BLOCKING ===
|
||||
blockedContinentCodes = ["AF", "SA", "AS", "AN"]
|
||||
|
||||
# === ASN NUMBER GROUPS ===
|
||||
[blockedASNs]
|
||||
# empty by default
|
||||
|
||||
# === ASN NAME GROUPS ===
|
||||
[blockedASNNames]
|
||||
"Data Center" = [
|
||||
"Cloudflare", "GOOGLE-CLOUD-PLATFORM", "Microsoft", "Amazon", "AWS",
|
||||
"Digitalocean", "OVH", "HUAWEI CLOUDS", "HWCLOUDS", "M247",
|
||||
"Datacamp", "Datapacket", "Amanah", "Hern Labs"
|
||||
]
|
||||
|
||||
# === CUSTOM BLOCK PAGES ===
|
||||
[countryBlockPages]
|
||||
IN = "india-block.html"
|
||||
|
||||
[continentBlockPages]
|
||||
# none by default
|
||||
|
||||
# Custom pages by ASN group
|
||||
[asnGroupBlockPages]
|
||||
"Data Center" = "datacenter-block.html"
|
||||
498
checkpoint_service/middleware/ipfilter.go
Normal file
498
checkpoint_service/middleware/ipfilter.go
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
// IPFilter middleware blocks requests from unwanted IPs based on GeoIP, ASN, and other rules.
|
||||
// Package middleware provides IP and User-Agent filtering to protect against abuse.
|
||||
// Features:
|
||||
// - GeoIP country/continent blocking
|
||||
// - ASN blocking
|
||||
// - Data center detection
|
||||
// - Bot detection
|
||||
// - Cache-optimized lookups
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/ahocorasick"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
// --- IP Block Cache --- //
|
||||
// blockCacheEntry stores the result of a block check for an IP
|
||||
type blockCacheEntry struct {
|
||||
blocked bool
|
||||
blockType string
|
||||
blockValue string
|
||||
customPage string
|
||||
asnOrgName string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// Cache for IP block lookups (TTL set via config)
|
||||
var (
|
||||
ipBlockCache map[string]blockCacheEntry
|
||||
ipBlockCacheMutex sync.RWMutex
|
||||
ipBlockCacheTTL time.Duration
|
||||
)
|
||||
|
||||
// --- GeoIP Configuration ---
|
||||
var (
|
||||
geoIPCountryDBPath = "./data/GeoLite2-Country.mmdb"
|
||||
geoIPASNDBPath = "./data/GeoLite2-ASN.mmdb"
|
||||
geoipCountryReader *geoip2.Reader
|
||||
geoipASNReader *geoip2.Reader
|
||||
|
||||
// Lists and pages (populated from ipfilter.toml)
|
||||
blockedCountryCodes map[string]bool
|
||||
blockedContinentCodes map[string]bool
|
||||
blockedASNs map[string][]uint
|
||||
blockedASNNames map[string][]string
|
||||
countryBlockPages map[string]string
|
||||
continentBlockPages map[string]string
|
||||
asnGroupBlockPages map[string]string
|
||||
defaultBlockPage string
|
||||
|
||||
// Aho-Corasick matchers for ASN name groups
|
||||
asnNameMatchers map[string]*ahocorasick.Matcher
|
||||
asnNameMatchersMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// init loads ipfilter.toml, sets up all block lists, and hooks this into our plugin registry
|
||||
func init() {
|
||||
// Load full configuration from ipfilter.toml
|
||||
type ipfilterConfig struct {
|
||||
BlockedCountryCodes []string `toml:"blockedCountryCodes"`
|
||||
BlockedContinentCodes []string `toml:"blockedContinentCodes"`
|
||||
BlockedASNs map[string][]int `toml:"blockedASNs"`
|
||||
BlockedASNNames map[string][]string `toml:"blockedASNNames"`
|
||||
CountryBlockPages map[string]string `toml:"countryBlockPages"`
|
||||
ContinentBlockPages map[string]string `toml:"continentBlockPages"`
|
||||
ASNGroupBlockPages map[string]string `toml:"asnGroupBlockPages"`
|
||||
DefaultBlockPage string `toml:"defaultBlockPage"`
|
||||
IPBlockCacheTTLSec int `toml:"ipBlockCacheTTLSec"`
|
||||
}
|
||||
var cfg ipfilterConfig
|
||||
if err := LoadConfig("ipfilter", &cfg); err != nil {
|
||||
log.Fatalf("Failed to load ipfilter config: %v", err)
|
||||
}
|
||||
// override blockedCountryCodes
|
||||
blockedCountryCodes = make(map[string]bool)
|
||||
for _, c := range cfg.BlockedCountryCodes {
|
||||
blockedCountryCodes[c] = true
|
||||
}
|
||||
// override blockedContinentCodes
|
||||
blockedContinentCodes = make(map[string]bool)
|
||||
for _, c := range cfg.BlockedContinentCodes {
|
||||
blockedContinentCodes[c] = true
|
||||
}
|
||||
// override blockedASNs
|
||||
newASNs := make(map[string][]uint)
|
||||
for group, arr := range cfg.BlockedASNs {
|
||||
uintArr := make([]uint, len(arr))
|
||||
for i, v := range arr {
|
||||
uintArr[i] = uint(v)
|
||||
}
|
||||
newASNs[group] = uintArr
|
||||
}
|
||||
blockedASNs = newASNs
|
||||
// override blockedASNNames
|
||||
blockedASNNames = cfg.BlockedASNNames
|
||||
// override page maps
|
||||
countryBlockPages = cfg.CountryBlockPages
|
||||
continentBlockPages = cfg.ContinentBlockPages
|
||||
asnGroupBlockPages = cfg.ASNGroupBlockPages
|
||||
// override default page
|
||||
defaultBlockPage = cfg.DefaultBlockPage
|
||||
// override cache TTL
|
||||
ipBlockCacheTTL = time.Duration(cfg.IPBlockCacheTTLSec) * time.Second
|
||||
|
||||
// Initialize the cache
|
||||
ipBlockCache = make(map[string]blockCacheEntry)
|
||||
|
||||
// Initialize GeoIP databases
|
||||
initGeoIP()
|
||||
|
||||
// Build Aho-Corasick matchers for ASN name matching
|
||||
buildASNNameMatchers()
|
||||
|
||||
// Register IP-block plugin
|
||||
RegisterPlugin("ipfilter", IPBlockMiddleware)
|
||||
}
|
||||
|
||||
// IPBlockMiddleware returns a handler that stops requests coming from blocked IPs
|
||||
// Use this early so bad traffic gets rejected fast
|
||||
func IPBlockMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Get the client IP
|
||||
clientIP := getRealIP(c)
|
||||
|
||||
// Check IP blocklist (GeoIP/ASN)
|
||||
if blocked, blockType, blockValue, customPage, asnOrgName := isBlockedIPExtended(clientIP); blocked {
|
||||
// For API requests, return JSON response
|
||||
if strings.HasPrefix(c.Path(), "/api") {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "Access denied from your location or network.",
|
||||
"reason": "geoip",
|
||||
"type": blockType,
|
||||
"value": blockValue,
|
||||
"asn_org": asnOrgName,
|
||||
})
|
||||
}
|
||||
|
||||
// For HTML requests, serve a block page
|
||||
// Log block information
|
||||
log.Printf("Blocking access: type=%s, value=%s, custom_page=%s, asn_org=%s",
|
||||
blockType, blockValue, customPage, asnOrgName)
|
||||
|
||||
// If no custom page specified, use the default
|
||||
if customPage == "" {
|
||||
customPage = defaultBlockPage
|
||||
}
|
||||
|
||||
// Try to serve the block page
|
||||
possiblePaths := []string{
|
||||
"./public/static/" + customPage,
|
||||
"./public/html/" + customPage,
|
||||
"./public/" + customPage,
|
||||
"./develop/static/" + customPage,
|
||||
}
|
||||
|
||||
var htmlContentBytes []byte
|
||||
var err error
|
||||
foundPath := ""
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
if htmlContentBytes, err = os.ReadFile(path); err == nil {
|
||||
// Found the file, serve it
|
||||
foundPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundPath != "" {
|
||||
// Found the file, serve it
|
||||
c.Status(fiber.StatusForbidden)
|
||||
c.Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
// Replace placeholder with ASN Org Name if available
|
||||
htmlContent := string(htmlContentBytes)
|
||||
replaceValue := "Blocked Network" // Default fallback text
|
||||
if asnOrgName != "" {
|
||||
replaceValue = asnOrgName
|
||||
}
|
||||
htmlContent = strings.Replace(htmlContent, "{{.ASNName}}", replaceValue, -1)
|
||||
|
||||
return c.SendString(htmlContent)
|
||||
}
|
||||
|
||||
// If no custom or default page was found, fall back to default message
|
||||
return c.Status(fiber.StatusForbidden).SendString("Access denied from your location or network.")
|
||||
}
|
||||
|
||||
// Not blocked, continue to next middleware
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// initGeoIP loads the GeoLite2 databases
|
||||
func initGeoIP() {
|
||||
var err error
|
||||
geoipCountryReader, err = geoip2.Open(geoIPCountryDBPath)
|
||||
if err != nil {
|
||||
log.Printf("WARNING: Could not open GeoLite2 Country database at %s: %v. GeoIP country/continent blocking disabled.", geoIPCountryDBPath, err)
|
||||
}
|
||||
geoipASNReader, err = geoip2.Open(geoIPASNDBPath)
|
||||
if err != nil {
|
||||
log.Printf("WARNING: Could not open GeoLite2 ASN database at %s: %v. GeoIP ASN blocking disabled.", geoIPASNDBPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// isBlockedIP checks if the IP address is blocked based on GeoIP or ASN.
|
||||
func isBlockedIP(ipStr string) bool {
|
||||
blocked, _, _, _, _ := isBlockedIPExtended(ipStr)
|
||||
return blocked
|
||||
}
|
||||
|
||||
// isBlockedIPExtended checks if the IP address is blocked and returns detailed information.
|
||||
// Returns:
|
||||
// - blocked: true if IP is blocked
|
||||
// - blockType: "country", "continent", "asn_number_group", or "asn_name_group"
|
||||
// - blockValue: the country code, continent code, or ASN group name (that triggered the block)
|
||||
// - customPage: path to custom block page if defined, or empty string
|
||||
// - asnOrgName: the specific ASN Organization name if blockType is "asn_name_group"
|
||||
func isBlockedIPExtended(ipStr string) (blocked bool, blockType string, blockValue string, customPage string, asnOrgName string) {
|
||||
// Safeguard against empty IPs causing issues
|
||||
if ipStr == "" {
|
||||
log.Printf("ERROR: Empty IP passed to isBlockedIPExtended")
|
||||
return false, "", "", "", ""
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
log.Printf("WARNING: Could not parse IP address for GeoIP check: %s", ipStr)
|
||||
return false, "", "", "", "" // Cannot check invalid IP
|
||||
}
|
||||
|
||||
// --- Check Cache First --- //
|
||||
ipBlockCacheMutex.RLock()
|
||||
entry, found := ipBlockCache[ipStr]
|
||||
ipBlockCacheMutex.RUnlock() // Unlock immediately after reading
|
||||
|
||||
if found && time.Now().Before(entry.expiresAt) {
|
||||
// Return cached result
|
||||
return entry.blocked, entry.blockType, entry.blockValue, entry.customPage, entry.asnOrgName
|
||||
}
|
||||
// --- End Cache Check --- //
|
||||
|
||||
var countryRecord *geoip2.Country
|
||||
var asnRecord *geoip2.ASN
|
||||
var countryErr, asnErr error
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Concurrently perform GeoIP lookups if readers are available
|
||||
if geoipCountryReader != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
countryRecord, countryErr = geoipCountryReader.Country(ip)
|
||||
}()
|
||||
}
|
||||
|
||||
if geoipASNReader != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
asnRecord, asnErr = geoipASNReader.ASN(ip)
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for lookups to complete
|
||||
wg.Wait()
|
||||
|
||||
// --- Process Results --- //
|
||||
|
||||
// 1. Check Country/Continent
|
||||
if countryRecord != nil && countryErr == nil {
|
||||
// Check country first
|
||||
if blockedCountryCodes[countryRecord.Country.IsoCode] {
|
||||
blockType = "country"
|
||||
blockValue = countryRecord.Country.IsoCode
|
||||
log.Printf("INFO: Blocking IP %s based on %s: %s", ipStr, blockType, blockValue)
|
||||
customPage = countryBlockPages[blockValue]
|
||||
return cacheAndReturnBlockResult(ipStr, blockType, blockValue, customPage, "")
|
||||
}
|
||||
|
||||
// Then check continent
|
||||
if blockedContinentCodes[countryRecord.Continent.Code] {
|
||||
blockType = "continent"
|
||||
blockValue = countryRecord.Continent.Code
|
||||
log.Printf("INFO: Blocking IP %s based on %s: %s", ipStr, blockType, blockValue)
|
||||
customPage = continentBlockPages[blockValue]
|
||||
return cacheAndReturnBlockResult(ipStr, blockType, blockValue, customPage, "")
|
||||
}
|
||||
} else if countryErr != nil && !strings.Contains(countryErr.Error(), "cannot be found in the database") {
|
||||
// Log errors other than "not found"
|
||||
log.Printf("WARNING: GeoIP country lookup error for IP %s: %v", ipStr, countryErr)
|
||||
}
|
||||
|
||||
// 2. Check ASN (Number and Name)
|
||||
if asnRecord != nil && asnErr == nil {
|
||||
clientASN := asnRecord.AutonomousSystemNumber
|
||||
asnOrg := asnRecord.AutonomousSystemOrganization // Keep org name for potential use
|
||||
|
||||
// Check ASN Number Groups
|
||||
for groupName, asnList := range blockedASNs {
|
||||
for _, blockedASN := range asnList {
|
||||
if clientASN == blockedASN {
|
||||
blockType = "asn_number_group"
|
||||
blockValue = groupName
|
||||
log.Printf("INFO: Blocking IP %s based on %s: %s (ASN: %d - %s)", ipStr, blockType, blockValue, clientASN, asnOrg)
|
||||
customPage = asnGroupBlockPages[groupName]
|
||||
return cacheAndReturnBlockResult(ipStr, blockType, blockValue, customPage, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check ASN Name Groups (Case-Insensitive Substring Match)
|
||||
if asnOrg != "" {
|
||||
lowerASNOrg := strings.ToLower(asnOrg)
|
||||
|
||||
// Acquire read lock for accessing the global matchers map
|
||||
asnNameMatchersMutex.RLock()
|
||||
defer asnNameMatchersMutex.RUnlock()
|
||||
|
||||
for groupName, matcher := range asnNameMatchers {
|
||||
if matcher == nil { // Check added during previous fix, keep it
|
||||
log.Printf("WARNING: Nil matcher found for group: %s during check", groupName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Use Aho-Corasick matcher - protect against any potential panic
|
||||
var matches []int
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("RECOVERED from panic in Aho-Corasick: %v", r)
|
||||
matches = nil // Ensure it's empty if we panic
|
||||
}
|
||||
}()
|
||||
// Match against the globally shared matcher (assumed read-safe)
|
||||
matches = matcher.Match([]byte(lowerASNOrg))
|
||||
}()
|
||||
|
||||
if len(matches) > 0 {
|
||||
blockType = "asn_name_group"
|
||||
blockValue = groupName
|
||||
log.Printf("INFO: Blocking IP %s based on %s: %s (ASN: %d, Org: '%s')", ipStr, blockType, blockValue, clientASN, asnOrg)
|
||||
customPage = asnGroupBlockPages[groupName]
|
||||
// No need to unlock here, defer handles it
|
||||
return cacheAndReturnBlockResult(ipStr, blockType, blockValue, customPage, asnOrg)
|
||||
}
|
||||
}
|
||||
// RUnlock happens via defer
|
||||
}
|
||||
} else if asnErr != nil && !strings.Contains(asnErr.Error(), "cannot be found in the database") {
|
||||
// Log errors other than "not found"
|
||||
log.Printf("WARNING: GeoIP ASN lookup error for IP %s: %v", ipStr, asnErr)
|
||||
}
|
||||
|
||||
// --- Cache the result before returning --- //
|
||||
computedEntry := blockCacheEntry{
|
||||
blocked: false,
|
||||
expiresAt: time.Now().Add(ipBlockCacheTTL),
|
||||
}
|
||||
ipBlockCacheMutex.Lock()
|
||||
ipBlockCache[ipStr] = computedEntry
|
||||
ipBlockCacheMutex.Unlock()
|
||||
return false, "", "", "", "" // Not blocked
|
||||
}
|
||||
|
||||
// Helper function to cache block results
|
||||
func cacheAndReturnBlockResult(ipStr string, blockType string, blockValue string, customPage string, asnOrgName string) (bool, string, string, string, string) {
|
||||
// Create the cache entry
|
||||
computedEntry := blockCacheEntry{
|
||||
blocked: true,
|
||||
blockType: blockType,
|
||||
blockValue: blockValue,
|
||||
customPage: customPage,
|
||||
asnOrgName: asnOrgName,
|
||||
expiresAt: time.Now().Add(ipBlockCacheTTL),
|
||||
}
|
||||
|
||||
// Use a separate defer+recover to ensure we don't crash the entire server
|
||||
// if there's any issue with the cache
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("RECOVERED from panic while caching result: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ipBlockCacheMutex.Lock()
|
||||
defer ipBlockCacheMutex.Unlock() // Use defer to ensure unlock happens
|
||||
ipBlockCache[ipStr] = computedEntry
|
||||
}()
|
||||
|
||||
return true, blockType, blockValue, customPage, asnOrgName
|
||||
}
|
||||
|
||||
// buildASNNameMatchers creates Aho-Corasick matchers for faster ASN name checking
|
||||
func buildASNNameMatchers() {
|
||||
// Acquire write lock before modifying the global map
|
||||
asnNameMatchersMutex.Lock()
|
||||
defer asnNameMatchersMutex.Unlock()
|
||||
|
||||
// Clear any existing matchers first
|
||||
asnNameMatchers = make(map[string]*ahocorasick.Matcher)
|
||||
|
||||
for groupName, nameList := range blockedASNNames {
|
||||
// Skip if the name list is empty
|
||||
if len(nameList) == 0 {
|
||||
log.Printf("Skipping matcher build for empty group: %s", groupName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert names to lowercase byte slices for case-insensitive matching
|
||||
dict := make([][]byte, 0, len(nameList))
|
||||
for _, name := range nameList {
|
||||
if name != "" {
|
||||
dict = append(dict, []byte(strings.ToLower(name)))
|
||||
}
|
||||
}
|
||||
|
||||
// Only create a matcher if we have patterns
|
||||
if len(dict) > 0 {
|
||||
// Use a recovery mechanism in case the matcher creation fails
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC while building Aho-Corasick matcher for group %s: %v", groupName, r)
|
||||
// Ensure the entry for this group is nil if creation failed
|
||||
asnNameMatchers[groupName] = nil
|
||||
}
|
||||
}()
|
||||
|
||||
// This assignment happens under the write lock
|
||||
asnNameMatchers[groupName] = ahocorasick.NewMatcher(dict)
|
||||
log.Printf("Built Aho-Corasick matcher for ASN name group: %s (%d patterns)", groupName, len(dict))
|
||||
}()
|
||||
} else {
|
||||
log.Printf("No valid patterns found for ASN name group: %s", groupName)
|
||||
}
|
||||
}
|
||||
// Unlock happens via defer
|
||||
}
|
||||
|
||||
// ReloadGeoIPDatabases closes and reopens the GeoIP database readers
|
||||
// to load updated database files. Safe to call while the server is running.
|
||||
func ReloadGeoIPDatabases() {
|
||||
// Close existing readers if they're open
|
||||
if geoipCountryReader != nil {
|
||||
geoipCountryReader.Close()
|
||||
geoipCountryReader = nil
|
||||
}
|
||||
if geoipASNReader != nil {
|
||||
geoipASNReader.Close()
|
||||
geoipASNReader = nil
|
||||
}
|
||||
|
||||
// Re-initialize the readers
|
||||
initGeoIP()
|
||||
log.Printf("GeoIP databases reloaded")
|
||||
}
|
||||
|
||||
// getRealIP gets the real client IP when behind a reverse proxy
|
||||
// It checks X-Forwarded-For header first, then falls back to c.IP()
|
||||
func getRealIP(c *fiber.Ctx) string {
|
||||
// Check X-Forwarded-For header first
|
||||
if xff := c.Get("X-Forwarded-For"); xff != "" {
|
||||
// X-Forwarded-For can contain multiple IPs (client, proxy1, proxy2, ...)
|
||||
// The first one is the original client IP
|
||||
ips := strings.Split(xff, ",")
|
||||
if len(ips) > 0 {
|
||||
// Get the first IP and trim whitespace
|
||||
clientIP := strings.TrimSpace(ips[0])
|
||||
// Validate it's a real IP
|
||||
if net.ParseIP(clientIP) != nil {
|
||||
log.Printf("Using X-Forwarded-For IP: %s (original: %s)", clientIP, c.IP())
|
||||
return clientIP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for custom Remote-Addr header that might be set by some proxies
|
||||
if remoteAddr := c.Get("$remote_addr"); remoteAddr != "" {
|
||||
// Validate it's a real IP
|
||||
if net.ParseIP(remoteAddr) != nil {
|
||||
log.Printf("Using $remote_addr IP: %s (original: %s)", remoteAddr, c.IP())
|
||||
return remoteAddr
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default IP
|
||||
return c.IP()
|
||||
}
|
||||
1482
checkpoint_service/middleware/middleware/checkpoint.go
Normal file
1482
checkpoint_service/middleware/middleware/checkpoint.go
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,77 @@
|
|||
# -----------------------------------------------------------------------------
|
||||
# Checkpoint Middleware Configuration (checkpoint.toml)
|
||||
#
|
||||
# All durations are parsed via time.ParseDuration (e.g. "24h").
|
||||
# Arrays and tables map directly to the Config struct fields.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# === GENERAL SETTINGS ===
|
||||
# Number of leading zeros required in PoW hash
|
||||
Difficulty = 4
|
||||
# Validity period for issued tokens
|
||||
TokenExpiration = "24h"
|
||||
# Name of the cookie used to store the checkpoint token
|
||||
CookieName = "checkpoint_token"
|
||||
# Domain attribute for the cookie; empty = host-only (localhost)
|
||||
CookieDomain = ""
|
||||
# Length of the random salt in bytes for challenges
|
||||
SaltLength = 16
|
||||
|
||||
# === RATE LIMITING & EXPIRATION ===
|
||||
# Max PoW verification attempts per IP per hour
|
||||
MaxAttemptsPerHour = 10
|
||||
# Max age for used nonces before cleanup
|
||||
MaxNonceAge = "24h"
|
||||
# Time allowed for solving a challenge
|
||||
ChallengeExpiration = "5m"
|
||||
|
||||
# === PERSISTENCE PATHS ===
|
||||
# File where HMAC secret is stored
|
||||
SecretConfigPath = "./data/checkpoint_secret.json"
|
||||
# Directory for BadgerDB token store
|
||||
TokenStoreDBPath = "./data/checkpoint_tokendb"
|
||||
# Ordered fallback paths for interstitial HTML
|
||||
InterstitialPaths = [
|
||||
"./public/static/pow-interstitial.html",
|
||||
"./develop/static/pow-interstitial.html"
|
||||
]
|
||||
|
||||
# === SECURITY SETTINGS ===
|
||||
# Enable Proof-of-Space-Time consistency checks
|
||||
CheckPoSTimes = true
|
||||
# Allowed ratio between slowest and fastest PoS runs
|
||||
PoSTimeConsistencyRatio = 1.35
|
||||
|
||||
# === HTML CHECKPOINT EXCLUSIONS ===
|
||||
# Path prefixes to skip PoW interstitial
|
||||
HTMLCheckpointExclusions = ["/api"]
|
||||
# File extensions to skip PoW check
|
||||
HTMLCheckpointExcludedExtensions = { ".jpg" = true, ".jpeg" = true, ".png" = true, ".gif" = true, ".svg" = true, ".webp" = true, ".ico" = true, ".bmp" = true, ".tif" = true, ".tiff" = true, ".mp4" = true, ".webm" = true, ".css" = true, ".js" = true, ".mjs" = true, ".woff" = true, ".woff2" = true, ".ttf" = true, ".otf" = true, ".eot" = true, ".json" = true, ".xml" = true, ".txt" = true, ".pdf" = true, ".map" = true, ".wasm" = true }
|
||||
|
||||
# === QUERY SANITIZATION ===
|
||||
# Regex patterns (case-insensitive) to block in query strings
|
||||
DangerousQueryPatterns = [
|
||||
"(?i)union\\s+select",
|
||||
"(?i)drop\\s+table",
|
||||
"(?i)insert\\s+into",
|
||||
"(?i)<script",
|
||||
"(?i)javascript:",
|
||||
"(?i)onerror=",
|
||||
]
|
||||
# Block queries containing ';', '`', or '\\'
|
||||
BlockDangerousPathChars = true
|
||||
|
||||
# === USER-AGENT VALIDATION ===
|
||||
# Path prefixes to skip UA validation
|
||||
UserAgentValidationExclusions = ["/api"]
|
||||
# Required UA prefix per path prefix
|
||||
[UserAgentRequiredPrefixes]
|
||||
"/demo1" = "Dart/"
|
||||
|
||||
# === REVERSE PROXY MAPPINGS ===
|
||||
# Hostname-to-backend URL map
|
||||
[ReverseProxyMappings]
|
||||
"jellyfin.caileb.com" = "http://192.168.0.2:8096"
|
||||
"archive.caileb.com" = "http://192.168.0.2:7461"
|
||||
"music.caileb.com" = "http://192.168.0.2:4533"
|
||||
"gallery.caileb.com" = "http://192.168.0.2:2283"
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# IPFilter Configuration
|
||||
|
||||
# Page shown when a request is blocked
|
||||
defaultBlockPage = "default-block.html"
|
||||
# Cache block decisions (seconds)
|
||||
ipBlockCacheTTLSec = 300
|
||||
|
||||
# Country codes to block
|
||||
blockedCountryCodes = [
|
||||
"IN", "BH", "AE", "OM", "QA", "KW", "SA", "YE", "IR", "IQ",
|
||||
"LB", "PS", "CY", "TR", "AZ", "AM", "TM", "UZ", "KZ", "KG",
|
||||
"TJ", "KE", "ET", "SO", "SD", "SS", "KP", "UA", "IL"
|
||||
]
|
||||
|
||||
# === CONTINENT-BASED BLOCKING ===
|
||||
blockedContinentCodes = ["AF", "SA", "AS", "AN"]
|
||||
|
||||
# === ASN NUMBER GROUPS ===
|
||||
[blockedASNs]
|
||||
# empty by default
|
||||
|
||||
# === ASN NAME GROUPS ===
|
||||
[blockedASNNames]
|
||||
"Data Center" = [
|
||||
"Cloudflare", "GOOGLE-CLOUD-PLATFORM", "Microsoft", "Amazon", "AWS",
|
||||
"Digitalocean", "OVH", "HUAWEI CLOUDS", "HWCLOUDS", "M247",
|
||||
"Datacamp", "Datapacket", "Amanah", "Hern Labs"
|
||||
]
|
||||
|
||||
# === CUSTOM BLOCK PAGES ===
|
||||
[countryBlockPages]
|
||||
IN = "india-block.html"
|
||||
|
||||
[continentBlockPages]
|
||||
# none by default
|
||||
|
||||
# Custom pages by ASN group
|
||||
[asnGroupBlockPages]
|
||||
"Data Center" = "datacenter-block.html"
|
||||
498
checkpoint_service/middleware/middleware/ipfilter.go
Normal file
498
checkpoint_service/middleware/middleware/ipfilter.go
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
// IPFilter middleware blocks requests from unwanted IPs based on GeoIP, ASN, and other rules.
|
||||
// Package middleware provides IP and User-Agent filtering to protect against abuse.
|
||||
// Features:
|
||||
// - GeoIP country/continent blocking
|
||||
// - ASN blocking
|
||||
// - Data center detection
|
||||
// - Bot detection
|
||||
// - Cache-optimized lookups
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/ahocorasick"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
// --- IP Block Cache --- //
|
||||
// blockCacheEntry stores the result of a block check for an IP
|
||||
type blockCacheEntry struct {
|
||||
blocked bool
|
||||
blockType string
|
||||
blockValue string
|
||||
customPage string
|
||||
asnOrgName string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// Cache for IP block lookups (TTL set via config)
|
||||
var (
|
||||
ipBlockCache map[string]blockCacheEntry
|
||||
ipBlockCacheMutex sync.RWMutex
|
||||
ipBlockCacheTTL time.Duration
|
||||
)
|
||||
|
||||
// --- GeoIP Configuration ---
|
||||
var (
|
||||
geoIPCountryDBPath = "./data/GeoLite2-Country.mmdb"
|
||||
geoIPASNDBPath = "./data/GeoLite2-ASN.mmdb"
|
||||
geoipCountryReader *geoip2.Reader
|
||||
geoipASNReader *geoip2.Reader
|
||||
|
||||
// Lists and pages (populated from ipfilter.toml)
|
||||
blockedCountryCodes map[string]bool
|
||||
blockedContinentCodes map[string]bool
|
||||
blockedASNs map[string][]uint
|
||||
blockedASNNames map[string][]string
|
||||
countryBlockPages map[string]string
|
||||
continentBlockPages map[string]string
|
||||
asnGroupBlockPages map[string]string
|
||||
defaultBlockPage string
|
||||
|
||||
// Aho-Corasick matchers for ASN name groups
|
||||
asnNameMatchers map[string]*ahocorasick.Matcher
|
||||
asnNameMatchersMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// init loads ipfilter.toml, sets up all block lists, and hooks this into our plugin registry
|
||||
func init() {
|
||||
// Load full configuration from ipfilter.toml
|
||||
type ipfilterConfig struct {
|
||||
BlockedCountryCodes []string `toml:"blockedCountryCodes"`
|
||||
BlockedContinentCodes []string `toml:"blockedContinentCodes"`
|
||||
BlockedASNs map[string][]int `toml:"blockedASNs"`
|
||||
BlockedASNNames map[string][]string `toml:"blockedASNNames"`
|
||||
CountryBlockPages map[string]string `toml:"countryBlockPages"`
|
||||
ContinentBlockPages map[string]string `toml:"continentBlockPages"`
|
||||
ASNGroupBlockPages map[string]string `toml:"asnGroupBlockPages"`
|
||||
DefaultBlockPage string `toml:"defaultBlockPage"`
|
||||
IPBlockCacheTTLSec int `toml:"ipBlockCacheTTLSec"`
|
||||
}
|
||||
var cfg ipfilterConfig
|
||||
if err := LoadConfig("ipfilter", &cfg); err != nil {
|
||||
log.Fatalf("Failed to load ipfilter config: %v", err)
|
||||
}
|
||||
// override blockedCountryCodes
|
||||
blockedCountryCodes = make(map[string]bool)
|
||||
for _, c := range cfg.BlockedCountryCodes {
|
||||
blockedCountryCodes[c] = true
|
||||
}
|
||||
// override blockedContinentCodes
|
||||
blockedContinentCodes = make(map[string]bool)
|
||||
for _, c := range cfg.BlockedContinentCodes {
|
||||
blockedContinentCodes[c] = true
|
||||
}
|
||||
// override blockedASNs
|
||||
newASNs := make(map[string][]uint)
|
||||
for group, arr := range cfg.BlockedASNs {
|
||||
uintArr := make([]uint, len(arr))
|
||||
for i, v := range arr {
|
||||
uintArr[i] = uint(v)
|
||||
}
|
||||
newASNs[group] = uintArr
|
||||
}
|
||||
blockedASNs = newASNs
|
||||
// override blockedASNNames
|
||||
blockedASNNames = cfg.BlockedASNNames
|
||||
// override page maps
|
||||
countryBlockPages = cfg.CountryBlockPages
|
||||
continentBlockPages = cfg.ContinentBlockPages
|
||||
asnGroupBlockPages = cfg.ASNGroupBlockPages
|
||||
// override default page
|
||||
defaultBlockPage = cfg.DefaultBlockPage
|
||||
// override cache TTL
|
||||
ipBlockCacheTTL = time.Duration(cfg.IPBlockCacheTTLSec) * time.Second
|
||||
|
||||
// Initialize the cache
|
||||
ipBlockCache = make(map[string]blockCacheEntry)
|
||||
|
||||
// Initialize GeoIP databases
|
||||
initGeoIP()
|
||||
|
||||
// Build Aho-Corasick matchers for ASN name matching
|
||||
buildASNNameMatchers()
|
||||
|
||||
// Register IP-block plugin
|
||||
RegisterPlugin("ipfilter", IPBlockMiddleware)
|
||||
}
|
||||
|
||||
// IPBlockMiddleware returns a handler that stops requests coming from blocked IPs
|
||||
// Use this early so bad traffic gets rejected fast
|
||||
func IPBlockMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Get the client IP
|
||||
clientIP := getRealIP(c)
|
||||
|
||||
// Check IP blocklist (GeoIP/ASN)
|
||||
if blocked, blockType, blockValue, customPage, asnOrgName := isBlockedIPExtended(clientIP); blocked {
|
||||
// For API requests, return JSON response
|
||||
if strings.HasPrefix(c.Path(), "/api") {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "Access denied from your location or network.",
|
||||
"reason": "geoip",
|
||||
"type": blockType,
|
||||
"value": blockValue,
|
||||
"asn_org": asnOrgName,
|
||||
})
|
||||
}
|
||||
|
||||
// For HTML requests, serve a block page
|
||||
// Log block information
|
||||
log.Printf("Blocking access: type=%s, value=%s, custom_page=%s, asn_org=%s",
|
||||
blockType, blockValue, customPage, asnOrgName)
|
||||
|
||||
// If no custom page specified, use the default
|
||||
if customPage == "" {
|
||||
customPage = defaultBlockPage
|
||||
}
|
||||
|
||||
// Try to serve the block page
|
||||
possiblePaths := []string{
|
||||
"./public/static/" + customPage,
|
||||
"./public/html/" + customPage,
|
||||
"./public/" + customPage,
|
||||
"./develop/static/" + customPage,
|
||||
}
|
||||
|
||||
var htmlContentBytes []byte
|
||||
var err error
|
||||
foundPath := ""
|
||||
|
||||
for _, path := range possiblePaths {
|
||||
if htmlContentBytes, err = os.ReadFile(path); err == nil {
|
||||
// Found the file, serve it
|
||||
foundPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundPath != "" {
|
||||
// Found the file, serve it
|
||||
c.Status(fiber.StatusForbidden)
|
||||
c.Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
// Replace placeholder with ASN Org Name if available
|
||||
htmlContent := string(htmlContentBytes)
|
||||
replaceValue := "Blocked Network" // Default fallback text
|
||||
if asnOrgName != "" {
|
||||
replaceValue = asnOrgName
|
||||
}
|
||||
htmlContent = strings.Replace(htmlContent, "{{.ASNName}}", replaceValue, -1)
|
||||
|
||||
return c.SendString(htmlContent)
|
||||
}
|
||||
|
||||
// If no custom or default page was found, fall back to default message
|
||||
return c.Status(fiber.StatusForbidden).SendString("Access denied from your location or network.")
|
||||
}
|
||||
|
||||
// Not blocked, continue to next middleware
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// initGeoIP loads the GeoLite2 databases
|
||||
func initGeoIP() {
|
||||
var err error
|
||||
geoipCountryReader, err = geoip2.Open(geoIPCountryDBPath)
|
||||
if err != nil {
|
||||
log.Printf("WARNING: Could not open GeoLite2 Country database at %s: %v. GeoIP country/continent blocking disabled.", geoIPCountryDBPath, err)
|
||||
}
|
||||
geoipASNReader, err = geoip2.Open(geoIPASNDBPath)
|
||||
if err != nil {
|
||||
log.Printf("WARNING: Could not open GeoLite2 ASN database at %s: %v. GeoIP ASN blocking disabled.", geoIPASNDBPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// isBlockedIP checks if the IP address is blocked based on GeoIP or ASN.
|
||||
func isBlockedIP(ipStr string) bool {
|
||||
blocked, _, _, _, _ := isBlockedIPExtended(ipStr)
|
||||
return blocked
|
||||
}
|
||||
|
||||
// isBlockedIPExtended checks if the IP address is blocked and returns detailed information.
|
||||
// Returns:
|
||||
// - blocked: true if IP is blocked
|
||||
// - blockType: "country", "continent", "asn_number_group", or "asn_name_group"
|
||||
// - blockValue: the country code, continent code, or ASN group name (that triggered the block)
|
||||
// - customPage: path to custom block page if defined, or empty string
|
||||
// - asnOrgName: the specific ASN Organization name if blockType is "asn_name_group"
|
||||
func isBlockedIPExtended(ipStr string) (blocked bool, blockType string, blockValue string, customPage string, asnOrgName string) {
|
||||
// Safeguard against empty IPs causing issues
|
||||
if ipStr == "" {
|
||||
log.Printf("ERROR: Empty IP passed to isBlockedIPExtended")
|
||||
return false, "", "", "", ""
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
log.Printf("WARNING: Could not parse IP address for GeoIP check: %s", ipStr)
|
||||
return false, "", "", "", "" // Cannot check invalid IP
|
||||
}
|
||||
|
||||
// --- Check Cache First --- //
|
||||
ipBlockCacheMutex.RLock()
|
||||
entry, found := ipBlockCache[ipStr]
|
||||
ipBlockCacheMutex.RUnlock() // Unlock immediately after reading
|
||||
|
||||
if found && time.Now().Before(entry.expiresAt) {
|
||||
// Return cached result
|
||||
return entry.blocked, entry.blockType, entry.blockValue, entry.customPage, entry.asnOrgName
|
||||
}
|
||||
// --- End Cache Check --- //
|
||||
|
||||
var countryRecord *geoip2.Country
|
||||
var asnRecord *geoip2.ASN
|
||||
var countryErr, asnErr error
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Concurrently perform GeoIP lookups if readers are available
|
||||
if geoipCountryReader != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
countryRecord, countryErr = geoipCountryReader.Country(ip)
|
||||
}()
|
||||
}
|
||||
|
||||
if geoipASNReader != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
asnRecord, asnErr = geoipASNReader.ASN(ip)
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for lookups to complete
|
||||
wg.Wait()
|
||||
|
||||
// --- Process Results --- //
|
||||
|
||||
// 1. Check Country/Continent
|
||||
if countryRecord != nil && countryErr == nil {
|
||||
// Check country first
|
||||
if blockedCountryCodes[countryRecord.Country.IsoCode] {
|
||||
blockType = "country"
|
||||
blockValue = countryRecord.Country.IsoCode
|
||||
log.Printf("INFO: Blocking IP %s based on %s: %s", ipStr, blockType, blockValue)
|
||||
customPage = countryBlockPages[blockValue]
|
||||
return cacheAndReturnBlockResult(ipStr, blockType, blockValue, customPage, "")
|
||||
}
|
||||
|
||||
// Then check continent
|
||||
if blockedContinentCodes[countryRecord.Continent.Code] {
|
||||
blockType = "continent"
|
||||
blockValue = countryRecord.Continent.Code
|
||||
log.Printf("INFO: Blocking IP %s based on %s: %s", ipStr, blockType, blockValue)
|
||||
customPage = continentBlockPages[blockValue]
|
||||
return cacheAndReturnBlockResult(ipStr, blockType, blockValue, customPage, "")
|
||||
}
|
||||
} else if countryErr != nil && !strings.Contains(countryErr.Error(), "cannot be found in the database") {
|
||||
// Log errors other than "not found"
|
||||
log.Printf("WARNING: GeoIP country lookup error for IP %s: %v", ipStr, countryErr)
|
||||
}
|
||||
|
||||
// 2. Check ASN (Number and Name)
|
||||
if asnRecord != nil && asnErr == nil {
|
||||
clientASN := asnRecord.AutonomousSystemNumber
|
||||
asnOrg := asnRecord.AutonomousSystemOrganization // Keep org name for potential use
|
||||
|
||||
// Check ASN Number Groups
|
||||
for groupName, asnList := range blockedASNs {
|
||||
for _, blockedASN := range asnList {
|
||||
if clientASN == blockedASN {
|
||||
blockType = "asn_number_group"
|
||||
blockValue = groupName
|
||||
log.Printf("INFO: Blocking IP %s based on %s: %s (ASN: %d - %s)", ipStr, blockType, blockValue, clientASN, asnOrg)
|
||||
customPage = asnGroupBlockPages[groupName]
|
||||
return cacheAndReturnBlockResult(ipStr, blockType, blockValue, customPage, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check ASN Name Groups (Case-Insensitive Substring Match)
|
||||
if asnOrg != "" {
|
||||
lowerASNOrg := strings.ToLower(asnOrg)
|
||||
|
||||
// Acquire read lock for accessing the global matchers map
|
||||
asnNameMatchersMutex.RLock()
|
||||
defer asnNameMatchersMutex.RUnlock()
|
||||
|
||||
for groupName, matcher := range asnNameMatchers {
|
||||
if matcher == nil { // Check added during previous fix, keep it
|
||||
log.Printf("WARNING: Nil matcher found for group: %s during check", groupName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Use Aho-Corasick matcher - protect against any potential panic
|
||||
var matches []int
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("RECOVERED from panic in Aho-Corasick: %v", r)
|
||||
matches = nil // Ensure it's empty if we panic
|
||||
}
|
||||
}()
|
||||
// Match against the globally shared matcher (assumed read-safe)
|
||||
matches = matcher.Match([]byte(lowerASNOrg))
|
||||
}()
|
||||
|
||||
if len(matches) > 0 {
|
||||
blockType = "asn_name_group"
|
||||
blockValue = groupName
|
||||
log.Printf("INFO: Blocking IP %s based on %s: %s (ASN: %d, Org: '%s')", ipStr, blockType, blockValue, clientASN, asnOrg)
|
||||
customPage = asnGroupBlockPages[groupName]
|
||||
// No need to unlock here, defer handles it
|
||||
return cacheAndReturnBlockResult(ipStr, blockType, blockValue, customPage, asnOrg)
|
||||
}
|
||||
}
|
||||
// RUnlock happens via defer
|
||||
}
|
||||
} else if asnErr != nil && !strings.Contains(asnErr.Error(), "cannot be found in the database") {
|
||||
// Log errors other than "not found"
|
||||
log.Printf("WARNING: GeoIP ASN lookup error for IP %s: %v", ipStr, asnErr)
|
||||
}
|
||||
|
||||
// --- Cache the result before returning --- //
|
||||
computedEntry := blockCacheEntry{
|
||||
blocked: false,
|
||||
expiresAt: time.Now().Add(ipBlockCacheTTL),
|
||||
}
|
||||
ipBlockCacheMutex.Lock()
|
||||
ipBlockCache[ipStr] = computedEntry
|
||||
ipBlockCacheMutex.Unlock()
|
||||
return false, "", "", "", "" // Not blocked
|
||||
}
|
||||
|
||||
// Helper function to cache block results
|
||||
func cacheAndReturnBlockResult(ipStr string, blockType string, blockValue string, customPage string, asnOrgName string) (bool, string, string, string, string) {
|
||||
// Create the cache entry
|
||||
computedEntry := blockCacheEntry{
|
||||
blocked: true,
|
||||
blockType: blockType,
|
||||
blockValue: blockValue,
|
||||
customPage: customPage,
|
||||
asnOrgName: asnOrgName,
|
||||
expiresAt: time.Now().Add(ipBlockCacheTTL),
|
||||
}
|
||||
|
||||
// Use a separate defer+recover to ensure we don't crash the entire server
|
||||
// if there's any issue with the cache
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("RECOVERED from panic while caching result: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ipBlockCacheMutex.Lock()
|
||||
defer ipBlockCacheMutex.Unlock() // Use defer to ensure unlock happens
|
||||
ipBlockCache[ipStr] = computedEntry
|
||||
}()
|
||||
|
||||
return true, blockType, blockValue, customPage, asnOrgName
|
||||
}
|
||||
|
||||
// buildASNNameMatchers creates Aho-Corasick matchers for faster ASN name checking
|
||||
func buildASNNameMatchers() {
|
||||
// Acquire write lock before modifying the global map
|
||||
asnNameMatchersMutex.Lock()
|
||||
defer asnNameMatchersMutex.Unlock()
|
||||
|
||||
// Clear any existing matchers first
|
||||
asnNameMatchers = make(map[string]*ahocorasick.Matcher)
|
||||
|
||||
for groupName, nameList := range blockedASNNames {
|
||||
// Skip if the name list is empty
|
||||
if len(nameList) == 0 {
|
||||
log.Printf("Skipping matcher build for empty group: %s", groupName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert names to lowercase byte slices for case-insensitive matching
|
||||
dict := make([][]byte, 0, len(nameList))
|
||||
for _, name := range nameList {
|
||||
if name != "" {
|
||||
dict = append(dict, []byte(strings.ToLower(name)))
|
||||
}
|
||||
}
|
||||
|
||||
// Only create a matcher if we have patterns
|
||||
if len(dict) > 0 {
|
||||
// Use a recovery mechanism in case the matcher creation fails
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("PANIC while building Aho-Corasick matcher for group %s: %v", groupName, r)
|
||||
// Ensure the entry for this group is nil if creation failed
|
||||
asnNameMatchers[groupName] = nil
|
||||
}
|
||||
}()
|
||||
|
||||
// This assignment happens under the write lock
|
||||
asnNameMatchers[groupName] = ahocorasick.NewMatcher(dict)
|
||||
log.Printf("Built Aho-Corasick matcher for ASN name group: %s (%d patterns)", groupName, len(dict))
|
||||
}()
|
||||
} else {
|
||||
log.Printf("No valid patterns found for ASN name group: %s", groupName)
|
||||
}
|
||||
}
|
||||
// Unlock happens via defer
|
||||
}
|
||||
|
||||
// ReloadGeoIPDatabases closes and reopens the GeoIP database readers
|
||||
// to load updated database files. Safe to call while the server is running.
|
||||
func ReloadGeoIPDatabases() {
|
||||
// Close existing readers if they're open
|
||||
if geoipCountryReader != nil {
|
||||
geoipCountryReader.Close()
|
||||
geoipCountryReader = nil
|
||||
}
|
||||
if geoipASNReader != nil {
|
||||
geoipASNReader.Close()
|
||||
geoipASNReader = nil
|
||||
}
|
||||
|
||||
// Re-initialize the readers
|
||||
initGeoIP()
|
||||
log.Printf("GeoIP databases reloaded")
|
||||
}
|
||||
|
||||
// getRealIP gets the real client IP when behind a reverse proxy
|
||||
// It checks X-Forwarded-For header first, then falls back to c.IP()
|
||||
func getRealIP(c *fiber.Ctx) string {
|
||||
// Check X-Forwarded-For header first
|
||||
if xff := c.Get("X-Forwarded-For"); xff != "" {
|
||||
// X-Forwarded-For can contain multiple IPs (client, proxy1, proxy2, ...)
|
||||
// The first one is the original client IP
|
||||
ips := strings.Split(xff, ",")
|
||||
if len(ips) > 0 {
|
||||
// Get the first IP and trim whitespace
|
||||
clientIP := strings.TrimSpace(ips[0])
|
||||
// Validate it's a real IP
|
||||
if net.ParseIP(clientIP) != nil {
|
||||
log.Printf("Using X-Forwarded-For IP: %s (original: %s)", clientIP, c.IP())
|
||||
return clientIP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for custom Remote-Addr header that might be set by some proxies
|
||||
if remoteAddr := c.Get("$remote_addr"); remoteAddr != "" {
|
||||
// Validate it's a real IP
|
||||
if net.ParseIP(remoteAddr) != nil {
|
||||
log.Printf("Using $remote_addr IP: %s (original: %s)", remoteAddr, c.IP())
|
||||
return remoteAddr
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default IP
|
||||
return c.IP()
|
||||
}
|
||||
47
checkpoint_service/middleware/middleware/plugin.go
Normal file
47
checkpoint_service/middleware/middleware/plugin.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Package middleware contains a simple plugin system for Fiber middleware.
|
||||
// Register plugins by name and factory, then main.go will load them automatically.
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// Plugin holds a plugin's name and a function that makes its handler.
|
||||
type Plugin struct {
|
||||
Name string
|
||||
Factory func() fiber.Handler
|
||||
}
|
||||
|
||||
// registry stores every plugin we've registered.
|
||||
var registry []Plugin
|
||||
|
||||
// RegisterPlugin tags a plugin with a name and a factory so we can use it in the app.
|
||||
func RegisterPlugin(name string, factory func() fiber.Handler) {
|
||||
registry = append(registry, Plugin{Name: name, Factory: factory})
|
||||
}
|
||||
|
||||
// LoadPlugins returns the handler functions for each plugin.
|
||||
// If skipCheckpoint is true, it skips the plugin named "checkpoint".
|
||||
func LoadPlugins(skipCheckpoint bool) []fiber.Handler {
|
||||
var handlers []fiber.Handler
|
||||
for _, p := range registry {
|
||||
if skipCheckpoint && p.Name == "checkpoint" {
|
||||
continue
|
||||
}
|
||||
handlers = append(handlers, p.Factory())
|
||||
}
|
||||
return handlers
|
||||
}
|
||||
|
||||
// LoadConfig loads the TOML file at middleware/config/[name].toml
|
||||
// and decodes it into the struct you provide.
|
||||
func LoadConfig(name string, v interface{}) error {
|
||||
path := filepath.Join("middleware", "config", name+".toml")
|
||||
if _, err := toml.DecodeFile(path, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
47
checkpoint_service/middleware/plugin.go
Normal file
47
checkpoint_service/middleware/plugin.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Package middleware contains a simple plugin system for Fiber middleware.
|
||||
// Register plugins by name and factory, then main.go will load them automatically.
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// Plugin holds a plugin's name and a function that makes its handler.
|
||||
type Plugin struct {
|
||||
Name string
|
||||
Factory func() fiber.Handler
|
||||
}
|
||||
|
||||
// registry stores every plugin we've registered.
|
||||
var registry []Plugin
|
||||
|
||||
// RegisterPlugin tags a plugin with a name and a factory so we can use it in the app.
|
||||
func RegisterPlugin(name string, factory func() fiber.Handler) {
|
||||
registry = append(registry, Plugin{Name: name, Factory: factory})
|
||||
}
|
||||
|
||||
// LoadPlugins returns the handler functions for each plugin.
|
||||
// If skipCheckpoint is true, it skips the plugin named "checkpoint".
|
||||
func LoadPlugins(skipCheckpoint bool) []fiber.Handler {
|
||||
var handlers []fiber.Handler
|
||||
for _, p := range registry {
|
||||
if skipCheckpoint && p.Name == "checkpoint" {
|
||||
continue
|
||||
}
|
||||
handlers = append(handlers, p.Factory())
|
||||
}
|
||||
return handlers
|
||||
}
|
||||
|
||||
// LoadConfig loads the TOML file at middleware/config/[name].toml
|
||||
// and decodes it into the struct you provide.
|
||||
func LoadConfig(name string, v interface{}) error {
|
||||
path := filepath.Join("middleware", "config", name+".toml")
|
||||
if _, err := toml.DecodeFile(path, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in a new issue