diff --git a/.env.example b/.env.example index 4ef221d96f4d561e3d9238847bf441eb6f2a3704..ba76ff58b1d5c2efc18274ce16393ec2077fd99d 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,6 @@ -ALLOWED_ORIGINS=https://yourdomain.com \ No newline at end of file +ALLOWED_ORIGINS=https://hizla.io +DB=db +VERBOSE=1 +LISTEN_ADDR=127.0.0.1:3000 +HCAPTCHA_SITE_KEY=unset +HCAPTCHA_SECRET=unset \ No newline at end of file diff --git a/app.go b/app.go index df0e0bc00b02fef6b8e5ef1c2107a68b1cb728d3..2f2e8b76055aa48c8dedceccab3b1f27a846deba 100644 --- a/app.go +++ b/app.go @@ -6,9 +6,10 @@ import ( "os" "time" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/limiter" + "github.com/gofiber/contrib/hcaptcha" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/cors" + "github.com/gofiber/fiber/v3/middleware/limiter" "github.com/syndtr/goleveldb/leveldb" ) @@ -17,15 +18,15 @@ func serve(sig chan os.Signal, db *leveldb.DB) error { // cors app.Use(cors.New(cors.Config{ - AllowOrigins: conf[allowedOrigins], - AllowHeaders: "Origin, Content-Type, Accept", + AllowOrigins: []string{conf[allowedURL]}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, })) // rate limiting app.Use(limiter.New(limiter.Config{ Max: 5, Expiration: 7 * 24 * time.Hour, // 1 week expiration - LimitReached: func(c *fiber.Ctx) error { + LimitReached: func(c fiber.Ctx) error { log.Printf("Rate limit exceeded for IP: %s", c.IP()) return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ "message": "Rate limit exceeded. Max 5 registrations per week.", @@ -33,13 +34,31 @@ func serve(sig chan os.Signal, db *leveldb.DB) error { }, })) + var captcha fiber.Handler + hCaptchaEnable := conf[hCaptchaSiteKey] != "unset" && conf[hCaptchaSecretKey] != "unset" + + if hCaptchaEnable { + // create hCaptcha middleware if enabled + captcha = hcaptcha.New(hcaptcha.Config{ + SecretKey: conf[hCaptchaSecretKey], + }) + + log.Printf("hCaptcha enabled with site key %q", conf[hCaptchaSiteKey]) + } else { + // empty middleware if disabled + captcha = func(c fiber.Ctx) error { + return c.Next() + } + + log.Printf("hCaptcha disabled because one or both of %q and %q are unset", + confEnv[hCaptchaSiteKey][0], confEnv[hCaptchaSecretKey][0]) + } + // /register - routeRegister(app, db) + routeRegister(app, db, captcha) - // Graceful shutdown - app.Use(func(c *fiber.Ctx) error { - return c.Next() - }) + // /hcaptcha-site-key + routeHCaptchaSiteKey(app, !hCaptchaEnable, conf[hCaptchaSiteKey]) // graceful shutdown go func() { diff --git a/captcha.go b/captcha.go new file mode 100644 index 0000000000000000000000000000000000000000..f2d517d514d49c5f41f1f7d69333fe2023b58f15 --- /dev/null +++ b/captcha.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/gofiber/fiber/v3" +) + +type respHSiteKey struct { + Success bool `json:"success"` + SiteKey string `json:"hcaptcha_site_key"` +} + +// Route to expose hCaptcha site key. +// Returns a constant pre-generated response +// to avoid unnecessary allocations or serialisations +func routeHCaptchaSiteKey(app *fiber.App, stub bool, siteKey string) { + var resp string + if stub { + resp = mustConstResp(newMessage(false, "hCaptcha is not enabled on this instance.")) + } else { + resp = mustConstResp(respHSiteKey{true, siteKey}) + } + + app.Get("/captcha", func(c fiber.Ctx) error { + c.Set("Content-Type", "application/json; charset=utf-8") + return c.SendString(resp) + }) +} diff --git a/conf.go b/conf.go index cb9cafb6c9df22993b66ac9dfe2e536b8e8346cd..a95b71fafe42fc5bae60d13befa533a67b89674b 100644 --- a/conf.go +++ b/conf.go @@ -8,27 +8,29 @@ import ( const ( dbPath uint8 = iota listenAddr - allowedOrigins + allowedURL + hCaptchaSiteKey + hCaptchaSecretKey verboseLogging - - confLen ) // env variable, default pairing -var confEnv = [confLen][2]string{ - {"DB", "db"}, - {"LISTEN_ADDR", "127.0.0.1:3000"}, - {"ALLOWED_ORIGINS", "https://hizla.io"}, - {"VERBOSE", "1"}, +var confEnv = [...][2]string{ + dbPath: {"DB", "db"}, + listenAddr: {"LISTEN_ADDR", "127.0.0.1:3000"}, + allowedURL: {"ALLOWED_URL", "https://hizla.io"}, + hCaptchaSiteKey: {"HCAPTCHA_SITE_KEY", "unset"}, + hCaptchaSecretKey: {"HCAPTCHA_SECRET_KEY", "unset"}, + verboseLogging: {"VERBOSE", "1"}, } // resolved config values -var conf [confLen]string +var conf [len(confEnv)]string var verbose bool func init() { - for i := 0; i < int(confLen); i++ { + for i := 0; i < len(confEnv); i++ { if v, ok := os.LookupEnv(confEnv[i][0]); !ok { conf[i] = confEnv[i][1] } else { diff --git a/go.mod b/go.mod index 9236dc386fbdb1360fdb2bdd28b0ef6fcbc7c196..ad3be326fc2c784e15c8e9314ae14bf981a33a0e 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,20 @@ module git.randomchars.net/hizla/waitlist/backend go 1.22.3 require ( - github.com/andybalholm/brotli v1.0.5 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/dgraph-io/ristretto v1.0.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/gofiber/fiber/v2 v2.52.5 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/gofiber/contrib/hcaptcha v0.1.1 // indirect + github.com/gofiber/fiber/v3 v3.0.0-beta.3 // indirect + github.com/gofiber/utils/v2 v2.0.0-beta.6 // indirect github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect - github.com/google/uuid v1.5.0 // indirect - github.com/klauspost/compress v1.17.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.11 // 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.15 // indirect github.com/philhofer/fwd v1.1.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/fasthttp v1.56.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect ) diff --git a/go.sum b/go.sum index b39830b4228c522354b3e26b3db4b708ab30c7f9..20edfa0723acce32d88dff9267ccffa04dcbc1de 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,49 @@ -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -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/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84= -github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc= -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/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= -github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/contrib/hcaptcha v0.1.1 h1:zc81+nY83BIieIzg9tr8mlFNGw2NpPjTyDoPsdkG7bQ= +github.com/gofiber/contrib/hcaptcha v0.1.1/go.mod h1:6/2mucDLwGEmwahbmRO07l+4zxjqFBhq7dVQ35C8bBY= +github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg= +github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY= +github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= +github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/gofiber/utils/v2 v2.0.0-beta.6 h1:ED62bOmpRXdgviPlfTmf0Q+AXzhaTUAFtdWjgx+XkYI= +github.com/gofiber/utils/v2 v2.0.0-beta.6/go.mod h1:3Kz8Px3jInKFvqxDzDeoSygwEOO+3uyubTmUa6PqY+0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= 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/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= +github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= +github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U= +github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -66,10 +67,10 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= diff --git a/register.go b/register.go index 38f846b80933dfd4718dca090482aa3b821d24c0..02b4f60031e328308469e0fa080950fd8192bb3f 100644 --- a/register.go +++ b/register.go @@ -4,7 +4,7 @@ import ( "log" "regexp" - "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v3" "github.com/syndtr/goleveldb/leveldb" ) @@ -15,11 +15,12 @@ type registration struct { } // Waitlist registration route -func routeRegister(app *fiber.App, db *leveldb.DB) { - app.Post("/register", func(c *fiber.Ctx) error { +func routeRegister(app *fiber.App, db *leveldb.DB, captcha fiber.Handler) { + app.Post("/register", func(c fiber.Ctx) error { req := new(registration) - if err := c.BodyParser(req); err != nil { + // Parse and validate the request + if err := c.Bind().Body(req); err != nil { if verbose { log.Printf("invalid request from %q: %v", c.IP(), err) } @@ -65,5 +66,5 @@ func routeRegister(app *fiber.App, db *leveldb.DB) { return c.JSON(fiber.Map{ "message": "Email registered successfully", }) - }) + }, captcha) } diff --git a/resp.go b/resp.go new file mode 100644 index 0000000000000000000000000000000000000000..6f763994f5759e0c3144312848cd57ead4ace751 --- /dev/null +++ b/resp.go @@ -0,0 +1,21 @@ +package main + +import "encoding/json" + +// pre-generate constant responses +func mustConstResp(a any) string { + if p, err := json.Marshal(a); err != nil { + panic(err.Error()) + } else { + return string(p) + } +} + +type respM struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +func newMessage(success bool, message string) *respM { + return &respM{success, message} +}