Merge pull request #5469 from filecoin-project/feat/faucet-captcha
Add spam protection to fountain
This commit is contained in:
commit
d12545af6f
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -68,6 +69,10 @@ var runCmd = &cli.Command{
|
||||
EnvVars: []string{"LOTUS_FOUNTAIN_AMOUNT"},
|
||||
Value: "50",
|
||||
},
|
||||
&cli.Float64Flag{
|
||||
Name: "captcha-threshold",
|
||||
Value: 0.5,
|
||||
},
|
||||
},
|
||||
Action: func(cctx *cli.Context) error {
|
||||
sendPerRequest, err := types.ParseFIL(cctx.String("amount"))
|
||||
@ -107,11 +112,13 @@ var runCmd = &cli.Command{
|
||||
WalletRate: 15 * time.Minute,
|
||||
WalletBurst: 2,
|
||||
}),
|
||||
recapThreshold: cctx.Float64("captcha-threshold"),
|
||||
}
|
||||
|
||||
http.Handle("/", http.FileServer(rice.MustFindBox("site").HTTPBox()))
|
||||
http.HandleFunc("/send", h.send)
|
||||
|
||||
box := rice.MustFindBox("site")
|
||||
http.Handle("/", http.FileServer(box.HTTPBox()))
|
||||
http.HandleFunc("/funds.html", prepFundsHtml(box))
|
||||
http.Handle("/send", h)
|
||||
fmt.Printf("Open http://%s\n", cctx.String("front"))
|
||||
|
||||
go func() {
|
||||
@ -123,6 +130,17 @@ var runCmd = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func prepFundsHtml(box *rice.Box) http.HandlerFunc {
|
||||
tmpl := template.Must(template.New("funds").Parse(box.MustString("funds.html")))
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := tmpl.Execute(w, os.Getenv("RECAPTCHA_SITE_KEY"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
ctx context.Context
|
||||
api api.FullNode
|
||||
@ -131,14 +149,44 @@ type handler struct {
|
||||
sendPerRequest types.FIL
|
||||
|
||||
limiter *Limiter
|
||||
recapThreshold float64
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "only POST is allowed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
reqIP := r.Header.Get("X-Real-IP")
|
||||
if reqIP == "" {
|
||||
h, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
log.Errorf("could not get ip from: %s, err: %s", r.RemoteAddr, err)
|
||||
}
|
||||
reqIP = h
|
||||
}
|
||||
|
||||
capResp, err := VerifyToken(r.FormValue("g-recaptcha-response"), reqIP)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
if !capResp.Success || capResp.Score < h.recapThreshold {
|
||||
log.Infow("spam", "capResp", capResp)
|
||||
http.Error(w, "spam protection", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *handler) send(w http.ResponseWriter, r *http.Request) {
|
||||
to, err := address.NewFromString(r.FormValue("address"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if to == address.Undef {
|
||||
http.Error(w, "empty address", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Limit based on wallet address
|
||||
limiter := h.limiter.GetWalletLimiter(to.String())
|
||||
@ -148,15 +196,6 @@ func (h *handler) send(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Limit based on IP
|
||||
|
||||
reqIP := r.Header.Get("X-Real-IP")
|
||||
if reqIP == "" {
|
||||
h, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
log.Errorf("could not get ip from: %s, err: %s", r.RemoteAddr, err)
|
||||
}
|
||||
reqIP = h
|
||||
}
|
||||
if i := net.ParseIP(reqIP); i != nil && i.IsLoopback() {
|
||||
log.Errorf("rate limiting localhost: %s", reqIP)
|
||||
}
|
||||
|
73
cmd/lotus-fountain/recaptcha.go
Normal file
73
cmd/lotus-fountain/recaptcha.go
Normal file
@ -0,0 +1,73 @@
|
||||
// From https://github.com/lukasaron/recaptcha
|
||||
// BLS-3 Licensed
|
||||
// Copyright (c) 2020, Lukas Aron
|
||||
// Modified by Kubuxu
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// content type for communication with the verification server.
|
||||
const (
|
||||
contentType = "application/json"
|
||||
)
|
||||
|
||||
// VerifyURL defines the endpoint which is called when a token needs to be verified.
|
||||
var (
|
||||
VerifyURL, _ = url.Parse("https://www.google.com/recaptcha/api/siteverify")
|
||||
)
|
||||
|
||||
// Response defines the response format from the verification endpoint.
|
||||
type Response struct {
|
||||
Success bool `json:"success"` // status of the verification
|
||||
TimeStamp time.Time `json:"challenge_ts"` // timestamp of the challenge load (ISO format)
|
||||
HostName string `json:"hostname"` // the hostname of the site where the reCAPTCHA was solved
|
||||
Score float64 `json:"score"` // the score for this request (0.0 - 1.0)
|
||||
Action string `json:"action"` // the action name for this request
|
||||
ErrorCodes []string `json:"error-codes"` // error codes
|
||||
AndroidPackageName string `json:"apk_package_name"` // android related only
|
||||
}
|
||||
|
||||
// VerifyToken function implements the basic logic of verification of ReCaptcha token that is usually created
|
||||
// on the user site (front-end) and then sent to verify on the server side (back-end).
|
||||
// To provide a successful verification process the secret key is required. Based on the security recommendations
|
||||
// the key has to be passed as an environmental variable SECRET_KEY.
|
||||
//
|
||||
// Token parameter is required, however remoteIP is optional.
|
||||
func VerifyToken(token, remoteIP string) (Response, error) {
|
||||
resp := Response{}
|
||||
if len(token) == 0 {
|
||||
resp.ErrorCodes = []string{"no-token"}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Add("secret", os.Getenv("RECAPTCHA_SECRET_KEY"))
|
||||
q.Add("response", token)
|
||||
q.Add("remoteip", remoteIP)
|
||||
|
||||
var u *url.URL
|
||||
{
|
||||
verifyCopy := *VerifyURL
|
||||
u = &verifyCopy
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
r, err := http.Post(u.String(), contentType, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
_ = r.Body.Close() // close immediately after reading finished
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, json.Unmarshal(b, &resp)
|
||||
}
|
@ -3,6 +3,13 @@
|
||||
<head>
|
||||
<title>Sending Funds - Lotus Fountain</title>
|
||||
<link rel="stylesheet" type="text/css" href="main.css">
|
||||
<script src="https://www.google.com/recaptcha/api.js"></script>
|
||||
<script>
|
||||
function onSubmit(token) {
|
||||
document.getElementById("funds-form").submit();
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="Index">
|
||||
@ -11,10 +18,13 @@
|
||||
[SENDING FUNDS]
|
||||
</div>
|
||||
<div class="Index-node">
|
||||
<form action='/send' method='get'>
|
||||
<form action='/send' method='post' id='funds-form'>
|
||||
<span>Enter destination address:</span>
|
||||
<input type='text' name='address' style="width: 300px">
|
||||
<button type='submit'>Send Funds</button>
|
||||
<button class="g-recaptcha"
|
||||
data-sitekey="{{ . }}"
|
||||
data-callback='onSubmit'
|
||||
data-action='submit'>Send Funds</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
2
go.mod
2
go.mod
@ -121,6 +121,7 @@ require (
|
||||
github.com/multiformats/go-multiaddr-dns v0.2.0
|
||||
github.com/multiformats/go-multibase v0.0.3
|
||||
github.com/multiformats/go-multihash v0.0.14
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/polydawn/refmt v0.0.0-20190809202753-05966cbd336a
|
||||
github.com/prometheus/client_golang v1.6.0
|
||||
@ -145,6 +146,7 @@ require (
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28
|
||||
gotest.tools v2.2.0+incompatible
|
||||
)
|
||||
|
4
go.sum
4
go.sum
@ -1228,6 +1228,8 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nikkolasg/hexjson v0.0.0-20181101101858-78e39397e00c h1:5bFTChQxSKNwy8ALwOebjekYExl9HTT9urdawqC95tA=
|
||||
github.com/nikkolasg/hexjson v0.0.0-20181101101858-78e39397e00c/go.mod h1:7qN3Y0BvzRUf4LofcoJplQL10lsFDb4PYlePTVwrP28=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
|
||||
@ -1918,6 +1920,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
|
Loading…
Reference in New Issue
Block a user