1// OpenRDAP 2// Copyright 2017 Tom Harwood 3// MIT License, see the LICENSE file. 4 5// Package bootstrap implements an RDAP bootstrap client. 6// 7// All RDAP queries are handled by an RDAP server. To help clients discover 8// RDAP servers, IANA publishes Service Registry files 9// (https://data.iana.org/rdap) for several query types: Domain names, IP 10// addresses, and Autonomous Systems. 11// 12// Given an RDAP query, this package finds the list of RDAP server URLs which 13// can answer it. This includes downloading & parsing the Service Registry 14// files. 15// 16// Basic usage: 17// question := &bootstrap.Question{ 18// RegistryType: bootstrap.DNS, 19// Query: "example.cz", 20// } 21// 22// b := &bootstrap.Client{} 23// 24// var answer *bootstrap.Answer 25// answer, err := b.Lookup(question) 26// 27// if err == nil { 28// for _, url := range answer.URLs { 29// fmt.Println(url) 30// } 31// } 32// 33// Download and list the contents of the DNS Service Registry: 34// b := &bootstrap.Client{} 35// 36// // Before you can use a Registry, you need to download it first. 37// err := b.Download(bootstrap.DNS) // Downloads https://data.iana.org/rdap/dns.json. 38// 39// if err == nil { 40// var dns *DNSRegistry = b.DNS() 41// 42// // Print TLDs with RDAP service. 43// for tld, _ := range dns.File().Entries { 44// fmt.Println(tld) 45// } 46// } 47// 48// You can configure bootstrap.Client{} with a custom http.Client, base URL 49// (default https://data.iana.org/rdap), and custom cache. bootstrap.Question{} 50// support Contexts (for timeout, etc.). 51// 52// A bootstrap.Client caches the Service Registry files in memory for both 53// performance, and courtesy to data.iana.org. The functions which make network 54// requests are: 55// - Download() - force download one of Service Registry file. 56// - DownloadWithContext() - force download one of Service Registry file. 57// - Lookup() - download one Service Registry file if missing, or if the cached file is over (by default) 24 hours old. 58// 59// Lookup() is intended for repeated usage: A long lived bootstrap.Client will 60// download each of {asn,dns,ipv4,ipv6}.json once per 24 hours only, regardless 61// of the number of calls made to Lookup(). You can still refresh them manually 62// using Download() if required. 63// 64// By default, Service Registry files are cached in memory. bootstrap.Client 65// also supports caching the Service Registry files on disk. The default cache 66// location is 67// $HOME/.openrdap/. 68// 69// Disk cache usage: 70// 71// b := bootstrap.NewClient() 72// b.Cache = cache.NewDiskCache() 73// 74// dsr := b.DNS() // Tries to load dns.json from disk cache, doesn't exist yet, so returns nil. 75// b.Download(bootstrap.DNS) // Downloads dns.json, saves to disk cache. 76// 77// b2 := bootstrap.NewClient() 78// b2.Cache = cache.NewDiskCache() 79// 80// dsr2 := b.DNS() // Loads dns.json from disk cache. 81// 82// This package also implements the experimental Service Provider registry. Due 83// to the experimental nature, no Service Registry file exists on data.iana.org 84// yet, additionally the filename isn't known. The current filename used is 85// serviceprovider-draft-03.json. 86// 87// RDAP bootstrapping is defined in https://tools.ietf.org/html/rfc7484. 88package bootstrap 89 90import ( 91 "context" 92 "crypto/sha256" 93 "encoding/hex" 94 "fmt" 95 "io/ioutil" 96 "net/http" 97 "net/url" 98 "time" 99 100 "github.com/openrdap/rdap/bootstrap/cache" 101) 102 103// A RegistryType represents a bootstrap registry type. 104type RegistryType int 105 106const ( 107 DNS RegistryType = iota 108 IPv4 109 IPv6 110 ASN 111 ServiceProvider 112) 113 114func (r RegistryType) String() string { 115 switch r { 116 case DNS: 117 return "dns" 118 case IPv4: 119 return "ipv4" 120 case IPv6: 121 return "ipv6" 122 case ASN: 123 return "asn" 124 case ServiceProvider: 125 return "serviceprovider" 126 default: 127 panic("Unknown RegistryType") 128 } 129} 130 131const ( 132 // Default URL of the Service Registry files. 133 DefaultBaseURL = "https://data.iana.org/rdap/" 134 135 // Default cache timeout of Service Registries. 136 DefaultCacheTimeout = time.Hour * 24 137) 138 139// Client implements an RDAP bootstrap client. 140type Client struct { 141 HTTP *http.Client // HTTP client. 142 BaseURL *url.URL // Base URL of the Service Registry files. Default is DefaultBaseURL. 143 Cache cache.RegistryCache // Service Registry cache. Default is a MemoryCache. 144 145 // Optional callback function for verbose messages. 146 Verbose func(text string) 147 148 registries map[RegistryType]Registry 149} 150 151// A Registry implements bootstrap lookups. 152type Registry interface { 153 Lookup(question *Question) (*Answer, error) 154 File() *File 155} 156 157func (c *Client) init() { 158 if c.HTTP == nil { 159 c.HTTP = &http.Client{} 160 } 161 162 if c.Cache == nil { 163 c.Cache = cache.NewMemoryCache() 164 c.Cache.SetTimeout(DefaultCacheTimeout) 165 } 166 167 if c.registries == nil { 168 c.registries = make(map[RegistryType]Registry) 169 } 170 171 if c.BaseURL == nil { 172 c.BaseURL, _ = url.Parse(DefaultBaseURL) 173 } 174} 175 176// Download downloads a single bootstrap registry file. 177// 178// On success, the relevant Registry is refreshed. Use the matching accessor (ASN(), DNS(), IPv4(), or IPv6()) to access it. 179func (c *Client) Download(registry RegistryType) error { 180 return c.DownloadWithContext(context.Background(), registry) 181} 182 183// DownloadWithContext downloads a single bootstrap registry file, with context |context|. 184// 185// On success, the relevant Registry is refreshed. Use the matching accessor (ASN(), DNS(), IPv4(), or IPv6()) to access it. 186func (c *Client) DownloadWithContext(ctx context.Context, registry RegistryType) error { 187 c.init() 188 189 var json []byte 190 var s Registry 191 192 json, s, err := c.download(ctx, registry) 193 194 if err != nil { 195 return err 196 } 197 198 err = c.Cache.Save(c.filenameFor(registry), json) 199 if err != nil { 200 return err 201 } 202 203 c.registries[registry] = s 204 205 return nil 206 207} 208 209func (c *Client) download(ctx context.Context, registry RegistryType) ([]byte, Registry, error) { 210 u, err := url.Parse(registry.Filename()) 211 if err != nil { 212 return nil, nil, err 213 } 214 215 baseURL := new(url.URL) 216 *baseURL = *c.BaseURL 217 218 if baseURL.Path != "" && baseURL.Path[len(baseURL.Path)-1] != '/' { 219 baseURL.Path += "/" 220 } 221 222 var fetchURL *url.URL = baseURL.ResolveReference(u) 223 req, err := http.NewRequest("GET", fetchURL.String(), nil) 224 if err != nil { 225 return nil, nil, err 226 } 227 req = req.WithContext(ctx) 228 229 resp, err := c.HTTP.Do(req) 230 if err != nil { 231 return nil, nil, err 232 } 233 defer resp.Body.Close() 234 235 if resp.StatusCode != 200 { 236 return nil, nil, fmt.Errorf("Server returned non-200 status code: %s", resp.Status) 237 } 238 239 json, err := ioutil.ReadAll(resp.Body) 240 if err != nil { 241 return nil, nil, err 242 } 243 244 var s Registry 245 s, err = newRegistry(registry, json) 246 247 if err != nil { 248 return json, nil, err 249 } 250 251 return json, s, nil 252} 253 254func (c *Client) freshenFromCache(registry RegistryType) { 255 if c.Cache.State(c.filenameFor(registry)) == cache.ShouldReload { 256 c.reloadFromCache(registry) 257 } 258} 259 260func (c *Client) reloadFromCache(registry RegistryType) error { 261 json, err := c.Cache.Load(c.filenameFor(registry)) 262 263 if err != nil { 264 return err 265 } 266 267 var s Registry 268 s, err = newRegistry(registry, json) 269 270 if err != nil { 271 return err 272 } 273 274 c.registries[registry] = s 275 276 return nil 277} 278 279func newRegistry(registry RegistryType, json []byte) (Registry, error) { 280 var s Registry 281 var err error 282 283 switch registry { 284 case ASN: 285 s, err = NewASNRegistry(json) 286 case DNS: 287 s, err = NewDNSRegistry(json) 288 case IPv4: 289 s, err = NewNetRegistry(json, 4) 290 case IPv6: 291 s, err = NewNetRegistry(json, 6) 292 case ServiceProvider: 293 s, err = NewServiceProviderRegistry(json) 294 default: 295 panic("Unknown Registrytype") 296 } 297 298 return s, err 299} 300 301// Lookup returns the RDAP base URLs for the bootstrap question |question|. 302func (c *Client) Lookup(question *Question) (*Answer, error) { 303 c.init() 304 if c.Verbose == nil { 305 c.Verbose = func(text string) {} 306 } 307 308 c.Verbose(" bootstrap: Looking up...") 309 c.Verbose(fmt.Sprintf(" bootstrap: Question type : %s", question.RegistryType)) 310 c.Verbose(fmt.Sprintf(" bootstrap: Question query: %s", question.Query)) 311 312 registry := question.RegistryType 313 314 var state cache.FileState = c.Cache.State(c.filenameFor(registry)) 315 c.Verbose(fmt.Sprintf(" bootstrap: Cache state: %s: %s", c.filenameFor(registry), state)) 316 317 var forceDownload bool 318 if state == cache.ShouldReload { 319 if err := c.reloadFromCache(registry); err != nil { 320 forceDownload = true 321 322 c.Verbose(fmt.Sprintf(" bootstrap: Cache load error (%s), downloading...", err)) 323 } 324 } 325 326 if c.registries[registry] == nil || forceDownload { 327 c.Verbose(fmt.Sprintf(" bootstrap: Downloading %s", registry.Filename())) 328 329 err := c.DownloadWithContext(question.Context(), registry) 330 if err != nil { 331 return nil, err 332 } 333 } else { 334 c.Verbose(" bootstrap: Using cached Service Registry file") 335 } 336 337 answer, err := c.registries[registry].Lookup(question) 338 339 if answer != nil { 340 c.Verbose(fmt.Sprintf(" bootstrap: Looked up '%s'", answer.Query)) 341 if answer.Entry != "" { 342 c.Verbose(fmt.Sprintf(" bootstrap: Matching entry '%s'", answer.Entry)) 343 } else { 344 c.Verbose(fmt.Sprintf(" bootstrap: No match")) 345 } 346 347 for i, url := range answer.URLs { 348 c.Verbose(fmt.Sprintf(" bootstrap: Service URL #%d: '%s'", i+1, url)) 349 } 350 } 351 352 return answer, err 353} 354 355// ASN returns the current ASN Registry (or nil if the registry file hasn't been Download()ed). 356// 357// This function never initiates a network transfer. 358func (c *Client) ASN() *ASNRegistry { 359 c.init() 360 c.freshenFromCache(ServiceProvider) 361 362 s, _ := c.registries[ASN].(*ASNRegistry) 363 return s 364} 365 366// 367// DNS returns the current DNS Registry (or nil if the registry file hasn't been Download()ed). 368// 369// This function never initiates a network transfer. 370func (c *Client) DNS() *DNSRegistry { 371 c.init() 372 c.freshenFromCache(ServiceProvider) 373 374 s, _ := c.registries[DNS].(*DNSRegistry) 375 return s 376} 377 378// IPv4 returns the current IPv4 Registry (or nil if the registry file hasn't been Download()ed). 379// 380// This function never initiates a network transfer. 381func (c *Client) IPv4() *NetRegistry { 382 c.init() 383 c.freshenFromCache(ServiceProvider) 384 385 s, _ := c.registries[IPv4].(*NetRegistry) 386 return s 387} 388 389// IPv6 returns the current IPv6 Registry (or nil if the registry file hasn't been Download()ed). 390// 391// This function never initiates a network transfer. 392func (c *Client) IPv6() *NetRegistry { 393 c.init() 394 c.freshenFromCache(ServiceProvider) 395 396 s, _ := c.registries[IPv6].(*NetRegistry) 397 return s 398} 399 400// ServiceProvider returns the current ServiceProvider Registry (or nil if the registry file hasn't been Download()ed). 401// 402// This function never initiates a network transfer. 403func (c *Client) ServiceProvider() *ServiceProviderRegistry { 404 c.init() 405 c.freshenFromCache(ServiceProvider) 406 407 s, _ := c.registries[ServiceProvider].(*ServiceProviderRegistry) 408 return s 409} 410 411// fileFor returns a filename to save the bootstrap registry file |r| as. 412// 413// For the official IANA bootstrap service, this is the exact filename, e.g. 414// dns.json. 415// 416// For custom bootstrap services, a 6 character hash of the bootstrap service 417// URL is prepended to the filename (e.g. 012def_dns.json), to prevent mixing 418// them up. 419func (c *Client) filenameFor(r RegistryType) string { 420 filename := r.Filename() 421 422 if c.BaseURL.String() != DefaultBaseURL { 423 hasher := sha256.New() 424 hasher.Write([]byte(c.BaseURL.String())) 425 sha256Hash := hex.EncodeToString(hasher.Sum(nil)) 426 427 filename = sha256Hash[0:6] + "_" + filename 428 } 429 430 return filename 431} 432 433// Filename returns the JSON document filename: One of {asn,dns,ipv4,ipv6,service_provider}.json. 434func (r RegistryType) Filename() string { 435 switch r { 436 case ASN: 437 return "asn.json" 438 case DNS: 439 return "dns.json" 440 case IPv4: 441 return "ipv4.json" 442 case IPv6: 443 return "ipv6.json" 444 case ServiceProvider: 445 // This is a guess and will need fixing to match whatever IANA chooses. 446 return "serviceprovider-draft-03.json" 447 default: 448 panic("Unknown RegistryType") 449 } 450} 451