Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: HCaptcha Middleware #1071

Merged
merged 16 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ./...
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
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
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved

![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.
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved

:::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,
})

ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
app.Get("/api/", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{
"hcaptcha_site_key": TestSiteKey,
})
})

ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
app.Post("/api/robots-excluded", func(c fiber.Ctx) error {
return c.SendString("You are not a robot")
}, captcha)

ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
log.Fatal(app.Listen(":3000"))
}
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
```
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()
}
Loading
Loading