Skip to content

Commit

Permalink
Replace tronovav/geoip2-update (#188)
Browse files Browse the repository at this point in the history
Replaces `tronovav/geoip2-update` with custom update logic in `Database/Update`
  • Loading branch information
VerifiedJoseph authored Oct 2, 2023
1 parent f47fcd0 commit d7e3aca
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 96 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ composer test
- PHP
- [`geoip2/geoip2`](https://github.com/maxmind/GeoIP2-php)
- [`tronovav/geoip2-update`](https://github.com/tronovav/geoip2-update)
- JavaScript
- [Chart.js](https://github.com/chartjs/Chart.js/)
- [Spacetime](https://github.com/spencermountain/spacetime)
Expand Down
14 changes: 10 additions & 4 deletions backend/include/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class Config

/** @var array<string, string> $defaultGeoLite2Paths Default GeoLite2 database paths */
private array $defaultGeoLite2Paths = [
'GeoLite2-ASN' => 'data/geoip2/GeoLite2-ASN/GeoLite2-ASN.mmdb',
'GeoLite2-Country' => 'data/geoip2/GeoLite2-Country/GeoLite2-Country.mmdb'
'GeoLite2-ASN' => 'data/geoip2/GeoLite2-ASN.mmdb',
'GeoLite2-Country' => 'data/geoip2/GeoLite2-Country.mmdb'
];

/**
Expand Down Expand Up @@ -46,6 +46,14 @@ public function getVersion(): string
return (string) constant('VERSION');
}

public function getUseragent(): string
{
return sprintf(
'Intruder Alert/%s (+https://github.com/VerifiedJoseph/intruder-alert)',
$this->getVersion()
);
}

public function getChartsStatus(): bool
{
if ($this->getEnv('DASH_CHARTS') === 'false') {
Expand Down Expand Up @@ -177,8 +185,6 @@ private function checkDataFolder(): void
{
$folderPath = $this->getPath('data');

var_dump($folderPath);

if (file_exists($folderPath) === false) {
if (mkdir($folderPath, 0660) === false) {
throw new ConfigException('Failed to create data folder');
Expand Down
224 changes: 189 additions & 35 deletions backend/include/Database/Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
use IntruderAlert\Helper\File;
use IntruderAlert\Helper\Output;
use IntruderAlert\Exception\AppException;
use Exception;

class Update
{
private Config $config;

/** @var array<int, string> $editions GeoLite2 database editions */
private array $editions = ['GeoLite2-ASN', 'GeoLite2-Country'];
private string $folder = 'data/geoip2';

private string $destinationDir = 'data/geoip2';
private string $apiUrl = 'https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=%s';

public function __construct(Config $config)
{
Expand All @@ -24,53 +24,52 @@ public function __construct(Config $config)
public function run(): void
{
if ($this->config->getMaxMindLicenseKey() !== '') {
$path = $this->config->getPath($this->destinationDir);
$folder = $this->config->getPath($this->folder);

if (File::exists($path) === false) {
mkdir($path);
if (File::exists($folder) === false) {
mkdir($folder);
}

if ($this->updateRequired() === true) {
Output::text('Updating Geoip2 databases', log: true);
try {
foreach ($this->getDatabasePaths() as $edition => $path) {
if ($this->updateRequired($path) === true) {
Output::text('Updating Geoip2 database: ' . $edition, log: true);

$client = new \tronovav\GeoIP2Update\Client([
'license_key' => $this->config->getMaxMindLicenseKey(),
'dir' => $path,
'editions' => $this->editions,
]);
$archivePath = $path . '.tar.gz';
$checksum = $this->downloadChecksum($edition);

$client->run();
$this->downloadDatabase($edition, $archivePath);
$this->checkIntegrity($checksum['hash'], $archivePath);
$this->extractDatabase($archivePath, $edition, $path);

foreach ($client->updated() as $message) {
Output::text($message, log: true);
}

if ($client->errors() != []) {
foreach ($client->errors() as $message) {
Output::text($message, log: true);
Output::text('Updated Geoip2 database: ' . $edition, log: true);
}

throw new AppException('Failed to update Geoip2 databases');
}
} catch (\Exception $err) {
throw new AppException(sprintf(
'Update database failed. %s',
$err->getMessage()
));
}
}
}

/**
* Check if a database update is required
*
* @param string $path Database path
* @return boolean
*/
private function updateRequired(): bool
private function updateRequired(string $path): bool
{
foreach ($this->getDatabasePaths() as $path) {
if (File::exists($path) === false) {
return true;
}
if (File::exists($path) === false) {
return true;
}

$fileModTime = (int) filemtime($path);
$fileModTime = (int) filemtime($path);

if ($this->calculateTimeDiff($fileModTime) >= 86400) {
return true;
}
if ($this->calculateTimeDiff($fileModTime) >= 86400) {
return true;
}

return false;
Expand All @@ -93,13 +92,168 @@ private function calculateTimeDiff(int $lastMod): int
/**
* Get GeoIP2 database paths
*
* @return array<int, string>
* @return array<string, string>
*/
private function getDatabasePaths(): array
{
return [
$this->config->getAsnDatabasePath(),
$this->config->getCountryDatabasePath()
'GeoLite2-ASN' => $this->config->getAsnDatabasePath(),
'GeoLite2-Country' => $this->config->getCountryDatabasePath()
];
}

/**
* Download checksum for a database
*
* @param string $edition Database edition
* @return array<string, string> Database checksum and filename
*/
private function downloadChecksum(string $edition): array
{
$regex = '/^([A-Za-z0-9]+)\ \ (GeoLite2-(?:[A-Za-z]+)_(?:[0-9]{8})\.tar\.gz)$/';
$url = sprintf(
$this->apiUrl,
$edition,
$this->config->getMaxMindLicenseKey(),
'tar.gz.sha256'
);

$data = $this->fetch($url);

if (preg_match($regex, $data, $matches) !== 1) {
throw new Exception('Failed extract checksum from downloaded file.');
}

return [
'hash' => $matches[1],
'filename' => $matches[2]
];
}

/**
* Download a database
*
* @param string $edition Database edition
* @param string $path Path to save downloaded archive
*/
private function downloadDatabase(string $edition, string $path): void
{
$url = sprintf(
$this->apiUrl,
$edition,
$this->config->getMaxMindLicenseKey(),
'tar.gz'
);

$data = $this->fetch($url);

File::write($path, $data);
}

/**
* Check integrity of the archive
*
* @param string $checksum
* @param string $filepath
*
* @throws Exception if integrity check failed
*/
private function checkIntegrity(string $checksum, string $filepath): void
{
$fileHash = hash_file('sha256', $filepath);

if ($fileHash !== $checksum) {
throw new Exception('Integrity check failed:' . $filepath);
}
}

/**
* Extract database from archive
*
* @param string $archivePath Path of archive file
* @param string $edition Database edition
* @param string $path Path to save the database
*/
private function extractDatabase(string $archivePath, string $edition, string $path): void
{
$phar = new \PharData($archivePath);
$phar->extractTo($this->folder, null, true);

$regex = sprintf('/^%s_([0-9]{8})$/', $edition);
$directories = new \DirectoryIterator($this->folder);

foreach ($directories as $directory) {
if ($directory->isDir() && preg_match($regex, $directory->getBasename())) {
$filepath = $directory->getPathname() . DIRECTORY_SEPARATOR . $edition . '.mmdb';

rename($filepath, $path);

$this->removeDir($directory->getPathname());
}
}

// Remove archive
unlink($archivePath);
}

/**
* Fetch URL
*
* @param string $url URL
* @return string
*
* @throws Exception if an CURL error occurred
* @throws Exception if request returned non-200 status code
*/
private function fetch(string $url): string
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => $this->config->getUseragent()
]);

$response = curl_exec($ch);
$error = curl_error($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

if ($error !== '') {
throw new Exception($error);
}

if ($statusCode !== 200) {
throw new Exception(sprintf(
'Request failed. Returned HTTP %s',
$statusCode
));
}

return (string) $response;
}

/**
* Remove directory and its contents
*
* @param string $path Directory path
*/
private function removeDir($path): void
{
if (is_dir($path) === true) {
$directory = new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS);
$items = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::CHILD_FIRST);

foreach ($items as $item) {
if ($item->isDir() === true) {
rmdir($item);
} else {
unlink($item);
}
}

rmdir($path);
}
}
}
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"require": {
"geoip2/geoip2": "^2.13",
"tronovav/geoip2-update": "^2.1"
"geoip2/geoip2": "^2.13"
},
"require-dev": {
"phpstan/phpstan": "^1.10",
Expand Down
55 changes: 1 addition & 54 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d7e3aca

Please sign in to comment.