github.com/jimmyx0x/go-ethereum@v1.10.28/cmd/faucet/faucet.go (about) 1 // Copyright 2017 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // go-ethereum is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 // faucet is an Ether faucet backed by a light client. 18 package main 19 20 import ( 21 "bytes" 22 "context" 23 _ "embed" 24 "encoding/json" 25 "errors" 26 "flag" 27 "fmt" 28 "html/template" 29 "io" 30 "math" 31 "math/big" 32 "net/http" 33 "net/url" 34 "os" 35 "path/filepath" 36 "regexp" 37 "strconv" 38 "strings" 39 "sync" 40 "time" 41 42 "github.com/ethereum/go-ethereum/accounts" 43 "github.com/ethereum/go-ethereum/accounts/keystore" 44 "github.com/ethereum/go-ethereum/cmd/utils" 45 "github.com/ethereum/go-ethereum/common" 46 "github.com/ethereum/go-ethereum/core" 47 "github.com/ethereum/go-ethereum/core/types" 48 "github.com/ethereum/go-ethereum/eth/downloader" 49 "github.com/ethereum/go-ethereum/eth/ethconfig" 50 "github.com/ethereum/go-ethereum/ethclient" 51 "github.com/ethereum/go-ethereum/ethstats" 52 "github.com/ethereum/go-ethereum/internal/version" 53 "github.com/ethereum/go-ethereum/les" 54 "github.com/ethereum/go-ethereum/log" 55 "github.com/ethereum/go-ethereum/node" 56 "github.com/ethereum/go-ethereum/p2p" 57 "github.com/ethereum/go-ethereum/p2p/enode" 58 "github.com/ethereum/go-ethereum/p2p/nat" 59 "github.com/ethereum/go-ethereum/params" 60 "github.com/gorilla/websocket" 61 ) 62 63 var ( 64 genesisFlag = flag.String("genesis", "", "Genesis json file to seed the chain with") 65 apiPortFlag = flag.Int("apiport", 8080, "Listener port for the HTTP API connection") 66 ethPortFlag = flag.Int("ethport", 30303, "Listener port for the devp2p connection") 67 bootFlag = flag.String("bootnodes", "", "Comma separated bootnode enode URLs to seed with") 68 netFlag = flag.Uint64("network", 0, "Network ID to use for the Ethereum protocol") 69 statsFlag = flag.String("ethstats", "", "Ethstats network monitoring auth string") 70 71 netnameFlag = flag.String("faucet.name", "", "Network name to assign to the faucet") 72 payoutFlag = flag.Int("faucet.amount", 1, "Number of Ethers to pay out per user request") 73 minutesFlag = flag.Int("faucet.minutes", 1440, "Number of minutes to wait between funding rounds") 74 tiersFlag = flag.Int("faucet.tiers", 3, "Number of funding tiers to enable (x3 time, x2.5 funds)") 75 76 accJSONFlag = flag.String("account.json", "", "Key json file to fund user requests with") 77 accPassFlag = flag.String("account.pass", "", "Decryption password to access faucet funds") 78 79 captchaToken = flag.String("captcha.token", "", "Recaptcha site key to authenticate client side") 80 captchaSecret = flag.String("captcha.secret", "", "Recaptcha secret key to authenticate server side") 81 82 noauthFlag = flag.Bool("noauth", false, "Enables funding requests without authentication") 83 logFlag = flag.Int("loglevel", 3, "Log level to use for Ethereum and the faucet") 84 85 twitterTokenFlag = flag.String("twitter.token", "", "Bearer token to authenticate with the v2 Twitter API") 86 twitterTokenV1Flag = flag.String("twitter.token.v1", "", "Bearer token to authenticate with the v1.1 Twitter API") 87 88 goerliFlag = flag.Bool("goerli", false, "Initializes the faucet with Görli network config") 89 rinkebyFlag = flag.Bool("rinkeby", false, "Initializes the faucet with Rinkeby network config") 90 sepoliaFlag = flag.Bool("sepolia", false, "Initializes the faucet with Sepolia network config") 91 ) 92 93 var ( 94 ether = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) 95 ) 96 97 //go:embed faucet.html 98 var websiteTmpl string 99 100 func main() { 101 // Parse the flags and set up the logger to print everything requested 102 flag.Parse() 103 log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*logFlag), log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) 104 105 // Construct the payout tiers 106 amounts := make([]string, *tiersFlag) 107 periods := make([]string, *tiersFlag) 108 for i := 0; i < *tiersFlag; i++ { 109 // Calculate the amount for the next tier and format it 110 amount := float64(*payoutFlag) * math.Pow(2.5, float64(i)) 111 amounts[i] = fmt.Sprintf("%s Ethers", strconv.FormatFloat(amount, 'f', -1, 64)) 112 if amount == 1 { 113 amounts[i] = strings.TrimSuffix(amounts[i], "s") 114 } 115 // Calculate the period for the next tier and format it 116 period := *minutesFlag * int(math.Pow(3, float64(i))) 117 periods[i] = fmt.Sprintf("%d mins", period) 118 if period%60 == 0 { 119 period /= 60 120 periods[i] = fmt.Sprintf("%d hours", period) 121 122 if period%24 == 0 { 123 period /= 24 124 periods[i] = fmt.Sprintf("%d days", period) 125 } 126 } 127 if period == 1 { 128 periods[i] = strings.TrimSuffix(periods[i], "s") 129 } 130 } 131 website := new(bytes.Buffer) 132 err := template.Must(template.New("").Parse(websiteTmpl)).Execute(website, map[string]interface{}{ 133 "Network": *netnameFlag, 134 "Amounts": amounts, 135 "Periods": periods, 136 "Recaptcha": *captchaToken, 137 "NoAuth": *noauthFlag, 138 }) 139 if err != nil { 140 log.Crit("Failed to render the faucet template", "err", err) 141 } 142 // Load and parse the genesis block requested by the user 143 genesis, err := getGenesis(*genesisFlag, *goerliFlag, *rinkebyFlag, *sepoliaFlag) 144 if err != nil { 145 log.Crit("Failed to parse genesis config", "err", err) 146 } 147 // Convert the bootnodes to internal enode representations 148 var enodes []*enode.Node 149 for _, boot := range strings.Split(*bootFlag, ",") { 150 if url, err := enode.Parse(enode.ValidSchemes, boot); err == nil { 151 enodes = append(enodes, url) 152 } else { 153 log.Error("Failed to parse bootnode URL", "url", boot, "err", err) 154 } 155 } 156 // Load up the account key and decrypt its password 157 blob, err := os.ReadFile(*accPassFlag) 158 if err != nil { 159 log.Crit("Failed to read account password contents", "file", *accPassFlag, "err", err) 160 } 161 pass := strings.TrimSuffix(string(blob), "\n") 162 163 ks := keystore.NewKeyStore(filepath.Join(os.Getenv("HOME"), ".faucet", "keys"), keystore.StandardScryptN, keystore.StandardScryptP) 164 if blob, err = os.ReadFile(*accJSONFlag); err != nil { 165 log.Crit("Failed to read account key contents", "file", *accJSONFlag, "err", err) 166 } 167 acc, err := ks.Import(blob, pass, pass) 168 if err != nil && err != keystore.ErrAccountAlreadyExists { 169 log.Crit("Failed to import faucet signer account", "err", err) 170 } 171 if err := ks.Unlock(acc, pass); err != nil { 172 log.Crit("Failed to unlock faucet signer account", "err", err) 173 } 174 // Assemble and start the faucet light service 175 faucet, err := newFaucet(genesis, *ethPortFlag, enodes, *netFlag, *statsFlag, ks, website.Bytes()) 176 if err != nil { 177 log.Crit("Failed to start faucet", "err", err) 178 } 179 defer faucet.close() 180 181 if err := faucet.listenAndServe(*apiPortFlag); err != nil { 182 log.Crit("Failed to launch faucet API", "err", err) 183 } 184 } 185 186 // request represents an accepted funding request. 187 type request struct { 188 Avatar string `json:"avatar"` // Avatar URL to make the UI nicer 189 Account common.Address `json:"account"` // Ethereum address being funded 190 Time time.Time `json:"time"` // Timestamp when the request was accepted 191 Tx *types.Transaction `json:"tx"` // Transaction funding the account 192 } 193 194 // faucet represents a crypto faucet backed by an Ethereum light client. 195 type faucet struct { 196 config *params.ChainConfig // Chain configurations for signing 197 stack *node.Node // Ethereum protocol stack 198 client *ethclient.Client // Client connection to the Ethereum chain 199 index []byte // Index page to serve up on the web 200 201 keystore *keystore.KeyStore // Keystore containing the single signer 202 account accounts.Account // Account funding user faucet requests 203 head *types.Header // Current head header of the faucet 204 balance *big.Int // Current balance of the faucet 205 nonce uint64 // Current pending nonce of the faucet 206 price *big.Int // Current gas price to issue funds with 207 208 conns []*wsConn // Currently live websocket connections 209 timeouts map[string]time.Time // History of users and their funding timeouts 210 reqs []*request // Currently pending funding requests 211 update chan struct{} // Channel to signal request updates 212 213 lock sync.RWMutex // Lock protecting the faucet's internals 214 } 215 216 // wsConn wraps a websocket connection with a write mutex as the underlying 217 // websocket library does not synchronize access to the stream. 218 type wsConn struct { 219 conn *websocket.Conn 220 wlock sync.Mutex 221 } 222 223 func newFaucet(genesis *core.Genesis, port int, enodes []*enode.Node, network uint64, stats string, ks *keystore.KeyStore, index []byte) (*faucet, error) { 224 // Assemble the raw devp2p protocol stack 225 git, _ := version.VCS() 226 stack, err := node.New(&node.Config{ 227 Name: "geth", 228 Version: params.VersionWithCommit(git.Commit, git.Date), 229 DataDir: filepath.Join(os.Getenv("HOME"), ".faucet"), 230 P2P: p2p.Config{ 231 NAT: nat.Any(), 232 NoDiscovery: true, 233 DiscoveryV5: true, 234 ListenAddr: fmt.Sprintf(":%d", port), 235 MaxPeers: 25, 236 BootstrapNodesV5: enodes, 237 }, 238 }) 239 if err != nil { 240 return nil, err 241 } 242 243 // Assemble the Ethereum light client protocol 244 cfg := ethconfig.Defaults 245 cfg.SyncMode = downloader.LightSync 246 cfg.NetworkId = network 247 cfg.Genesis = genesis 248 utils.SetDNSDiscoveryDefaults(&cfg, genesis.ToBlock().Hash()) 249 250 lesBackend, err := les.New(stack, &cfg) 251 if err != nil { 252 return nil, fmt.Errorf("Failed to register the Ethereum service: %w", err) 253 } 254 255 // Assemble the ethstats monitoring and reporting service' 256 if stats != "" { 257 if err := ethstats.New(stack, lesBackend.ApiBackend, lesBackend.Engine(), stats); err != nil { 258 return nil, err 259 } 260 } 261 // Boot up the client and ensure it connects to bootnodes 262 if err := stack.Start(); err != nil { 263 return nil, err 264 } 265 for _, boot := range enodes { 266 old, err := enode.Parse(enode.ValidSchemes, boot.String()) 267 if err == nil { 268 stack.Server().AddPeer(old) 269 } 270 } 271 // Attach to the client and retrieve and interesting metadatas 272 api, err := stack.Attach() 273 if err != nil { 274 stack.Close() 275 return nil, err 276 } 277 client := ethclient.NewClient(api) 278 279 return &faucet{ 280 config: genesis.Config, 281 stack: stack, 282 client: client, 283 index: index, 284 keystore: ks, 285 account: ks.Accounts()[0], 286 timeouts: make(map[string]time.Time), 287 update: make(chan struct{}, 1), 288 }, nil 289 } 290 291 // close terminates the Ethereum connection and tears down the faucet. 292 func (f *faucet) close() error { 293 return f.stack.Close() 294 } 295 296 // listenAndServe registers the HTTP handlers for the faucet and boots it up 297 // for service user funding requests. 298 func (f *faucet) listenAndServe(port int) error { 299 go f.loop() 300 301 http.HandleFunc("/", f.webHandler) 302 http.HandleFunc("/api", f.apiHandler) 303 return http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 304 } 305 306 // webHandler handles all non-api requests, simply flattening and returning the 307 // faucet website. 308 func (f *faucet) webHandler(w http.ResponseWriter, r *http.Request) { 309 w.Write(f.index) 310 } 311 312 // apiHandler handles requests for Ether grants and transaction statuses. 313 func (f *faucet) apiHandler(w http.ResponseWriter, r *http.Request) { 314 upgrader := websocket.Upgrader{} 315 conn, err := upgrader.Upgrade(w, r, nil) 316 if err != nil { 317 return 318 } 319 320 // Start tracking the connection and drop at the end 321 defer conn.Close() 322 323 f.lock.Lock() 324 wsconn := &wsConn{conn: conn} 325 f.conns = append(f.conns, wsconn) 326 f.lock.Unlock() 327 328 defer func() { 329 f.lock.Lock() 330 for i, c := range f.conns { 331 if c.conn == conn { 332 f.conns = append(f.conns[:i], f.conns[i+1:]...) 333 break 334 } 335 } 336 f.lock.Unlock() 337 }() 338 // Gather the initial stats from the network to report 339 var ( 340 head *types.Header 341 balance *big.Int 342 nonce uint64 343 ) 344 for head == nil || balance == nil { 345 // Retrieve the current stats cached by the faucet 346 f.lock.RLock() 347 if f.head != nil { 348 head = types.CopyHeader(f.head) 349 } 350 if f.balance != nil { 351 balance = new(big.Int).Set(f.balance) 352 } 353 nonce = f.nonce 354 f.lock.RUnlock() 355 356 if head == nil || balance == nil { 357 // Report the faucet offline until initial stats are ready 358 //lint:ignore ST1005 This error is to be displayed in the browser 359 if err = sendError(wsconn, errors.New("Faucet offline")); err != nil { 360 log.Warn("Failed to send faucet error to client", "err", err) 361 return 362 } 363 time.Sleep(3 * time.Second) 364 } 365 } 366 // Send over the initial stats and the latest header 367 f.lock.RLock() 368 reqs := f.reqs 369 f.lock.RUnlock() 370 if err = send(wsconn, map[string]interface{}{ 371 "funds": new(big.Int).Div(balance, ether), 372 "funded": nonce, 373 "peers": f.stack.Server().PeerCount(), 374 "requests": reqs, 375 }, 3*time.Second); err != nil { 376 log.Warn("Failed to send initial stats to client", "err", err) 377 return 378 } 379 if err = send(wsconn, head, 3*time.Second); err != nil { 380 log.Warn("Failed to send initial header to client", "err", err) 381 return 382 } 383 // Keep reading requests from the websocket until the connection breaks 384 for { 385 // Fetch the next funding request and validate against github 386 var msg struct { 387 URL string `json:"url"` 388 Tier uint `json:"tier"` 389 Captcha string `json:"captcha"` 390 } 391 if err = conn.ReadJSON(&msg); err != nil { 392 return 393 } 394 if !*noauthFlag && !strings.HasPrefix(msg.URL, "https://twitter.com/") && !strings.HasPrefix(msg.URL, "https://www.facebook.com/") { 395 if err = sendError(wsconn, errors.New("URL doesn't link to supported services")); err != nil { 396 log.Warn("Failed to send URL error to client", "err", err) 397 return 398 } 399 continue 400 } 401 if msg.Tier >= uint(*tiersFlag) { 402 //lint:ignore ST1005 This error is to be displayed in the browser 403 if err = sendError(wsconn, errors.New("Invalid funding tier requested")); err != nil { 404 log.Warn("Failed to send tier error to client", "err", err) 405 return 406 } 407 continue 408 } 409 log.Info("Faucet funds requested", "url", msg.URL, "tier", msg.Tier) 410 411 // If captcha verifications are enabled, make sure we're not dealing with a robot 412 if *captchaToken != "" { 413 form := url.Values{} 414 form.Add("secret", *captchaSecret) 415 form.Add("response", msg.Captcha) 416 417 res, err := http.PostForm("https://www.google.com/recaptcha/api/siteverify", form) 418 if err != nil { 419 if err = sendError(wsconn, err); err != nil { 420 log.Warn("Failed to send captcha post error to client", "err", err) 421 return 422 } 423 continue 424 } 425 var result struct { 426 Success bool `json:"success"` 427 Errors json.RawMessage `json:"error-codes"` 428 } 429 err = json.NewDecoder(res.Body).Decode(&result) 430 res.Body.Close() 431 if err != nil { 432 if err = sendError(wsconn, err); err != nil { 433 log.Warn("Failed to send captcha decode error to client", "err", err) 434 return 435 } 436 continue 437 } 438 if !result.Success { 439 log.Warn("Captcha verification failed", "err", string(result.Errors)) 440 //lint:ignore ST1005 it's funny and the robot won't mind 441 if err = sendError(wsconn, errors.New("Beep-bop, you're a robot!")); err != nil { 442 log.Warn("Failed to send captcha failure to client", "err", err) 443 return 444 } 445 continue 446 } 447 } 448 // Retrieve the Ethereum address to fund, the requesting user and a profile picture 449 var ( 450 id string 451 username string 452 avatar string 453 address common.Address 454 ) 455 switch { 456 case strings.HasPrefix(msg.URL, "https://twitter.com/"): 457 id, username, avatar, address, err = authTwitter(msg.URL, *twitterTokenV1Flag, *twitterTokenFlag) 458 case strings.HasPrefix(msg.URL, "https://www.facebook.com/"): 459 username, avatar, address, err = authFacebook(msg.URL) 460 id = username 461 case *noauthFlag: 462 username, avatar, address, err = authNoAuth(msg.URL) 463 id = username 464 default: 465 //lint:ignore ST1005 This error is to be displayed in the browser 466 err = errors.New("Something funky happened, please open an issue at https://github.com/ethereum/go-ethereum/issues") 467 } 468 if err != nil { 469 if err = sendError(wsconn, err); err != nil { 470 log.Warn("Failed to send prefix error to client", "err", err) 471 return 472 } 473 continue 474 } 475 log.Info("Faucet request valid", "url", msg.URL, "tier", msg.Tier, "user", username, "address", address) 476 477 // Ensure the user didn't request funds too recently 478 f.lock.Lock() 479 var ( 480 fund bool 481 timeout time.Time 482 ) 483 if timeout = f.timeouts[id]; time.Now().After(timeout) { 484 // User wasn't funded recently, create the funding transaction 485 amount := new(big.Int).Mul(big.NewInt(int64(*payoutFlag)), ether) 486 amount = new(big.Int).Mul(amount, new(big.Int).Exp(big.NewInt(5), big.NewInt(int64(msg.Tier)), nil)) 487 amount = new(big.Int).Div(amount, new(big.Int).Exp(big.NewInt(2), big.NewInt(int64(msg.Tier)), nil)) 488 489 tx := types.NewTransaction(f.nonce+uint64(len(f.reqs)), address, amount, 21000, f.price, nil) 490 signed, err := f.keystore.SignTx(f.account, tx, f.config.ChainID) 491 if err != nil { 492 f.lock.Unlock() 493 if err = sendError(wsconn, err); err != nil { 494 log.Warn("Failed to send transaction creation error to client", "err", err) 495 return 496 } 497 continue 498 } 499 // Submit the transaction and mark as funded if successful 500 if err := f.client.SendTransaction(context.Background(), signed); err != nil { 501 f.lock.Unlock() 502 if err = sendError(wsconn, err); err != nil { 503 log.Warn("Failed to send transaction transmission error to client", "err", err) 504 return 505 } 506 continue 507 } 508 f.reqs = append(f.reqs, &request{ 509 Avatar: avatar, 510 Account: address, 511 Time: time.Now(), 512 Tx: signed, 513 }) 514 timeout := time.Duration(*minutesFlag*int(math.Pow(3, float64(msg.Tier)))) * time.Minute 515 grace := timeout / 288 // 24h timeout => 5m grace 516 517 f.timeouts[id] = time.Now().Add(timeout - grace) 518 fund = true 519 } 520 f.lock.Unlock() 521 522 // Send an error if too frequent funding, othewise a success 523 if !fund { 524 if err = sendError(wsconn, fmt.Errorf("%s left until next allowance", common.PrettyDuration(time.Until(timeout)))); err != nil { // nolint: gosimple 525 log.Warn("Failed to send funding error to client", "err", err) 526 return 527 } 528 continue 529 } 530 if err = sendSuccess(wsconn, fmt.Sprintf("Funding request accepted for %s into %s", username, address.Hex())); err != nil { 531 log.Warn("Failed to send funding success to client", "err", err) 532 return 533 } 534 select { 535 case f.update <- struct{}{}: 536 default: 537 } 538 } 539 } 540 541 // refresh attempts to retrieve the latest header from the chain and extract the 542 // associated faucet balance and nonce for connectivity caching. 543 func (f *faucet) refresh(head *types.Header) error { 544 // Ensure a state update does not run for too long 545 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 546 defer cancel() 547 548 // If no header was specified, use the current chain head 549 var err error 550 if head == nil { 551 if head, err = f.client.HeaderByNumber(ctx, nil); err != nil { 552 return err 553 } 554 } 555 // Retrieve the balance, nonce and gas price from the current head 556 var ( 557 balance *big.Int 558 nonce uint64 559 price *big.Int 560 ) 561 if balance, err = f.client.BalanceAt(ctx, f.account.Address, head.Number); err != nil { 562 return err 563 } 564 if nonce, err = f.client.NonceAt(ctx, f.account.Address, head.Number); err != nil { 565 return err 566 } 567 if price, err = f.client.SuggestGasPrice(ctx); err != nil { 568 return err 569 } 570 // Everything succeeded, update the cached stats and eject old requests 571 f.lock.Lock() 572 f.head, f.balance = head, balance 573 f.price, f.nonce = price, nonce 574 for len(f.reqs) > 0 && f.reqs[0].Tx.Nonce() < f.nonce { 575 f.reqs = f.reqs[1:] 576 } 577 f.lock.Unlock() 578 579 return nil 580 } 581 582 // loop keeps waiting for interesting events and pushes them out to connected 583 // websockets. 584 func (f *faucet) loop() { 585 // Wait for chain events and push them to clients 586 heads := make(chan *types.Header, 16) 587 sub, err := f.client.SubscribeNewHead(context.Background(), heads) 588 if err != nil { 589 log.Crit("Failed to subscribe to head events", "err", err) 590 } 591 defer sub.Unsubscribe() 592 593 // Start a goroutine to update the state from head notifications in the background 594 update := make(chan *types.Header) 595 596 go func() { 597 for head := range update { 598 // New chain head arrived, query the current stats and stream to clients 599 timestamp := time.Unix(int64(head.Time), 0) 600 if time.Since(timestamp) > time.Hour { 601 log.Warn("Skipping faucet refresh, head too old", "number", head.Number, "hash", head.Hash(), "age", common.PrettyAge(timestamp)) 602 continue 603 } 604 if err := f.refresh(head); err != nil { 605 log.Warn("Failed to update faucet state", "block", head.Number, "hash", head.Hash(), "err", err) 606 continue 607 } 608 // Faucet state retrieved, update locally and send to clients 609 f.lock.RLock() 610 log.Info("Updated faucet state", "number", head.Number, "hash", head.Hash(), "age", common.PrettyAge(timestamp), "balance", f.balance, "nonce", f.nonce, "price", f.price) 611 612 balance := new(big.Int).Div(f.balance, ether) 613 peers := f.stack.Server().PeerCount() 614 615 for _, conn := range f.conns { 616 if err := send(conn, map[string]interface{}{ 617 "funds": balance, 618 "funded": f.nonce, 619 "peers": peers, 620 "requests": f.reqs, 621 }, time.Second); err != nil { 622 log.Warn("Failed to send stats to client", "err", err) 623 conn.conn.Close() 624 continue 625 } 626 if err := send(conn, head, time.Second); err != nil { 627 log.Warn("Failed to send header to client", "err", err) 628 conn.conn.Close() 629 } 630 } 631 f.lock.RUnlock() 632 } 633 }() 634 // Wait for various events and assing to the appropriate background threads 635 for { 636 select { 637 case head := <-heads: 638 // New head arrived, send if for state update if there's none running 639 select { 640 case update <- head: 641 default: 642 } 643 644 case <-f.update: 645 // Pending requests updated, stream to clients 646 f.lock.RLock() 647 for _, conn := range f.conns { 648 if err := send(conn, map[string]interface{}{"requests": f.reqs}, time.Second); err != nil { 649 log.Warn("Failed to send requests to client", "err", err) 650 conn.conn.Close() 651 } 652 } 653 f.lock.RUnlock() 654 } 655 } 656 } 657 658 // sends transmits a data packet to the remote end of the websocket, but also 659 // setting a write deadline to prevent waiting forever on the node. 660 func send(conn *wsConn, value interface{}, timeout time.Duration) error { 661 if timeout == 0 { 662 timeout = 60 * time.Second 663 } 664 conn.wlock.Lock() 665 defer conn.wlock.Unlock() 666 conn.conn.SetWriteDeadline(time.Now().Add(timeout)) 667 return conn.conn.WriteJSON(value) 668 } 669 670 // sendError transmits an error to the remote end of the websocket, also setting 671 // the write deadline to 1 second to prevent waiting forever. 672 func sendError(conn *wsConn, err error) error { 673 return send(conn, map[string]string{"error": err.Error()}, time.Second) 674 } 675 676 // sendSuccess transmits a success message to the remote end of the websocket, also 677 // setting the write deadline to 1 second to prevent waiting forever. 678 func sendSuccess(conn *wsConn, msg string) error { 679 return send(conn, map[string]string{"success": msg}, time.Second) 680 } 681 682 // authTwitter tries to authenticate a faucet request using Twitter posts, returning 683 // the uniqueness identifier (user id/username), username, avatar URL and Ethereum address to fund on success. 684 func authTwitter(url string, tokenV1, tokenV2 string) (string, string, string, common.Address, error) { 685 // Ensure the user specified a meaningful URL, no fancy nonsense 686 parts := strings.Split(url, "/") 687 if len(parts) < 4 || parts[len(parts)-2] != "status" { 688 //lint:ignore ST1005 This error is to be displayed in the browser 689 return "", "", "", common.Address{}, errors.New("Invalid Twitter status URL") 690 } 691 // Strip any query parameters from the tweet id and ensure it's numeric 692 tweetID := strings.Split(parts[len(parts)-1], "?")[0] 693 if !regexp.MustCompile("^[0-9]+$").MatchString(tweetID) { 694 return "", "", "", common.Address{}, errors.New("Invalid Tweet URL") 695 } 696 // Twitter's API isn't really friendly with direct links. 697 // It is restricted to 300 queries / 15 minute with an app api key. 698 // Anything more will require read only authorization from the users and that we want to avoid. 699 700 // If Twitter bearer token is provided, use the API, selecting the version 701 // the user would prefer (currently there's a limit of 1 v2 app / developer 702 // but unlimited v1.1 apps). 703 switch { 704 case tokenV1 != "": 705 return authTwitterWithTokenV1(tweetID, tokenV1) 706 case tokenV2 != "": 707 return authTwitterWithTokenV2(tweetID, tokenV2) 708 } 709 // Twitter API token isn't provided so we just load the public posts 710 // and scrape it for the Ethereum address and profile URL. We need to load 711 // the mobile page though since the main page loads tweet contents via JS. 712 url = strings.Replace(url, "https://twitter.com/", "https://mobile.twitter.com/", 1) 713 714 res, err := http.Get(url) 715 if err != nil { 716 return "", "", "", common.Address{}, err 717 } 718 defer res.Body.Close() 719 720 // Resolve the username from the final redirect, no intermediate junk 721 parts = strings.Split(res.Request.URL.String(), "/") 722 if len(parts) < 4 || parts[len(parts)-2] != "status" { 723 //lint:ignore ST1005 This error is to be displayed in the browser 724 return "", "", "", common.Address{}, errors.New("Invalid Twitter status URL") 725 } 726 username := parts[len(parts)-3] 727 728 body, err := io.ReadAll(res.Body) 729 if err != nil { 730 return "", "", "", common.Address{}, err 731 } 732 address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body))) 733 if address == (common.Address{}) { 734 //lint:ignore ST1005 This error is to be displayed in the browser 735 return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund") 736 } 737 var avatar string 738 if parts = regexp.MustCompile(`src="([^"]+twimg\.com/profile_images[^"]+)"`).FindStringSubmatch(string(body)); len(parts) == 2 { 739 avatar = parts[1] 740 } 741 return username + "@twitter", username, avatar, address, nil 742 } 743 744 // authTwitterWithTokenV1 tries to authenticate a faucet request using Twitter's v1 745 // API, returning the user id, username, avatar URL and Ethereum address to fund on 746 // success. 747 func authTwitterWithTokenV1(tweetID string, token string) (string, string, string, common.Address, error) { 748 // Query the tweet details from Twitter 749 url := fmt.Sprintf("https://api.twitter.com/1.1/statuses/show.json?id=%s", tweetID) 750 req, err := http.NewRequest("GET", url, nil) 751 if err != nil { 752 return "", "", "", common.Address{}, err 753 } 754 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 755 res, err := http.DefaultClient.Do(req) 756 if err != nil { 757 return "", "", "", common.Address{}, err 758 } 759 defer res.Body.Close() 760 761 var result struct { 762 Text string `json:"text"` 763 User struct { 764 ID string `json:"id_str"` 765 Username string `json:"screen_name"` 766 Avatar string `json:"profile_image_url"` 767 } `json:"user"` 768 } 769 err = json.NewDecoder(res.Body).Decode(&result) 770 if err != nil { 771 return "", "", "", common.Address{}, err 772 } 773 address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(result.Text)) 774 if address == (common.Address{}) { 775 //lint:ignore ST1005 This error is to be displayed in the browser 776 return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund") 777 } 778 return result.User.ID + "@twitter", result.User.Username, result.User.Avatar, address, nil 779 } 780 781 // authTwitterWithTokenV2 tries to authenticate a faucet request using Twitter's v2 782 // API, returning the user id, username, avatar URL and Ethereum address to fund on 783 // success. 784 func authTwitterWithTokenV2(tweetID string, token string) (string, string, string, common.Address, error) { 785 // Query the tweet details from Twitter 786 url := fmt.Sprintf("https://api.twitter.com/2/tweets/%s?expansions=author_id&user.fields=profile_image_url", tweetID) 787 req, err := http.NewRequest("GET", url, nil) 788 if err != nil { 789 return "", "", "", common.Address{}, err 790 } 791 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 792 res, err := http.DefaultClient.Do(req) 793 if err != nil { 794 return "", "", "", common.Address{}, err 795 } 796 defer res.Body.Close() 797 798 var result struct { 799 Data struct { 800 AuthorID string `json:"author_id"` 801 Text string `json:"text"` 802 } `json:"data"` 803 Includes struct { 804 Users []struct { 805 ID string `json:"id"` 806 Username string `json:"username"` 807 Avatar string `json:"profile_image_url"` 808 } `json:"users"` 809 } `json:"includes"` 810 } 811 812 err = json.NewDecoder(res.Body).Decode(&result) 813 if err != nil { 814 return "", "", "", common.Address{}, err 815 } 816 817 address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(result.Data.Text)) 818 if address == (common.Address{}) { 819 //lint:ignore ST1005 This error is to be displayed in the browser 820 return "", "", "", common.Address{}, errors.New("No Ethereum address found to fund") 821 } 822 return result.Data.AuthorID + "@twitter", result.Includes.Users[0].Username, result.Includes.Users[0].Avatar, address, nil 823 } 824 825 // authFacebook tries to authenticate a faucet request using Facebook posts, 826 // returning the username, avatar URL and Ethereum address to fund on success. 827 func authFacebook(url string) (string, string, common.Address, error) { 828 // Ensure the user specified a meaningful URL, no fancy nonsense 829 parts := strings.Split(strings.Split(url, "?")[0], "/") 830 if parts[len(parts)-1] == "" { 831 parts = parts[0 : len(parts)-1] 832 } 833 if len(parts) < 4 || parts[len(parts)-2] != "posts" { 834 //lint:ignore ST1005 This error is to be displayed in the browser 835 return "", "", common.Address{}, errors.New("Invalid Facebook post URL") 836 } 837 username := parts[len(parts)-3] 838 839 // Facebook's Graph API isn't really friendly with direct links. Still, we don't 840 // want to do ask read permissions from users, so just load the public posts and 841 // scrape it for the Ethereum address and profile URL. 842 // 843 // Facebook recently changed their desktop webpage to use AJAX for loading post 844 // content, so switch over to the mobile site for now. Will probably end up having 845 // to use the API eventually. 846 crawl := strings.Replace(url, "www.facebook.com", "m.facebook.com", 1) 847 848 res, err := http.Get(crawl) 849 if err != nil { 850 return "", "", common.Address{}, err 851 } 852 defer res.Body.Close() 853 854 body, err := io.ReadAll(res.Body) 855 if err != nil { 856 return "", "", common.Address{}, err 857 } 858 address := common.HexToAddress(string(regexp.MustCompile("0x[0-9a-fA-F]{40}").Find(body))) 859 if address == (common.Address{}) { 860 //lint:ignore ST1005 This error is to be displayed in the browser 861 return "", "", common.Address{}, errors.New("No Ethereum address found to fund. Please check the post URL and verify that it can be viewed publicly.") 862 } 863 var avatar string 864 if parts = regexp.MustCompile(`src="([^"]+fbcdn\.net[^"]+)"`).FindStringSubmatch(string(body)); len(parts) == 2 { 865 avatar = parts[1] 866 } 867 return username + "@facebook", avatar, address, nil 868 } 869 870 // authNoAuth tries to interpret a faucet request as a plain Ethereum address, 871 // without actually performing any remote authentication. This mode is prone to 872 // Byzantine attack, so only ever use for truly private networks. 873 func authNoAuth(url string) (string, string, common.Address, error) { 874 address := common.HexToAddress(regexp.MustCompile("0x[0-9a-fA-F]{40}").FindString(url)) 875 if address == (common.Address{}) { 876 //lint:ignore ST1005 This error is to be displayed in the browser 877 return "", "", common.Address{}, errors.New("No Ethereum address found to fund") 878 } 879 return address.Hex() + "@noauth", "", address, nil 880 } 881 882 // getGenesis returns a genesis based on input args 883 func getGenesis(genesisFlag string, goerliFlag bool, rinkebyFlag bool, sepoliaFlag bool) (*core.Genesis, error) { 884 switch { 885 case genesisFlag != "": 886 var genesis core.Genesis 887 err := common.LoadJSON(genesisFlag, &genesis) 888 return &genesis, err 889 case goerliFlag: 890 return core.DefaultGoerliGenesisBlock(), nil 891 case rinkebyFlag: 892 return core.DefaultRinkebyGenesisBlock(), nil 893 case sepoliaFlag: 894 return core.DefaultSepoliaGenesisBlock(), nil 895 default: 896 return nil, fmt.Errorf("no genesis flag provided") 897 } 898 }