From ba94f271dbb386c41e0f101d021e3d21a8842681 Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Thu, 28 Jan 2021 16:38:58 +0100 Subject: [PATCH 1/4] Add spam protection to fountain Uses reCAPTCHAv3, `RECAPTCHA_SITE_KEY` and `RECAPTCHA_SECRET_KEY` need to be set in env. Signed-off-by: Jakub Sztandera --- cmd/lotus-fountain/main.go | 67 +++++++++++++++++++++++------- cmd/lotus-fountain/recaptcha.go | 67 ++++++++++++++++++++++++++++++ cmd/lotus-fountain/site/funds.html | 16 +++++-- go.mod | 2 + go.sum | 4 ++ 5 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 cmd/lotus-fountain/recaptcha.go diff --git a/cmd/lotus-fountain/main.go b/cmd/lotus-fountain/main.go index 931978d96..1114ad07e 100644 --- a/cmd/lotus-fountain/main.go +++ b/cmd/lotus-fountain/main.go @@ -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, }), + recapTreshold: 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 @@ -130,15 +148,45 @@ type handler struct { from address.Address sendPerRequest types.FIL - limiter *Limiter + limiter *Limiter + recapTreshold float64 } -func (h *handler) send(w http.ResponseWriter, r *http.Request) { +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.recapTreshold { + log.Infow("spam", "capResp", capResp) + http.Error(w, "spam protection", http.StatusUnprocessableEntity) + return + } + 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) } diff --git a/cmd/lotus-fountain/recaptcha.go b/cmd/lotus-fountain/recaptcha.go new file mode 100644 index 000000000..11e8deaf9 --- /dev/null +++ b/cmd/lotus-fountain/recaptcha.go @@ -0,0 +1,67 @@ +// 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 := VerifyURL.Query() + q.Add("secret", os.Getenv("RECAPTCHA_SECRET_KEY")) + q.Add("response", token) + q.Add("remoteip", remoteIP) + VerifyURL.RawQuery = q.Encode() + + r, err := http.Post(VerifyURL.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) +} diff --git a/cmd/lotus-fountain/site/funds.html b/cmd/lotus-fountain/site/funds.html index cd26032f3..c6916239f 100644 --- a/cmd/lotus-fountain/site/funds.html +++ b/cmd/lotus-fountain/site/funds.html @@ -3,6 +3,13 @@ Sending Funds - Lotus Fountain + + +
@@ -11,10 +18,13 @@ [SENDING FUNDS]
-
+ Enter destination address: - - + +
diff --git a/go.mod b/go.mod index 273391a56..ce09895ad 100644 --- a/go.mod +++ b/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 launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect diff --git a/go.sum b/go.sum index 6d38f4a8e..6d35032f8 100644 --- a/go.sum +++ b/go.sum @@ -1226,6 +1226,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= @@ -1916,6 +1918,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= From 608793314f857c45ab99a7f824792e5150e65cea Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Thu, 28 Jan 2021 18:07:37 +0100 Subject: [PATCH 2/4] Fix url Values Signed-off-by: Jakub Sztandera --- cmd/lotus-fountain/recaptcha.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/lotus-fountain/recaptcha.go b/cmd/lotus-fountain/recaptcha.go index 11e8deaf9..d4e71cc02 100644 --- a/cmd/lotus-fountain/recaptcha.go +++ b/cmd/lotus-fountain/recaptcha.go @@ -46,13 +46,15 @@ func VerifyToken(token, remoteIP string) (Response, error) { resp.ErrorCodes = []string{"no-token"} return resp, nil } - q := VerifyURL.Query() + + q := url.Values{} q.Add("secret", os.Getenv("RECAPTCHA_SECRET_KEY")) q.Add("response", token) q.Add("remoteip", remoteIP) - VerifyURL.RawQuery = q.Encode() - r, err := http.Post(VerifyURL.String(), contentType, nil) + u := &(*VerifyURL) + u.RawQuery = q.Encode() + r, err := http.Post(u.String(), contentType, nil) if err != nil { return resp, err } From 0a0fc47655cd1e4aa9eb6bd53b8c7472226a4be4 Mon Sep 17 00:00:00 2001 From: Jakub Sztandera Date: Thu, 28 Jan 2021 19:58:28 +0100 Subject: [PATCH 3/4] Make lint happy Signed-off-by: Jakub Sztandera --- cmd/lotus-fountain/recaptcha.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/lotus-fountain/recaptcha.go b/cmd/lotus-fountain/recaptcha.go index d4e71cc02..69359faa3 100644 --- a/cmd/lotus-fountain/recaptcha.go +++ b/cmd/lotus-fountain/recaptcha.go @@ -52,7 +52,11 @@ func VerifyToken(token, remoteIP string) (Response, error) { q.Add("response", token) q.Add("remoteip", remoteIP) - u := &(*VerifyURL) + var u *url.URL + { + verifyCopy := *VerifyURL + u = &verifyCopy + } u.RawQuery = q.Encode() r, err := http.Post(u.String(), contentType, nil) if err != nil { From f358af6f5663c893c252e64a5c2d6dc98a4428fc Mon Sep 17 00:00:00 2001 From: Aayush Rajasekaran Date: Tue, 16 Feb 2021 19:19:27 -0500 Subject: [PATCH 4/4] Fix typo --- cmd/lotus-fountain/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/lotus-fountain/main.go b/cmd/lotus-fountain/main.go index 1114ad07e..79f08aa83 100644 --- a/cmd/lotus-fountain/main.go +++ b/cmd/lotus-fountain/main.go @@ -112,7 +112,7 @@ var runCmd = &cli.Command{ WalletRate: 15 * time.Minute, WalletBurst: 2, }), - recapTreshold: cctx.Float64("captcha-threshold"), + recapThreshold: cctx.Float64("captcha-threshold"), } box := rice.MustFindBox("site") @@ -148,8 +148,8 @@ type handler struct { from address.Address sendPerRequest types.FIL - limiter *Limiter - recapTreshold float64 + limiter *Limiter + recapThreshold float64 } func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -172,7 +172,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadGateway) return } - if !capResp.Success || capResp.Score < h.recapTreshold { + if !capResp.Success || capResp.Score < h.recapThreshold { log.Infow("spam", "capResp", capResp) http.Error(w, "spam protection", http.StatusUnprocessableEntity) return