Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
# any change to gnovm code can make examples fail
- gnovm/**
- examples/**
- misc/deployments/**
- contribs/gnogenesis/**
workflow_dispatch:
inputs:
debug:
Expand Down Expand Up @@ -67,6 +69,28 @@ jobs:
modulepath: "examples"
go-version: "1.24.x"

#deployments:
# name: Validate deployments
# runs-on: ubuntu-latest
# timeout-minutes: 15
# steps:
# - uses: actions/checkout@v5
# - uses: actions/setup-go@v5
# with:
# go-version: "1.23.x"
# - name: Run deployment generate & test
# run: |
# for dir in misc/deployments/*/; do
# if [ -f "$dir/Makefile" ] && make -C "$dir" -n generate >/dev/null 2>&1; then
# echo "=== $dir ==="
# make -C "$dir" generate
# if make -C "$dir" -n test >/dev/null 2>&1; then
# make -C "$dir" test
# fi
# make -C "$dir" clean
# fi
# done

mod-tidy:
strategy:
fail-fast: false
Expand Down
23 changes: 18 additions & 5 deletions contribs/gnofaucet/captcha.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import (
)

type captchaCfg struct {
rootCfg *serveCfg
captchaSecret string
rootCfg *serveCfg
captchaSecret string
captchaSitekey string
}

var errCaptchaMissing = fmt.Errorf("captcha secret is required")
Expand All @@ -22,7 +23,14 @@ func (c *captchaCfg) RegisterFlags(fs *flag.FlagSet) {
&c.captchaSecret,
"captcha-secret",
"",
"hcaptcha secret key (if empty, captcha are disabled)",
"hcaptcha secret key (required)",
)

fs.StringVar(
&c.captchaSitekey,
"captcha-sitekey",
"",
"hcaptcha site key; when set, tokens issued for other site keys are rejected",
)
}

Expand All @@ -49,6 +57,11 @@ func execCaptcha(ctx context.Context, cfg *captchaCfg, io commands.IO) error {
return errCaptchaMissing
}

logger, err := cfg.rootCfg.newLogger(io)
if err != nil {
return err
}

// Start the IP throttler
st := newIPThrottler(defaultRateLimitInterval, defaultCleanTimeout)
st.start(ctx)
Expand All @@ -59,13 +72,13 @@ func execCaptcha(ctx context.Context, cfg *captchaCfg, io commands.IO) error {
}

rpcMiddlewares := []faucet.Middleware{
captchaMiddleware(cfg.captchaSecret),
captchaMiddleware(cfg.captchaSecret, cfg.captchaSitekey, logger),
}

return serveFaucet(
ctx,
cfg.rootCfg,
io,
logger,
faucet.WithHTTPMiddlewares(httpMiddlewares),
faucet.WithMiddlewares(rpcMiddlewares),
)
Expand Down
24 changes: 9 additions & 15 deletions contribs/gnofaucet/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ import (

"github.com/Khan/genqlient/graphql"
"github.com/gnolang/faucet"
"github.com/gnolang/gno/gno.land/pkg/log"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/google/go-github/v74/github"
"github.com/jferrl/go-githubauth"
"github.com/redis/go-redis/v9"
"go.uber.org/zap/zapcore"
"golang.org/x/oauth2"

igh "github.com/gnolang/gno/contribs/gnofaucet/github"
Expand Down Expand Up @@ -159,12 +157,10 @@ func execGithub(ctx context.Context, cfg *githubCfg, io commands.IO) error {

rr := igh.NewRedisRewarder(rdb, rewarderCfg)

logger := log.ZapLoggerToSlog(
log.NewZapJSONLogger(
io.Out(),
zapcore.DebugLevel,
),
)
logger, err := cfg.rootCfg.newLogger(io)
if err != nil {
return err
}

// Prepare the middlewares
httpMiddlewares := []func(http.Handler) http.Handler{
Expand All @@ -177,7 +173,7 @@ func execGithub(ctx context.Context, cfg *githubCfg, io commands.IO) error {
return serveFaucet(
ctx,
cfg.rootCfg,
io,
logger,
faucet.WithHTTPMiddlewares(httpMiddlewares),
faucet.WithMiddlewares(rpcMiddlewares),
)
Expand Down Expand Up @@ -215,12 +211,10 @@ func execGHFetcher(ctx context.Context, cfg *ghFetcherCfg, io commands.IO) error
return fmt.Errorf("unable to connect to redis, %w", err)
}

logger := log.ZapLoggerToSlog(
log.NewZapJSONLogger(
io.Out(),
zapcore.DebugLevel,
),
)
logger, err := cfg.rootCfg.newLogger(io)
if err != nil {
return err
}

appTokenSource, err := githubauth.NewApplicationTokenSource(appID, privKey)
if err != nil {
Expand Down
39 changes: 35 additions & 4 deletions contribs/gnofaucet/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net"
"net/http"
"net/netip"
Expand All @@ -16,6 +17,11 @@ import (
"github.com/gnolang/faucet/spec"
)

// contextKey is an unexported type for context keys in this package.
type contextKey int

const remoteIPContextKey contextKey = iota

// ipMiddleware returns the IP verification middleware, using the given subnet throttler
func ipMiddleware(behindProxy bool, st *ipThrottler) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
Expand Down Expand Up @@ -69,15 +75,18 @@ func ipMiddleware(behindProxy bool, st *ipThrottler) func(next http.Handler) htt
return
}

// Store the resolved IP in the context for use by RPC middlewares
ctx := context.WithValue(r.Context(), remoteIPContextKey, hostAddr.String())

// Continue with serving the faucet request
next.ServeHTTP(w, r)
next.ServeHTTP(w, r.WithContext(ctx))
},
)
}
}

