-
Notifications
You must be signed in to change notification settings - Fork 7
/
transport.go
355 lines (287 loc) · 9.97 KB
/
transport.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
package rdap
import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"github.com/registrobr/rdap/protocol"
)
// List of resource type path segments for exact match lookup as described in
// RFC 7482, section 3.1
const (
// QueryTypeDomain used to identify reverse DNS (RIR) or domain name (DNR)
// information and associated data referenced using a fully qualified domain
// name
QueryTypeDomain QueryType = "domain"
// QueryTypeTicket used to query a domain request. This query type was
// created by NIC.br to allow retrieving information about the domain
// requests
QueryTypeTicket QueryType = "ticket"
// QueryTypeAutnum used to identify Autonomous System number registrations
// and associated data referenced using an asplain Autonomous System number
QueryTypeAutnum QueryType = "autnum"
// QueryTypeIP used to identify IP networks and associated data referenced
// using either an IPv4 or IPv6 address
QueryTypeIP QueryType = "ip"
// QueryTypeEntity used to identify an entity information query using a
// string identifier
QueryTypeEntity QueryType = "entity"
)
// QueryType stores the query type when sending a query to an RDAP server
type QueryType string
const (
bootstrapQueryTypeNone bootstrapQueryType = ""
bootstrapQueryTypeDNS bootstrapQueryType = "dns"
bootstrapQueryTypeASN bootstrapQueryType = "asn"
bootstrapQueryTypeIPv4 bootstrapQueryType = "ipv4"
bootstrapQueryTypeIPv6 bootstrapQueryType = "ipv6"
)
type bootstrapQueryType string
type ErrNoMatch struct {
QueryValue string
}
func (e *ErrNoMatch) Error() string {
return fmt.Sprintf("no matches for %v", e.QueryValue)
}
func newBootstrapQueryType(queryType QueryType, queryValue string) (bootstrapQueryType, bool) {
switch queryType {
case QueryTypeDomain:
return bootstrapQueryTypeDNS, true
case QueryTypeAutnum:
return bootstrapQueryTypeASN, true
case QueryTypeIP:
ip := net.ParseIP(queryValue)
if ip != nil {
if ip.To4() != nil {
return bootstrapQueryTypeIPv4, true
}
return bootstrapQueryTypeIPv6, true
}
var err error
ip, _, err = net.ParseCIDR(queryValue)
if err != nil {
return bootstrapQueryTypeNone, false
}
if ip.To4() != nil {
return bootstrapQueryTypeIPv4, true
}
return bootstrapQueryTypeIPv6, true
}
return bootstrapQueryTypeNone, false
}
const (
// IANABootstrap stores the default URL to query to retrieve the RDAP
// servers that contain the desired information
IANABootstrap = "https://data.iana.org/rdap/%s.json"
)
var (
// ErrNotFound is used when the RDAP server doesn't contain any
// information of the requested object
ErrNotFound = errors.New("not found")
ErrForbidden = errors.New("forbidden")
)
// Fetcher represents the network layer responsible for retrieving the
// resource information from a RDAP server
type Fetcher interface {
Fetch(uris []string, queryType QueryType, queryValue string, header http.Header, queryString url.Values) (*http.Response, error)
}
// fetcherFunc is a function type that implements the Fetcher interface
type fetcherFunc func([]string, QueryType, string, http.Header, url.Values) (*http.Response, error)
// Fetch will try to use the addresses from the uris parameter to send
// requests using the queryType and queryValue parameters. You can optionally
// set HTTP headers (like X-Forwarded-For) for the RDAP server request. On
// success will return a HTTP response, otherwise an error will be returned.
// The caller is responsible for closing the response body
func (f fetcherFunc) Fetch(uris []string, queryType QueryType, queryValue string, header http.Header, queryString url.Values) (*http.Response, error) {
return f(uris, queryType, queryValue, header, queryString)
}
type decorator func(Fetcher) Fetcher
func decorate(f Fetcher, ds ...decorator) Fetcher {
for _, decorate := range ds {
f = decorate(f)
}
return f
}
// CacheDetector is used to define how do you detect if a HTTP response is
// from cache when performing bootstrap. This depends on the proxy that you
// are using between the client and the bootstrap server
type CacheDetector func(*http.Response) bool
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
type defaultFetcher struct {
httpClient httpClient
}
// NewDefaultFetcher returns a transport layer that send requests directly to
// the RDAP servers
func NewDefaultFetcher(httpClient httpClient) Fetcher {
return &defaultFetcher{
httpClient: httpClient,
}
}
func (d *defaultFetcher) Fetch(uris []string, queryType QueryType, queryValue string, header http.Header, queryString url.Values) (resp *http.Response, err error) {
if len(uris) == 0 {
return nil, fmt.Errorf("no URIs defined to query")
}
for _, uri := range uris {
resp, err = d.fetchURI(uri, queryType, queryValue, header, queryString)
if err != nil {
continue
}
return
}
return
}
func (d *defaultFetcher) fetchURI(uri string, queryType QueryType, queryValue string, header http.Header, queryString url.Values) (*http.Response, error) {
if !strings.HasPrefix(uri, "http://") && !strings.HasPrefix(uri, "https://") {
uri = "http://" + uri
}
if pos := strings.Index(uri, "?"); pos != -1 {
uri = uri[:pos]
}
uri = strings.TrimRight(uri, "/")
uri = fmt.Sprintf("%s/%s/%s", uri, queryType, queryValue)
if q := queryString.Encode(); len(q) > 0 {
uri += "?" + q
}
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil {
return nil, err
}
if header != nil {
req.Header = header
}
req.Header.Set("Accept", "application/rdap+json")
req.Header.Set("User-Agent", "registrobr-rdap")
resp, err := d.httpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusNotFound {
// we will return the response here so the client can analyze the body or
// some special HTTP headers to identify the reason why it does not
// exists
return resp, ErrNotFound
} else if resp.StatusCode == http.StatusForbidden {
return resp, ErrForbidden
}
contentType := resp.Header.Get("Content-Type")
contentTypeParts := strings.Split(contentType, ";")
if len(contentTypeParts) == 0 || contentTypeParts[0] != "application/rdap+json" {
return nil, fmt.Errorf("unexpected response: %d %s",
resp.StatusCode, http.StatusText(resp.StatusCode))
}
if resp.StatusCode != http.StatusOK {
var responseErr protocol.Error
if err := json.NewDecoder(resp.Body).Decode(&responseErr); err != nil {
return nil, err
}
return nil, responseErr
}
return resp, nil
}
// NewBootstrapFetcher returns a transport layer that tries to find the
// resource in a bootstrap strategy to detect the RDAP servers that can contain
// the information. After finding the RDAP servers, it will send the requests to
// retrieve the desired information
func NewBootstrapFetcher(httpClient httpClient, bootstrapURI string, cacheDetector CacheDetector) Fetcher {
return decorate(
NewDefaultFetcher(httpClient),
bootstrap(bootstrapURI, httpClient, cacheDetector),
)
}
func bootstrap(bootstrapURI string, httpClient httpClient, cacheDetector CacheDetector) decorator {
return func(f Fetcher) Fetcher {
return fetcherFunc(func(uris []string, queryType QueryType, queryValue string, header http.Header, queryString url.Values) (*http.Response, error) {
bootstrapQueryType, ok := newBootstrapQueryType(queryType, queryValue)
if !ok {
// if we can't convert the queryType the resource is probably not
// supported by the bootstrap
return f.Fetch(uris, queryType, queryValue, header, queryString)
}
bootstrapURI := fmt.Sprintf(bootstrapURI, bootstrapQueryType)
serviceRegistry, cached, err := bootstrapFetch(httpClient, bootstrapURI, false, cacheDetector)
if err != nil {
return nil, err
}
switch queryType {
case QueryTypeDomain:
uris, err = serviceRegistry.matchDomain(queryValue)
if err == nil && len(uris) == 0 && cached {
var nsSet []*net.NS
if nsSet, err = lookupNS(queryValue); err == nil && len(nsSet) > 0 {
serviceRegistry, _, err = bootstrapFetch(httpClient, bootstrapURI, true, cacheDetector)
if err == nil {
uris, err = serviceRegistry.matchDomain(queryValue)
}
}
}
case QueryTypeAutnum:
var asn uint64
if asn, err = strconv.ParseUint(queryValue, 10, 32); err == nil {
uris, err = serviceRegistry.matchAS(uint32(asn))
}
case QueryTypeIP:
ip := net.ParseIP(queryValue)
if ip != nil {
uris, err = serviceRegistry.matchIP(ip)
} else {
var cidr *net.IPNet
if _, cidr, err = net.ParseCIDR(queryValue); err == nil {
uris, err = serviceRegistry.matchIPNetwork(cidr)
}
}
}
if err != nil {
return nil, err
}
if len(uris) == 0 {
return nil, &ErrNoMatch{QueryValue: queryValue}
}
sort.Sort(prioritizeHTTPS(uris))
return f.Fetch(uris, queryType, queryValue, header, queryString)
})
}
}
func bootstrapFetch(httpClient httpClient, uri string, reloadCache bool, cacheDetector CacheDetector) (*serviceRegistry, bool, error) {
req, err := http.NewRequest(http.MethodGet, uri, nil)
if err != nil {
return nil, false, err
}
req.Header.Add("Accept", "application/json")
if reloadCache {
req.Header.Add("Cache-Control", "max-age=0")
}
resp, err := httpClient.Do(req)
if err != nil {
return nil, false, err
}
defer func() {
if resp.Body != nil {
resp.Body.Close()
}
}()
cached := false
if cacheDetector != nil {
cached = cacheDetector(resp)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotModified {
return nil, cached, fmt.Errorf("unexpected status code %d %s", resp.StatusCode, http.StatusText(resp.StatusCode))
}
var serviceRegistry serviceRegistry
if err := json.NewDecoder(resp.Body).Decode(&serviceRegistry); err != nil {
return nil, cached, err
}
if serviceRegistry.Version != version {
return nil, false, fmt.Errorf("incompatible bootstrap specification version: %s (expecting %s)", serviceRegistry.Version, version)
}
return &serviceRegistry, cached, nil
}
var lookupNS = func(name string) (nss []*net.NS, err error) {
return net.LookupNS(name)
}