2019-09-20 21:27:40 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2021-01-28 15:38:58 +00:00
|
|
|
"html/template"
|
2019-11-06 15:10:27 +00:00
|
|
|
"net"
|
2019-09-20 21:27:40 +00:00
|
|
|
"net/http"
|
|
|
|
"os"
|
2019-10-17 04:11:47 +00:00
|
|
|
"time"
|
2019-09-20 21:27:40 +00:00
|
|
|
|
2019-10-13 07:33:25 +00:00
|
|
|
rice "github.com/GeertJohan/go.rice"
|
2020-01-08 19:10:57 +00:00
|
|
|
logging "github.com/ipfs/go-log/v2"
|
2020-06-02 18:12:53 +00:00
|
|
|
"github.com/urfave/cli/v2"
|
2020-06-05 22:59:01 +00:00
|
|
|
"golang.org/x/xerrors"
|
2019-09-20 21:27:40 +00:00
|
|
|
|
2019-12-19 20:13:17 +00:00
|
|
|
"github.com/filecoin-project/go-address"
|
2022-06-14 15:00:51 +00:00
|
|
|
|
2021-04-05 18:12:47 +00:00
|
|
|
"github.com/filecoin-project/lotus/api/v0api"
|
2019-10-18 04:47:41 +00:00
|
|
|
"github.com/filecoin-project/lotus/build"
|
|
|
|
"github.com/filecoin-project/lotus/chain/types"
|
|
|
|
lcli "github.com/filecoin-project/lotus/cli"
|
2019-09-20 21:27:40 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var log = logging.Logger("main")
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
logging.SetLogLevel("*", "INFO")
|
|
|
|
|
|
|
|
log.Info("Starting fountain")
|
|
|
|
|
|
|
|
local := []*cli.Command{
|
|
|
|
runCmd,
|
|
|
|
}
|
|
|
|
|
|
|
|
app := &cli.App{
|
|
|
|
Name: "lotus-fountain",
|
|
|
|
Usage: "Devnet token distribution utility",
|
2020-06-01 18:43:51 +00:00
|
|
|
Version: build.UserVersion(),
|
2019-09-20 21:27:40 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "repo",
|
|
|
|
EnvVars: []string{"LOTUS_PATH"},
|
|
|
|
Value: "~/.lotus", // TODO: Consider XDG_DATA_HOME
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
Commands: local,
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := app.Run(os.Args); err != nil {
|
|
|
|
log.Warn(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var runCmd = &cli.Command{
|
|
|
|
Name: "run",
|
|
|
|
Usage: "Start lotus fountain",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "front",
|
|
|
|
Value: "127.0.0.1:7777",
|
|
|
|
},
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "from",
|
|
|
|
},
|
2020-07-12 21:03:05 +00:00
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "amount",
|
|
|
|
EnvVars: []string{"LOTUS_FOUNTAIN_AMOUNT"},
|
|
|
|
Value: "50",
|
|
|
|
},
|
2021-01-28 15:38:58 +00:00
|
|
|
&cli.Float64Flag{
|
|
|
|
Name: "captcha-threshold",
|
|
|
|
Value: 0.5,
|
|
|
|
},
|
2022-11-25 22:32:42 +00:00
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "http-server-timeout",
|
|
|
|
Value: "3s",
|
|
|
|
},
|
2019-09-20 21:27:40 +00:00
|
|
|
},
|
|
|
|
Action: func(cctx *cli.Context) error {
|
2020-07-12 21:03:05 +00:00
|
|
|
sendPerRequest, err := types.ParseFIL(cctx.String("amount"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-10-04 22:43:04 +00:00
|
|
|
nodeApi, closer, err := lcli.GetFullNodeAPI(cctx)
|
2019-09-20 21:27:40 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-10-04 16:02:25 +00:00
|
|
|
defer closer()
|
2019-09-20 21:27:40 +00:00
|
|
|
ctx := lcli.ReqContext(cctx)
|
|
|
|
|
|
|
|
v, err := nodeApi.Version(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-11-24 11:09:48 +00:00
|
|
|
log.Infof("Remote version: %s", v.Version)
|
2019-09-20 21:27:40 +00:00
|
|
|
|
|
|
|
from, err := address.NewFromString(cctx.String("from"))
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("parsing source address (provide correct --from flag!): %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
h := &handler{
|
2020-07-12 21:03:05 +00:00
|
|
|
ctx: ctx,
|
|
|
|
api: nodeApi,
|
|
|
|
from: from,
|
|
|
|
sendPerRequest: sendPerRequest,
|
2019-10-17 04:11:47 +00:00
|
|
|
limiter: NewLimiter(LimiterConfig{
|
2020-07-29 01:22:29 +00:00
|
|
|
TotalRate: 500 * time.Millisecond,
|
|
|
|
TotalBurst: build.BlockMessageLimit,
|
|
|
|
IPRate: 10 * time.Minute,
|
2019-10-17 04:11:47 +00:00
|
|
|
IPBurst: 5,
|
2019-10-17 14:28:03 +00:00
|
|
|
WalletRate: 15 * time.Minute,
|
2019-12-11 15:23:11 +00:00
|
|
|
WalletBurst: 2,
|
2019-10-17 04:11:47 +00:00
|
|
|
}),
|
2021-02-17 00:19:27 +00:00
|
|
|
recapThreshold: cctx.Float64("captcha-threshold"),
|
2019-09-20 21:27:40 +00:00
|
|
|
}
|
|
|
|
|
2021-01-28 15:38:58 +00:00
|
|
|
box := rice.MustFindBox("site")
|
|
|
|
http.Handle("/", http.FileServer(box.HTTPBox()))
|
|
|
|
http.HandleFunc("/funds.html", prepFundsHtml(box))
|
|
|
|
http.Handle("/send", h)
|
2019-09-20 21:27:40 +00:00
|
|
|
fmt.Printf("Open http://%s\n", cctx.String("front"))
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
<-ctx.Done()
|
|
|
|
os.Exit(0)
|
|
|
|
}()
|
|
|
|
|
2022-11-25 22:32:42 +00:00
|
|
|
timeout, err := time.ParseDuration(cctx.String("http-server-timeout"))
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("invalid time string %s: %x", cctx.String("http-server-timeout"), err)
|
|
|
|
}
|
|
|
|
|
2022-11-25 21:19:20 +00:00
|
|
|
server := &http.Server{
|
|
|
|
Addr: cctx.String("front"),
|
2022-11-25 22:32:42 +00:00
|
|
|
ReadHeaderTimeout: timeout,
|
2022-11-25 21:19:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return server.ListenAndServe()
|
2019-09-20 21:27:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-01-28 15:38:58 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-20 21:27:40 +00:00
|
|
|
type handler struct {
|
|
|
|
ctx context.Context
|
2021-04-05 18:12:47 +00:00
|
|
|
api v0api.FullNode
|
2019-09-20 21:27:40 +00:00
|
|
|
|
2020-07-12 21:03:05 +00:00
|
|
|
from address.Address
|
|
|
|
sendPerRequest types.FIL
|
2019-10-17 04:11:47 +00:00
|
|
|
|
2021-02-17 00:19:27 +00:00
|
|
|
limiter *Limiter
|
|
|
|
recapThreshold float64
|
2019-09-20 21:27:40 +00:00
|
|
|
}
|
|
|
|
|
2021-01-28 15:38:58 +00:00
|
|
|
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
|
|
|
|
}
|
2021-02-17 00:19:27 +00:00
|
|
|
if !capResp.Success || capResp.Score < h.recapThreshold {
|
2021-01-28 15:38:58 +00:00
|
|
|
log.Infow("spam", "capResp", capResp)
|
|
|
|
http.Error(w, "spam protection", http.StatusUnprocessableEntity)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-10-31 17:24:59 +00:00
|
|
|
to, err := address.NewFromString(r.FormValue("address"))
|
|
|
|
if err != nil {
|
2020-08-20 04:49:10 +00:00
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
2019-10-17 04:11:47 +00:00
|
|
|
return
|
|
|
|
}
|
2021-01-28 15:38:58 +00:00
|
|
|
if to == address.Undef {
|
|
|
|
http.Error(w, "empty address", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
2019-10-17 04:11:47 +00:00
|
|
|
|
2019-10-31 17:24:59 +00:00
|
|
|
// Limit based on wallet address
|
|
|
|
limiter := h.limiter.GetWalletLimiter(to.String())
|
2019-10-17 04:11:47 +00:00
|
|
|
if !limiter.Allow() {
|
2019-12-11 15:23:11 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusTooManyRequests)+": wallet limit", http.StatusTooManyRequests)
|
2019-10-17 04:11:47 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-10-31 17:24:59 +00:00
|
|
|
// Limit based on IP
|
2019-11-06 15:10:27 +00:00
|
|
|
if i := net.ParseIP(reqIP); i != nil && i.IsLoopback() {
|
|
|
|
log.Errorf("rate limiting localhost: %s", reqIP)
|
|
|
|
}
|
|
|
|
|
|
|
|
limiter = h.limiter.GetIPLimiter(reqIP)
|
2019-10-31 17:24:59 +00:00
|
|
|
if !limiter.Allow() {
|
2019-12-11 15:23:11 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusTooManyRequests)+": IP limit", http.StatusTooManyRequests)
|
2019-09-20 21:27:40 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-10-31 17:24:59 +00:00
|
|
|
// General limiter to allow throttling all messages that can make it into the mpool
|
|
|
|
if !h.limiter.Allow() {
|
2019-12-11 15:23:11 +00:00
|
|
|
http.Error(w, http.StatusText(http.StatusTooManyRequests)+": global limit", http.StatusTooManyRequests)
|
2019-10-17 04:11:47 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-09-20 21:27:40 +00:00
|
|
|
smsg, err := h.api.MpoolPushMessage(h.ctx, &types.Message{
|
2020-07-12 21:03:05 +00:00
|
|
|
Value: types.BigInt(h.sendPerRequest),
|
2019-09-20 21:27:40 +00:00
|
|
|
From: h.from,
|
|
|
|
To: to,
|
2020-08-12 20:17:21 +00:00
|
|
|
}, nil)
|
2019-09-20 21:27:40 +00:00
|
|
|
if err != nil {
|
2020-08-20 04:49:10 +00:00
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
2019-09-20 21:27:40 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-05-27 20:53:20 +00:00
|
|
|
_, _ = w.Write([]byte(smsg.Cid().String()))
|
2019-09-20 21:27:40 +00:00
|
|
|
}
|