Skip to content

Commit

Permalink
Merge pull request #1071 from bndrmrtn/main
Browse files Browse the repository at this point in the history
feat: HCaptcha Middleware
  • Loading branch information
ReneWerner87 committed Apr 24, 2024
2 parents 6f2c3a3 + 21b1464 commit a07a32f
Show file tree
Hide file tree
Showing 12 changed files with 415 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,9 @@ updates:
- "🤖 Dependencies"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/hcaptcha" # Location of package manifests
labels:
- "🤖 Dependencies"
schedule:
interval: "daily"
50 changes: 50 additions & 0 deletions .github/release-drafter-hcaptcha.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name-template: 'HCaptcha - v$RESOLVED_VERSION'
tag-template: 'hcaptcha/v$RESOLVED_VERSION'
tag-prefix: hcaptcha/v
include-paths:
- hcaptcha
categories:
- title: '❗ Breaking Changes'
labels:
- '❗ BreakingChange'
- title: '🚀 New'
labels:
- '✏️ Feature'
- title: '🧹 Updates'
labels:
- '🧹 Updates'
- '🤖 Dependencies'
- title: '🐛 Fixes'
labels:
- '☢️ Bug'
- title: '📚 Documentation'
labels:
- '📒 Documentation'
change-template: '- $TITLE (#$NUMBER)'
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
exclude-contributors:
- dependabot
- dependabot[bot]
version-resolver:
major:
labels:
- 'major'
- '❗ BreakingChange'
minor:
labels:
- 'minor'
- '✏️ Feature'
patch:
labels:
- 'patch'
- '📒 Documentation'
- '☢️ Bug'
- '🤖 Dependencies'
- '🧹 Updates'
default: patch
template: |
$CHANGES
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...hcaptcha/v$RESOLVED_VERSION
Thank you $CONTRIBUTORS for making this update possible.
19 changes: 19 additions & 0 deletions .github/workflows/release-drafter-hcaptcha.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Release Drafter HCaptcha
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- master
- main
paths:
- 'hcaptcha/**'
jobs:
draft_release_jwt:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: release-drafter/release-drafter@v6
with:
config-name: release-drafter-hcaptcha.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 changes: 31 additions & 0 deletions .github/workflows/test-hcaptcha.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: "Test hcaptcha"

on:
push:
branches:
- master
- main
paths:
- 'hcaptcha/**'
pull_request:
paths:
- 'hcaptcha/**'

jobs:
Tests:
runs-on: ubuntu-latest
strategy:
matrix:
go-version:
- 1.21.x
- 1.22.x
steps:
- name: Fetch Repository
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '${{ matrix.go-version }}'
- name: Run Test
working-directory: ./hcaptcha
run: go test -v -race ./...
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,5 @@
*.workspace

