master
go 410 lines 10.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package main
4
5 import (
6 "archive/tar"
7 "archive/zip"
8 "bytes"
9 "compress/gzip"
10 "errors"
11 "fmt"
12 "io"
13 "net/http"
14 "net/url"
15 "os"
16 "path"
17 "regexp"
18 "sort"
19 "strconv"
20 "strings"
21 "time"
22 )
23
24 var errTarMMDBNotFound = errors.New("tar payload has no mmdb member")
25
26 type downloader struct {
27 client *http.Client
28 userAgent string
29 }
30
31 func newDownloader(httpCfg httpConfig) *downloader {
32 return &downloader{
33 client: &http.Client{Timeout: httpCfg.timeout},
34 userAgent: strings.TrimSpace(httpCfg.userAgent),
35 }
36 }
37
38 func (d *downloader) readDataset(source sourceEntry) (generationDatasetRef, []byte, error) {
39 resolved, err := d.resolveSource(source)
40 if err != nil {
41 return generationDatasetRef{}, nil, err
42 }
43
44 raw, err := d.readRaw(resolved.fetchPath, resolved.fetchURL)
45 if err != nil {
46 return generationDatasetRef{}, nil, err
47 }
48 payload, err := decodePayloadForSource(source, raw)
49 if err != nil {
50 return generationDatasetRef{}, nil, err
51 }
52 return resolved.ref, payload, nil
53 }
54
55 type resolvedSource struct {
56 ref generationDatasetRef
57 fetchURL string
58 fetchPath string
59 }
60
61 func (d *downloader) resolveSource(source sourceEntry) (resolvedSource, error) {
62 spec, ok := builtInSource(source.provider, source.artifact)
63 if !ok {
64 return resolvedSource{}, fmt.Errorf(
65 "unsupported provider/artifact %q/%q",
66 source.provider,
67 source.artifact,
68 )
69 }
70
71 ref := generationDatasetRef{
72 Name: source.name,
73 Family: source.family,
74 Provider: source.provider,
75 Artifact: source.artifact,
76 Format: source.format,
77 }
78
79 switch {
80 case source.path != "":
81 path, err := expandEnvPlaceholders(source.path)
82 if err != nil {
83 return resolvedSource{}, err
84 }
85 ref.Source = "path"
86 ref.Path = path
87 return resolvedSource{
88 ref: ref,
89 fetchPath: path,
90 }, nil
91 case source.url != "":
92 fetchURL, err := expandEnvPlaceholders(source.url)
93 if err != nil {
94 return resolvedSource{}, err
95 }
96 ref.Source = "url"
97 ref.URL = sanitizeURLForMetadata(source.url)
98 return resolvedSource{
99 ref: ref,
100 fetchURL: fetchURL,
101 }, nil
102 case spec.directURL != "":
103 fetchURL, err := expandEnvPlaceholders(spec.directURL)
104 if err != nil {
105 return resolvedSource{}, err
106 }
107 ref.Source = "builtin"
108 ref.URL = sanitizeURLForMetadata(spec.directURL)
109 return resolvedSource{
110 ref: ref,
111 fetchURL: fetchURL,
112 }, nil
113 case spec.pageURL != "":
114 var (
115 resolvedURL string
116 err error
117 )
118 if source.provider == providerCAIDA && source.artifact == artifactCAIDAPrefix2AS {
119 resolvedURL, err = d.resolveCAIDAPrefix2ASURL(spec.pageURL)
120 } else {
121 resolvedURL, err = d.resolveDBIPArtifactURL(spec.pageURL, source.artifact, source.format)
122 }
123 if err != nil {
124 return resolvedSource{}, err
125 }
126 ref.Source = "builtin"
127 ref.DownloadPage = sanitizeURLForMetadata(spec.pageURL)
128 ref.ResolvedURL = sanitizeURLForMetadata(resolvedURL)
129 return resolvedSource{
130 ref: ref,
131 fetchURL: resolvedURL,
132 }, nil
133 default:
134 return resolvedSource{}, fmt.Errorf(
135 "provider/artifact %q/%q has no usable locator",
136 source.provider,
137 source.artifact,
138 )
139 }
140 }
141
142 func (d *downloader) resolveDBIPArtifactURL(pageURL, artifact, format string) (string, error) {
143 page, err := d.readHTTP(pageURL)
144 if err != nil {
145 return "", fmt.Errorf("failed to fetch DB-IP landing page %s: %w", pageURL, err)
146 }
147
148 ext := format
149 if format == formatTSV {
150 return "", fmt.Errorf("dbip artifact %q does not support format %q", artifact, format)
151 }
152 pattern := fmt.Sprintf(
153 `https://download\.db-ip\.com/free/dbip-%s-\d{4}-\d{2}\.%s\.gz`,
154 regexp.QuoteMeta(strings.ToLower(strings.TrimSpace(artifact))),
155 regexp.QuoteMeta(ext),
156 )
157 re := regexp.MustCompile(pattern)
158 match := re.Find(page)
159 if len(match) == 0 {
160 return "", fmt.Errorf(
161 "failed to resolve DB-IP %s/%s download from %s",
162 artifact,
163 format,
164 pageURL,
165 )
166 }
167 return string(match), nil
168 }
169
170 func (d *downloader) resolveCAIDAPrefix2ASURL(logURL string) (string, error) {
171 page, err := d.readHTTP(logURL)
172 if err != nil {
173 return "", fmt.Errorf("failed to fetch CAIDA prefix2as creation log %s: %w", redactURLForDisplay(logURL), err)
174 }
175
176 type caidaCandidate struct {
177 path string
178 timestamp int64
179 hasTime bool
180 }
181
182 candidates := make([]caidaCandidate, 0)
183 for line := range strings.SplitSeq(string(page), "\n") {
184 fields := strings.Fields(strings.TrimSpace(line))
185 if len(fields) == 0 {
186 continue
187 }
188 candidate := fields[len(fields)-1]
189 if strings.HasSuffix(candidate, ".pfx2as.gz") {
190 entry := caidaCandidate{path: candidate}
191 if len(fields) >= 2 {
192 if timestamp, err := strconv.ParseInt(fields[len(fields)-2], 10, 64); err == nil {
193 entry.timestamp = timestamp
194 entry.hasTime = true
195 }
196 }
197 candidates = append(candidates, entry)
198 }
199 }
200 if len(candidates) == 0 {
201 return "", fmt.Errorf("failed to resolve latest CAIDA prefix2as download from %s", redactURLForDisplay(logURL))
202 }
203 sort.Slice(candidates, func(i, j int) bool {
204 if candidates[i].hasTime != candidates[j].hasTime {
205 return !candidates[i].hasTime
206 }
207 if candidates[i].timestamp != candidates[j].timestamp {
208 return candidates[i].timestamp < candidates[j].timestamp
209 }
210 return candidates[i].path < candidates[j].path
211 })
212 latest := candidates[len(candidates)-1].path
213
214 base, err := url.Parse(logURL)
215 if err != nil {
216 return "", fmt.Errorf("failed to parse CAIDA prefix2as creation log URL %s: %w", redactURLForDisplay(logURL), err)
217 }
218 base.RawQuery = ""
219 base.Fragment = ""
220 if !strings.HasSuffix(base.Path, "/") {
221 base.Path = path.Dir(base.Path) + "/"
222 }
223 ref, err := url.Parse(latest)
224 if err != nil {
225 return "", fmt.Errorf("failed to parse CAIDA prefix2as candidate %q: %w", latest, err)
226 }
227 return base.ResolveReference(ref).String(), nil
228 }
229
230 func expandEnvPlaceholders(raw string) (string, error) {
231 missing := map[string]struct{}{}
232 expanded := os.Expand(raw, func(name string) string {
233 value, ok := os.LookupEnv(name)
234 if !ok || strings.TrimSpace(value) == "" {
235 missing[name] = struct{}{}
236 return ""
237 }
238 return value
239 })
240 if len(missing) == 0 {
241 return expanded, nil
242 }
243 names := make([]string, 0, len(missing))
244 for name := range missing {
245 names = append(names, name)
246 }
247 sort.Strings(names)
248 return "", fmt.Errorf("missing environment variable(s): %s", strings.Join(names, ", "))
249 }
250
251 func sanitizeURLForMetadata(raw string) string {
252 parsed, err := url.Parse(raw)
253 if err != nil || parsed.Host == "" {
254 return "<redacted>"
255 }
256 parsed.User = nil
257 if parsed.RawQuery != "" {
258 parsed.RawQuery = "redacted"
259 }
260 parsed.Fragment = ""
261 return parsed.String()
262 }
263
264 func (d *downloader) readRaw(path, rawURL string) ([]byte, error) {
265 if path != "" {
266 content, err := os.ReadFile(path)
267 if err != nil {
268 return nil, fmt.Errorf("failed to read %s: %w", path, err)
269 }
270 return content, nil
271 }
272 return d.readHTTP(rawURL)
273 }
274
275 func (d *downloader) readHTTP(rawURL string) ([]byte, error) {
276 displayURL := redactURLForDisplay(rawURL)
277 req, err := http.NewRequest(http.MethodGet, rawURL, nil)
278 if err != nil {
279 return nil, fmt.Errorf("failed to build request %s: %w", displayURL, err)
280 }
281 req.Header.Set("User-Agent", d.userAgent)
282
283 start := time.Now()
284 resp, err := d.client.Do(req)
285 if err != nil {
286 return nil, fmt.Errorf("failed to fetch %s: %w", displayURL, err)
287 }
288 defer resp.Body.Close()
289
290 if resp.StatusCode != http.StatusOK {
291 return nil, fmt.Errorf("failed to fetch %s: unexpected status %d", displayURL, resp.StatusCode)
292 }
293
294 content, err := io.ReadAll(resp.Body)
295 if err != nil {
296 return nil, fmt.Errorf("failed to read %s: %w", displayURL, err)
297 }
298 _ = start
299 return content, nil
300 }
301
302 func decodePayloadForSource(source sourceEntry, raw []byte) ([]byte, error) {
303 switch {
304 case source.provider == providerMaxMind && source.artifact == artifactMaxMindGeoLite2ASN:
305 return decodeMaxMindASNPayload(raw)
306 case source.provider == providerMaxMind && source.artifact == artifactMaxMindGeoLite2Country:
307 return raw, nil
308 case source.provider == providerIP2Location && source.artifact == artifactIP2LocationCountryLite:
309 return raw, nil
310 case source.provider == providerIPDeny && source.artifact == artifactIPDenyCountryZones:
311 return raw, nil
312 case source.provider == providerIPIP && source.artifact == artifactIPIPCountry:
313 return raw, nil
314 default:
315 return decodePayload(raw)
316 }
317 }
318
319 func decodeMaxMindASNPayload(raw []byte) ([]byte, error) {
320 if len(raw) < 2 || raw[0] != 0x1f || raw[1] != 0x8b {
321 return raw, nil
322 }
323 content, err := decodeGzip(raw)
324 if err != nil {
325 return nil, err
326 }
327 mmdb, err := extractMMDBFromTar(content)
328 if err == nil {
329 return mmdb, nil
330 }
331 if !errors.Is(err, errTarMMDBNotFound) {
332 return nil, fmt.Errorf("failed to extract MaxMind ASN MMDB from tar payload: %w", err)
333 }
334 return content, nil
335 }
336
337 func decodePayload(raw []byte) ([]byte, error) {
338 if len(raw) >= 2 && raw[0] == 0x1f && raw[1] == 0x8b {
339 return decodeGzip(raw)
340 }
341 if len(raw) >= 4 && bytes.Equal(raw[:4], []byte{'P', 'K', 0x03, 0x04}) {
342 return decodeZip(raw)
343 }
344 return raw, nil
345 }
346
347 func decodeGzip(raw []byte) ([]byte, error) {
348 reader, err := gzip.NewReader(bytes.NewReader(raw))
349 if err != nil {
350 return nil, fmt.Errorf("failed to open gzip payload: %w", err)
351 }
352 defer reader.Close()
353
354 content, err := io.ReadAll(reader)
355 if err != nil {
356 return nil, fmt.Errorf("failed to read gzip payload: %w", err)
357 }
358 return content, nil
359 }
360
361 func decodeZip(raw []byte) ([]byte, error) {
362 archive, err := zip.NewReader(bytes.NewReader(raw), int64(len(raw)))
363 if err != nil {
364 return nil, fmt.Errorf("failed to open zip payload: %w", err)
365 }
366 for _, f := range archive.File {
367 if f.FileInfo().IsDir() {
368 continue
369 }
370 rc, err := f.Open()
371 if err != nil {
372 return nil, fmt.Errorf("failed to open zip member %s: %w", f.Name, err)
373 }
374 defer rc.Close()
375 content, err := io.ReadAll(rc)
376 if err != nil {
377 return nil, fmt.Errorf("failed to read zip member %s: %w", f.Name, err)
378 }
379 return content, nil
380 }
381 return nil, fmt.Errorf("zip payload has no regular files")
382 }
383
384 func extractMMDBFromTar(raw []byte) ([]byte, error) {
385 tr := tar.NewReader(bytes.NewReader(raw))
386 for {
387 header, err := tr.Next()
388 if err == io.EOF {
389 return nil, errTarMMDBNotFound
390 }
391 if err != nil {
392 if !looksLikeTarPayload(raw) {
393 return nil, errTarMMDBNotFound
394 }
395 return nil, err
396 }
397 if header.Typeflag != tar.TypeReg || !strings.HasSuffix(strings.ToLower(header.Name), ".mmdb") {
398 continue
399 }
400 content, err := io.ReadAll(tr)
401 if err != nil {
402 return nil, fmt.Errorf("failed to read tar member %s: %w", header.Name, err)
403 }
404 return content, nil
405 }
406 }
407
408 func looksLikeTarPayload(raw []byte) bool {
409 return len(raw) >= 512 && bytes.Equal(raw[257:262], []byte("ustar"))
410 }