| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- <?php
- declare(strict_types=1);
- namespace App\Infrastructure\Enrichment\Downloaders;
- use GuzzleHttp\ClientInterface;
- use GuzzleHttp\Exception\GuzzleException;
- use PharData;
- use RecursiveDirectoryIterator;
- use RecursiveIteratorIterator;
- use Throwable;
- /**
- * Opt-in — requires MAXMIND_LICENSE_KEY. Downloads the permalink
- * tarball and verifies SHA-256 against the published companion file.
- *
- * The MaxMind tarball nests one directory like
- * `GeoLite2-Country_20260427/GeoLite2-Country.mmdb`. We extract and
- * walk to find the .mmdb leaf.
- *
- * The license key is never logged: error messages substitute a
- * fixed-shape URL token instead of the real query string.
- */
- final class MaxMindDownloader implements GeoIpDownloader
- {
- private const ENDPOINT = 'https://download.maxmind.com/app/geoip_download';
- public function __construct(
- private readonly ClientInterface $http,
- private readonly string $licenseKey,
- ) {
- }
- public function name(): string
- {
- return 'maxmind';
- }
- public function requiresCredential(): bool
- {
- return true;
- }
- public function hasCredential(): bool
- {
- return $this->licenseKey !== '';
- }
- public function download(string $tempDir): array
- {
- $countryPath = $this->fetchEdition($tempDir, 'GeoLite2-Country', 'country');
- MmdbVerifier::assertCountry($countryPath);
- $asnPath = $this->fetchEdition($tempDir, 'GeoLite2-ASN', 'asn');
- MmdbVerifier::assertAsn($asnPath);
- return ['country' => $countryPath, 'asn' => $asnPath];
- }
- private function fetchEdition(string $tempDir, string $edition, string $kind): string
- {
- $tarPath = $tempDir . '/' . $kind . '.tar.gz';
- $sha256Path = $tempDir . '/' . $kind . '.tar.gz.sha256';
- $this->fetchTo($edition, 'tar.gz', $tarPath);
- $this->fetchTo($edition, 'tar.gz.sha256', $sha256Path);
- $expected = trim((string) file_get_contents($sha256Path));
- // The .sha256 file is "<hash> <filename>"; take the leading hash.
- if (preg_match('/^([0-9a-f]{64})/', $expected, $m) !== 1) {
- throw new DownloaderException(sprintf('maxmind %s sha256 file unparseable', $kind));
- }
- $expectedHash = $m[1];
- $actualHash = hash_file('sha256', $tarPath);
- if ($actualHash !== $expectedHash) {
- throw new DownloaderException(sprintf(
- 'maxmind %s sha256 mismatch (expected %s, got %s)',
- $kind,
- $expectedHash,
- (string) $actualHash,
- ));
- }
- $extractDir = $tempDir . '/' . $kind . '-extract';
- if (!mkdir($extractDir) && !is_dir($extractDir)) {
- throw new DownloaderException(sprintf('cannot mkdir %s', $extractDir));
- }
- try {
- $phar = new PharData($tarPath);
- $phar->extractTo($extractDir, null, true);
- } catch (Throwable $e) {
- throw new DownloaderException(
- sprintf('maxmind %s extract failed: %s', $kind, $e->getMessage()),
- 0,
- $e,
- );
- }
- $mmdbPath = $this->findMmdb($extractDir);
- if ($mmdbPath === null) {
- throw new DownloaderException(sprintf('maxmind %s tarball had no .mmdb', $kind));
- }
- $finalPath = $tempDir . '/' . $kind . '.mmdb';
- if (!@rename($mmdbPath, $finalPath)) {
- throw new DownloaderException(sprintf('rename %s -> %s failed', $mmdbPath, $finalPath));
- }
- return $finalPath;
- }
- private function fetchTo(string $edition, string $suffix, string $sink): void
- {
- $url = sprintf('%s?edition_id=%s&license_key=%s&suffix=%s', self::ENDPOINT, $edition, $this->licenseKey, $suffix);
- try {
- $this->http->request('GET', $url, ['sink' => $sink]);
- } catch (GuzzleException $e) {
- // Sanitise the URL in the surfaced message.
- $sanitised = sprintf('%s?edition_id=%s&license_key=***&suffix=%s', self::ENDPOINT, $edition, $suffix);
- throw new DownloaderException(sprintf('GET %s failed', $sanitised), 0, $e);
- }
- }
- private function findMmdb(string $dir): ?string
- {
- $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS));
- foreach ($it as $file) {
- if ($file->isFile() && str_ends_with($file->getFilename(), '.mmdb')) {
- return $file->getPathname();
- }
- }
- return null;
- }
- }
|