@@ -2,6 +2,7 @@ package main
22
33import (
44 "encoding/json"
5+ "errors"
56 "fmt"
67 "io"
78 "log/slog"
@@ -10,6 +11,7 @@ import (
1011 "net/http"
1112 "os"
1213 "os/exec"
14+ "path/filepath"
1315 "strings"
1416 "sync"
1517 "time"
@@ -28,9 +30,9 @@ const parallelism = 10
2830
2931func main () {
3032 log := slog .New (slog .NewTextHandler (os .Stdout , nil ))
31- googleCIDRs , err := helper .FetchGooglebotIPs (log , http .DefaultClient , "https://developers.google.com/static/search/apis/ipranges/googlebot.json" )
33+ googleCIDRs , err := helper .FetchGoogleCrawlerIPs (log , http .DefaultClient , helper . GoogleCrawlerIPRangeURLs )
3234 if err != nil {
33- slog .Error ("unable to fetch google bot ips" , "err" , err )
35+ slog .Error ("unable to fetch google crawler ips" , "err" , err )
3436 os .Exit (1 )
3537 }
3638
@@ -52,16 +54,27 @@ func main() {
5254 fmt .Printf ("Generating %d IPs\n " , numIPs )
5355 ips := generateUniquePublicIPs (numIPs )
5456
57+ statePath , err := prepareStateFile (0o777 , 0o666 )
58+ if err != nil {
59+ slog .Error ("Failed to prepare state file" , "statePath" , statePath , "err" , err )
60+ os .Exit (1 )
61+ }
62+
5563 fmt .Println ("Bringing traefik/nginx online" )
5664 runCommand ("docker" , "compose" , "up" , "-d" )
5765 waitForService ("http://localhost" )
5866 waitForService ("http://localhost/app2" )
67+ waitForGoogleExemptionReady (googleCIDRs )
5968
6069 fmt .Printf ("Making sure %d attempt(s) pass\n " , rateLimit )
6170 runParallelChecks (ips , rateLimit , "http://localhost" )
6271
63- time .Sleep (cp .StateSaveInterval + cp .StateSaveJitter + (1 * time .Second ))
64- runCommand ("jq" , "." , "tmp/state.json" )
72+ statePath , err = waitForStateFile (30 * time .Second )
73+ if err != nil {
74+ slog .Error ("State file was not created in time" , "err" , err )
75+ os .Exit (1 )
76+ }
77+ runCommand ("jq" , "." , statePath )
6578
6679 fmt .Printf ("Making sure attempt #%d causes a redirect to the challenge page\n " , rateLimit + 1 )
6780 ensureRedirect (ips , "http://localhost" )
@@ -81,7 +94,7 @@ func main() {
8194 time .Sleep (10 * time .Second )
8295 checkStateReload ()
8396
84- runCommand ("rm" , "tmp/state.json" )
97+ runCommand ("rm" , "-f" , statePath )
8598
8699}
87100
@@ -400,7 +413,7 @@ func testGoogleBotGetsThrough(googleCIDRs []string) {
400413
401414 // Prime the rate limiter for the GoogleBot IP with parameters
402415 fmt .Printf ("Priming rate limiter for GoogleBot IP %s with params (%d requests)\n " , googleIP , rateLimit )
403- for i := 0 ; i < rateLimit ; i ++ {
416+ for i := range rateLimit {
404417 output = httpRequest (googleIP , "http://localhost/?foo=bar" ) // Assign value
405418 if output != "" {
406419 slog .Error (fmt .Sprintf ("GoogleBot with params was challenged prematurely on request #%d" , i + 1 ), "ip" , googleIP , "output" , output )
@@ -421,3 +434,91 @@ func testGoogleBotGetsThrough(googleCIDRs []string) {
421434 // set things back to normal for other tests
422435 runCommand ("docker" , "compose" , "down" )
423436}
437+
438+ func waitForGoogleExemptionReady (googleCIDRs []string ) {
439+ googleIP , err := firstUsableIPv4FromCIDRs (googleCIDRs )
440+ if err != nil {
441+ slog .Warn ("Unable to select Google IP for readiness check; skipping warmup" , "err" , err )
442+ return
443+ }
444+
445+ deadline := time .Now ().Add (90 * time .Second )
446+ for time .Now ().Before (deadline ) {
447+ ready := true
448+ for i := 0 ; i < rateLimit + 1 ; i ++ {
449+ if output := httpRequest (googleIP , "http://localhost" ); output != "" {
450+ ready = false
451+ break
452+ }
453+ }
454+ if ready {
455+ fmt .Printf ("Google exemption is active for %s\n " , googleIP )
456+ return
457+ }
458+ time .Sleep (500 * time .Millisecond )
459+ }
460+
461+ slog .Error ("Timed out waiting for Google crawler IP exemption to become active" , "googleIP" , googleIP )
462+ os .Exit (1 )
463+ }
464+
465+ func firstUsableIPv4FromCIDRs (cidrs []string ) (string , error ) {
466+ for _ , cidr := range cidrs {
467+ ip , err := getIPFromCIDR (cidr )
468+ if err != nil {
469+ continue
470+ }
471+ parsed := net .ParseIP (ip )
472+ if parsed != nil && parsed .To4 () != nil {
473+ return ip , nil
474+ }
475+ }
476+
477+ return "" , fmt .Errorf ("no usable IPv4 found in CIDR list" )
478+ }
479+
480+ func waitForStateFile (timeout time.Duration ) (string , error ) {
481+ paths := []string {
482+ filepath .Join ("tmp" , "state.json" ),
483+ filepath .Join ("ci" , "tmp" , "state.json" ),
484+ }
485+
486+ deadline := time .Now ().Add (timeout )
487+ for time .Now ().Before (deadline ) {
488+ for _ , p := range paths {
489+ info , err := os .Stat (p )
490+ if err == nil && ! info .IsDir () {
491+ return p , nil
492+ }
493+ if err != nil && ! errors .Is (err , os .ErrNotExist ) {
494+ return "" , fmt .Errorf ("failed to stat %s: %w" , p , err )
495+ }
496+ }
497+ time .Sleep (500 * time .Millisecond )
498+ }
499+
500+ return "" , fmt .Errorf ("state file not found; checked: %s" , strings .Join (paths , ", " ))
501+ }
502+
503+ func prepareStateFile (dirMode , fileMode os.FileMode ) (string , error ) {
504+ p := filepath .Join ("tmp" , "state.json" )
505+
506+ dir := filepath .Dir (p )
507+ if err := os .MkdirAll (dir , dirMode ); err != nil {
508+ return "" , fmt .Errorf ("failed to create state dir %s: %w" , dir , err )
509+ }
510+ if err := os .Chmod (dir , dirMode ); err != nil {
511+ return "" , fmt .Errorf ("failed to chmod state dir %s: %w" , dir , err )
512+ }
513+
514+ f , err := os .OpenFile (p , os .O_CREATE | os .O_RDWR , fileMode )
515+ if err != nil {
516+ return "" , fmt .Errorf ("failed to open state file %s: %w" , p , err )
517+ }
518+ _ = f .Close ()
519+ if err := os .Chmod (p , fileMode ); err != nil {
520+ return "" , fmt .Errorf ("failed to chmod state file %s: %w" , p , err )
521+ }
522+
523+ return p , nil
524+ }
0 commit comments