@@ -7,10 +7,19 @@ import (
77 "log/slog"
88 "net"
99 "net/http"
10+ "net/netip"
11+ "slices"
1012 "sync"
1113 "time"
1214)
1315
16+ var GoogleCrawlerIPRangeURLs = []string {
17+ "https://developers.google.com/static/search/apis/ipranges/googlebot.json" ,
18+ "https://developers.google.com/static/crawling/ipranges/common-crawlers.json" ,
19+ "https://developers.google.com/static/crawling/ipranges/special-crawlers.json" ,
20+ "https://developers.google.com/static/crawling/ipranges/user-triggered-fetchers-google.json" ,
21+ }
22+
1423// GooglebotIPs holds the list of Googlebot IP ranges, providing a thread-safe way to check if an IP is a Googlebot.
1524type GooglebotIPs struct {
1625 cidrs []* net.IPNet
@@ -108,3 +117,76 @@ func FetchGooglebotIPs(log *slog.Logger, httpClient *http.Client, url string) ([
108117
109118 return cidrs , nil
110119}
120+
121+ // FetchGoogleCrawlerIPs fetches crawler IP ranges from multiple Google-managed endpoints,
122+ // then returns a canonical, unique list where broader prefixes replace narrower prefixes.
123+ func FetchGoogleCrawlerIPs (log * slog.Logger , httpClient * http.Client , urls []string ) ([]string , error ) {
124+ if len (urls ) == 0 {
125+ return nil , nil
126+ }
127+
128+ allCIDRs := make ([]string , 0 )
129+ for _ , url := range urls {
130+ cidrs , err := FetchGooglebotIPs (log , httpClient , url )
131+ if err != nil {
132+ return nil , err
133+ }
134+ allCIDRs = append (allCIDRs , cidrs ... )
135+ }
136+
137+ return ReduceCIDRs (allCIDRs , log ), nil
138+ }
139+
140+ // ReduceCIDRs canonicalizes CIDRs, removes exact duplicates, and removes narrower
141+ // ranges when they are fully covered by broader ranges.
142+ func ReduceCIDRs (cidrs []string , log * slog.Logger ) []string {
143+ prefixes := make ([]netip.Prefix , 0 , len (cidrs ))
144+ for _ , cidr := range cidrs {
145+ prefix , err := netip .ParsePrefix (cidr )
146+ if err != nil {
147+ if log != nil {
148+ log .Error ("error parsing CIDR" , "cidr" , cidr , "err" , err )
149+ }
150+ continue
151+ }
152+ prefixes = append (prefixes , prefix .Masked ())
153+ }
154+
155+ slices .SortFunc (prefixes , func (a , b netip.Prefix ) int {
156+ aIs4 := a .Addr ().Is4 ()
157+ bIs4 := b .Addr ().Is4 ()
158+ if aIs4 && ! bIs4 {
159+ return - 1
160+ }
161+ if ! aIs4 && bIs4 {
162+ return 1
163+ }
164+
165+ if a .Bits () != b .Bits () {
166+ return a .Bits () - b .Bits ()
167+ }
168+
169+ return a .Addr ().Compare (b .Addr ())
170+ })
171+
172+ reduced := make ([]netip.Prefix , 0 , len (prefixes ))
173+ for _ , candidate := range prefixes {
174+ covered := false
175+ for _ , existing := range reduced {
176+ if existing .Bits () <= candidate .Bits () && existing .Contains (candidate .Addr ()) {
177+ covered = true
178+ break
179+ }
180+ }
181+ if ! covered {
182+ reduced = append (reduced , candidate )
183+ }
184+ }
185+
186+ result := make ([]string , 0 , len (reduced ))
187+ for _ , prefix := range reduced {
188+ result = append (result , prefix .String ())
189+ }
190+
191+ return result
192+ }
0 commit comments