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