// captchaMiddleware returns the captcha middleware, if any
func captchaMiddleware(secret string) faucet.Middleware {
func captchaMiddleware(secret, sitekey string, logger *slog.Logger) faucet.Middleware {
return func(next faucet.HandlerFunc) faucet.HandlerFunc {
return func(ctx context.Context, req *spec.BaseJSONRequest) *spec.BaseJSONResponse {
// Parse the request meta to extract the captcha secret
Expand All @@ -94,8 +103,11 @@ func captchaMiddleware(secret string) faucet.Middleware {
)
}

// Extract the resolved client IP stored by ipMiddleware
remoteIP, _ := ctx.Value(remoteIPContextKey).(string)

// Verify the captcha response
if err := checkHcaptcha(secret, strings.TrimSpace(meta.Captcha)); err != nil {
if err := checkHcaptcha(secret, strings.TrimSpace(meta.Captcha), remoteIP, sitekey, logger); err != nil {
return spec.NewJSONResponse(
req.ID,
nil,
Expand All @@ -110,7 +122,7 @@ func captchaMiddleware(secret string) faucet.Middleware {
}

// checkHcaptcha checks the captcha challenge
func checkHcaptcha(secret, response string) error {
func checkHcaptcha(secret, response, remoteIP, sitekey string, logger *slog.Logger) error {
// Create an HTTP client with a timeout
client := &http.Client{
Timeout: time.Second * 10,
Expand All @@ -120,6 +132,18 @@ func checkHcaptcha(secret, response string) error {
form := url.Values{}
form.Set("secret", secret)
form.Set("response", response)
if remoteIP != "" {
form.Set("remoteip", remoteIP)
}
if sitekey != "" {
form.Set("sitekey", sitekey)
}

logger.Debug("sending hcaptcha verification request",
slog.String("remoteip", remoteIP),
slog.Bool("secret_set", secret != ""),
slog.Bool("sitekey_set", sitekey != ""),
)

// Create the request
req, err := http.NewRequest(
Expand Down Expand Up @@ -151,6 +175,13 @@ func checkHcaptcha(secret, response string) error {
return fmt.Errorf("failed to decode response, %w", err)
}

logger.Debug("received hcaptcha verification response",
slog.String("remoteip", remoteIP),
slog.Bool("success", body.Success),
slog.String("hostname", body.Hostname),
slog.Any("error_codes", body.ErrorCodes),
)

// Check if the hcaptcha verification was successful
if !body.Success {
return errInvalidCaptcha
Expand Down
37 changes: 33 additions & 4 deletions contribs/gnofaucet/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -12,6 +13,9 @@ import (
"github.com/stretchr/testify/require"
)

// discardLogger is a no-op logger for use in tests.
var discardLogger = slog.New(slog.NewTextHandler(io.Discard, nil))

// hCaptcha test credentials — always pass verification without a real browser.
// See: https://docs.hcaptcha.com/#integration-testing-test-keys
const (
Expand All @@ -37,6 +41,8 @@ func TestCheckHcaptcha(t *testing.T) {

assert.Equal(t, "test-secret", vals.Get("secret"))
assert.Equal(t, "test-response", vals.Get("response"))
assert.Empty(t, vals.Get("remoteip"))
assert.Empty(t, vals.Get("sitekey"))

// Verify no query params were used
assert.Empty(t, r.URL.RawQuery)
Expand All @@ -50,7 +56,30 @@ func TestCheckHcaptcha(t *testing.T) {
siteVerifyURL = srv.URL
defer func() { siteVerifyURL = orig }()

require.NoError(t, checkHcaptcha("test-secret", "test-response"))
require.NoError(t, checkHcaptcha("test-secret", "test-response", "", "", discardLogger))
})

t.Run("success with remoteip and sitekey", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)

vals, err := url.ParseQuery(string(body))
require.NoError(t, err)

assert.Equal(t, "1.2.3.4", vals.Get("remoteip"))
assert.Equal(t, "test-sitekey", vals.Get("sitekey"))

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(SiteVerifyResponse{Success: true})
}))
defer srv.Close()

orig := siteVerifyURL
siteVerifyURL = srv.URL
defer func() { siteVerifyURL = orig }()

require.NoError(t, checkHcaptcha("test-secret", "test-response", "1.2.3.4", "test-sitekey", discardLogger))
})

t.Run("verification failure", func(t *testing.T) {
Expand All @@ -64,7 +93,7 @@ func TestCheckHcaptcha(t *testing.T) {
siteVerifyURL = srv.URL
defer func() { siteVerifyURL = orig }()

err := checkHcaptcha("test-secret", "bad-token")
err := checkHcaptcha("test-secret", "bad-token", "", "", discardLogger)
assert.Equal(t, errInvalidCaptcha, err)
})

Expand All @@ -78,7 +107,7 @@ func TestCheckHcaptcha(t *testing.T) {
siteVerifyURL = srv.URL
defer func() { siteVerifyURL = orig }()

err := checkHcaptcha("test-secret", "test-response")
err := checkHcaptcha("test-secret", "test-response", "", "", discardLogger)
assert.ErrorContains(t, err, "unexpected status code")
})

Expand All @@ -89,6 +118,6 @@ func TestCheckHcaptcha(t *testing.T) {
t.Skip("skipping network test in short mode")
}

require.NoError(t, checkHcaptcha(hcaptchaTestSecret, hcaptchaTestResponse))
require.NoError(t, checkHcaptcha(hcaptchaTestSecret, hcaptchaTestResponse, "", "", discardLogger))
})
}
Loading
Loading