# Dependencies
/vendor/
vendor/
vendor
/Godeps/
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ Repository for third party middlewares with dependencies.
* [Fibersentry](./fibersentry/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+fibersentry%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-fibersentry.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
* [Fiberzap](./fiberzap/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+fiberzap%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-fiberzap.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
* [Fiberzerolog](./fiberzerolog/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+fiberzerolog%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-fiberzerolog.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
* [HCaptcha](./hcaptcha/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+hcaptcha%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-hcaptcha.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
* [JWT](./jwt/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+jwt%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-jwt.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
* [Loadshed](./loadshed/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+Loadshed%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-loadshed.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
* [Loadshed](./loadshed/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+loadshed%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-loadshed.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
* [NewRelic](./fibernewrelic/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+fibernewrelic%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-fibernewrelic.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
* [Open Policy Agent](./opafiber/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+opafiber%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-opafiber.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
* [Otelfiber (OpenTelemetry)](./otelfiber/README.md) <a href="https://github.com/gofiber/contrib/actions?query=workflow%3A%22Test+otelfiber%22"> <img src="https://img.shields.io/github/actions/workflow/status/gofiber/contrib/test-otelfiber.yml?branch=main&label=%F0%9F%A7%AA%20&style=flat&color=75C46B" /> </a>
Expand Down
83 changes: 83 additions & 0 deletions hcaptcha/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
id: hcaptcha
---

# HCaptcha

![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=hcaptcha*)
[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)
![Test](https://github.com/gofiber/contrib/workflows/Tests/badge.svg)
![Security](https://github.com/gofiber/contrib/workflows/Security/badge.svg)
![Linter](https://github.com/gofiber/contrib/workflows/Linter/badge.svg)

A simple [HCaptcha](https://hcaptcha.com) middleware to prevent bot attacks.

:::note

Requires Go **1.21** and above

:::

## Install

:::caution

This middleware only supports Fiber **v3**.

:::

```shell
go get -u github.com/gofiber/fiber/v3
go get -u github.com/gofiber/contrib/hcaptcha
```

## Signature

```go
hcaptcha.New(config hcaptcha.Config) fiber.Handler
```

## Config

| Property | Type | Description | Default |
|:----------------|:----------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------|
| SecretKey | `string` | The secret key you obtained from the HCaptcha admin panel. This field must not be empty. | `""` |
| ResponseKeyFunc | `func(fiber.Ctx) (string, error)` | ResponseKeyFunc should return the token that captcha provides upon successful solving. By default, it gets the token from the body by parsing a JSON request and returns the `hcaptcha_token` field. | `hcaptcha.DefaultResponseKeyFunc` |
| SiteVerifyURL | `string` | This property specifies the API resource used for token authentication. | `https://api.hcaptcha.com/siteverify` |

## Example

```go
package main

import (
"github.com/gofiber/contrib/hcaptcha"
"github.com/gofiber/fiber/v3"
"log"
)

const (
TestSecretKey = "0x0000000000000000000000000000000000000000"
TestSiteKey = "20000000-ffff-ffff-ffff-000000000002"
)

func main() {
app := fiber.New()
captcha := hcaptcha.New(hcaptcha.Config{
// Must set the secret key
SecretKey: TestSecretKey,
})

app.Get("/api/", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{
"hcaptcha_site_key": TestSiteKey,
})
})

app.Post("/api/robots-excluded", func(c fiber.Ctx) error {
return c.SendString("You are not a robot")
}, captcha)

log.Fatal(app.Listen(":3000"))
}
```
37 changes: 37 additions & 0 deletions hcaptcha/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package hcaptcha

import (
"bytes"
"encoding/json"
"fmt"
"github.com/gofiber/fiber/v3"
)

// DefaultSiteVerifyURL is the default URL for the HCaptcha API
const DefaultSiteVerifyURL = "https://api.hcaptcha.com/siteverify"

// Config defines the config for HCaptcha middleware.
type Config struct {
// SecretKey is the secret key you get from HCaptcha when you create a new application
SecretKey string
// ResponseKeyFunc should return the generated pass UUID from the ctx, which will be validated
ResponseKeyFunc func(fiber.Ctx) (string, error)
// SiteVerifyURL is the endpoint URL where the program should verify the given token
// default value is: "https://api.hcaptcha.com/siteverify"
SiteVerifyURL string
}

// DefaultResponseKeyFunc is the default function to get the HCaptcha token from the request body
func DefaultResponseKeyFunc(c fiber.Ctx) (string, error) {
data := struct {
HCaptchaToken string `json:"hcaptcha_token"`
}{}

err := json.NewDecoder(bytes.NewReader(c.Body())).Decode(&data)

if err != nil {
return "", fmt.Errorf("failed to decode HCaptcha token: %w", err)
}

return data.HCaptchaToken, nil
}
24 changes: 24 additions & 0 deletions hcaptcha/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module github.com/gofiber/contrib/hcaptcha

go 1.21

require (
github.com/gofiber/fiber/v3 v3.0.0-beta.2
github.com/stretchr/testify v1.9.0
github.com/valyala/fasthttp v1.52.0
)

require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.17.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
35 changes: 35 additions & 0 deletions hcaptcha/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
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/gofiber/fiber/v3 v3.0.0-beta.2 h1:mVVgt8PTaHGup3NGl/+7U7nEoZaXJ5OComV4E+HpAao=
github.com/gofiber/fiber/v3 v3.0.0-beta.2/go.mod h1:w7sdfTY0okjZ1oVH6rSOGvuACUIt0By1iK0HKUb3uqM=
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/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.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.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.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
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.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
78 changes: 78 additions & 0 deletions hcaptcha/hcaptcha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Package hcaptcha is a simple middleware that checks for an HCaptcha UUID
// and then validates it. It returns an error if the UUID is not valid (the request may have been sent by a robot).
package hcaptcha

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/gofiber/fiber/v3"
"github.com/valyala/fasthttp"
"net/url"
)

// HCaptcha is a middleware handler that checks for an HCaptcha UUID and then validates it.
type HCaptcha struct {
Config
}

// New creates a new HCaptcha middleware handler.
func New(config Config) fiber.Handler {
if config.SiteVerifyURL == "" {
config.SiteVerifyURL = DefaultSiteVerifyURL
}

if config.ResponseKeyFunc == nil {
config.ResponseKeyFunc = DefaultResponseKeyFunc
}

h := &HCaptcha{
config,
}
return h.Validate
}

// Validate checks for an HCaptcha UUID and then validates it.
func (h *HCaptcha) Validate(c fiber.Ctx) error {
token, err := h.ResponseKeyFunc(c)
if err != nil {
c.Status(fiber.StatusBadRequest)
return fmt.Errorf("error retrieving HCaptcha token: %w", err)
}

req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetBody([]byte(url.Values{
"secret": {h.SecretKey},
"response": {token},
}.Encode()))
req.Header.SetMethod("POST")
req.Header.SetContentType("application/x-www-form-urlencoded; charset=UTF-8")
req.Header.Set("Accept", "application/json")
req.SetRequestURI(h.SiteVerifyURL)
res := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(res)

// Send the request to the HCaptcha API
if err = fasthttp.Do(req, res); err != nil {
c.Status(fiber.StatusBadRequest)
return fmt.Errorf("error sending request to HCaptcha API: %w", err)
}

o := struct {
Success bool `json:"success"`
}{}

if err = json.NewDecoder(bytes.NewReader(res.Body())).Decode(&o); err != nil {
c.Status(fiber.StatusInternalServerError)
return fmt.Errorf("error decoding HCaptcha API response: %w", err)
}

if !o.Success {
c.Status(fiber.StatusForbidden)
return errors.New("unable to check that you are not a robot")
}

return c.Next()
}

0 comments on commit a07a32f

Please sign in to comment.