diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..7c4f64b --- /dev/null +++ b/.env.sample @@ -0,0 +1,4 @@ +MYSQL_HOST=provm.cl +MYSQL_DATABASE=incoviba +MYSQL_USER=remote_incoviba +MYSQL_PASSWORD=43918b603b84dd8bb29fd29a0ea21ba751de5dc90b26f36c diff --git a/.gitignore b/.gitignore index 6b7471f..aa7a937 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ **/vendor/ **/*.env **/*.lock +**/.idea/ +**/logs/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..786fbc3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM php:cli + +RUN docker-php-ext-install pdo_mysql + +WORKDIR /app diff --git a/Prod.Dockerfile b/Prod.Dockerfile new file mode 100644 index 0000000..70603ea --- /dev/null +++ b/Prod.Dockerfile @@ -0,0 +1,27 @@ +FROM php:cli AS build + +RUN apt-get update && apt-get install -yq --no-install-recommends git unzip && rm -r /var/lib/apt/lists/* +COPY --from=composer /usr/bin/composer /usr/bin/composer + +USER 1000 +WORKDIR /code +RUN git clone --branch master http://git.provm.cl/ProVM/remote_ip.git /code +RUN composer -d /code/app install + +FROM php:cli + +ENV MYSQL_HOST '' +ENV MYSQL_DATABASE '' +ENV MYSQL_USER '' +ENV MYSQL_PASSWORD '' + +RUN docker-php-ext-install pdo_mysql + +WORKDIR /app +COPY --from=build /code/app /app + +ENTRYPOINT [ "/app/bin/console" ] + +CMD [ "/app/bin/console", "watch" ] +#RUN apt-get update && apt-get install -yq --no-install-recommends cron && rm -r /var/lib/apt/lists/* && cp /app/crontab /var/spool/cron/crontabs/root +#CMD [ "cron", "-f", "-L", "15" ] diff --git a/app/bin/console b/app/bin/console new file mode 100755 index 0000000..b7e44fd --- /dev/null +++ b/app/bin/console @@ -0,0 +1,3 @@ +#!/bin/bash + +php /app/public/index.php "$@" diff --git a/app/common/Command/UpdateIp.php b/app/common/Command/UpdateIp.php new file mode 100644 index 0000000..2128483 --- /dev/null +++ b/app/common/Command/UpdateIp.php @@ -0,0 +1,42 @@ +title('Update IP'); + + try { + $io->info('Obtaining IP and updating database'); + $this->service->update(); + $io->info('Done'); + return Command::SUCCESS; + } catch (PDOException $e) { + $this->logger->warning($e); + return Command::FAILURE; + } catch (Exception $e) { + $this->logger->error($e); + return Command::FAILURE; + } + } +} diff --git a/app/common/Command/Watch.php b/app/common/Command/Watch.php new file mode 100644 index 0000000..397bcbf --- /dev/null +++ b/app/common/Command/Watch.php @@ -0,0 +1,50 @@ +title('Watch'); + + $period = new DateInterval($this->period); + $current = new DateTimeImmutable(); + $io->info('Starting'); + while(true) { + $now = new DateTimeImmutable(); + if ($now->diff($current) === $period) { + $io->info('Running Update'); + $this->runUpdate(); + $current = $now; + } + $wait = (new DateTimeImmutable((new DateTimeImmutable())->format('Y-m-d H:i:0')))->add(new DateInterval('PT1M')); + time_sleep_until($wait->getTimestamp()); + } + return Command::SUCCESS; + } + protected function runUpdate(): void + { + $command = "{$this->command} update"; + $this->logger->info("Running '{$command}'"); + shell_exec($command); + } +} diff --git a/app/common/Service/Connector.php b/app/common/Service/Connector.php new file mode 100644 index 0000000..7c8c6bc --- /dev/null +++ b/app/common/Service/Connector.php @@ -0,0 +1,41 @@ +connection)) { + $this->logger->debug('Connecting'); + $r = 0; + $exception = null; + while($r < $this->retries) { + try { + $dsn = "mysql:host={$this->host};dbname={$this->name}"; + $this->connection = new PDO($dsn, $this->username, $this->password); + return $this->connection; + } catch (PDOException $e) { + $this->logger->debug('Retrying'); + if ($exception !== null) { + $e = new PDOException($e->getMessage(), $e->getCode(), $exception); + } + $exception = $e; + } + $r ++; + } + throw $exception; + } + return $this->connection; + } +} diff --git a/app/common/Service/Ipify.php b/app/common/Service/Ipify.php new file mode 100644 index 0000000..2136521 --- /dev/null +++ b/app/common/Service/Ipify.php @@ -0,0 +1,28 @@ +logger->debug('Getting IP'); + $response = $this->client->get('?format=json'); + if (round($response->getStatusCode() / 100, 0) != 2) { + throw new Exception("Could not connect to '{$this->uri}'"); + } + $body = $response->getBody(); + $json = json_decode($body->getContents()); + if (!isset($json->ip)) { + throw new Exception('Missing `ip` in JSON response'); + } + $this->logger->debug("Current IP: {$json->ip}"); + return $json->ip; + } +} diff --git a/app/common/Service/Remote.php b/app/common/Service/Remote.php new file mode 100644 index 0000000..5a1131f --- /dev/null +++ b/app/common/Service/Remote.php @@ -0,0 +1,13 @@ +ipService->get(); + $this->dbService->update($ip); + } +} diff --git a/app/common/Service/Repository.php b/app/common/Service/Repository.php new file mode 100644 index 0000000..7300a46 --- /dev/null +++ b/app/common/Service/Repository.php @@ -0,0 +1,41 @@ +logger->debug('Updating Database'); + + $old_ip = $this->getOld(); + $this->logger->debug("Old IP: {$old_ip}"); + + if ($old_ip === $ip) { + $this->logger->debug('No change in IP'); + return; + } + + $this->doUpdate($ip); + $this->logger->debug('Updated IP'); + } + + protected function getOld(): string + { + $query = "SELECT `ip` FROM `{$this->table}` WHERE `host` = ?"; + $statement = $this->connector->connect()->prepare($query); + $statement->execute(['vialdelamaza']); + + return $statement->fetch()['ip']; + } + protected function doUpdate(string $ip): void + { + $query = "UPDATE `remote_ip` SET `ip` = ?, `updated` = CURRENT_TIMESTAMP() WHERE `host` = ?"; + $statement = $this->connector->connect()->prepare($query); + $statement->execute([$ip, 'vialdelamaza']); + } +} diff --git a/app/common/Wrapper/App.php b/app/common/Wrapper/App.php new file mode 100644 index 0000000..3c5107a --- /dev/null +++ b/app/common/Wrapper/App.php @@ -0,0 +1,19 @@ +container; + } + public function setContainer(ContainerInterface $container): App + { + $this->container = $container; + return $this; + } +} diff --git a/app/composer.json b/app/composer.json new file mode 100644 index 0000000..78aa08d --- /dev/null +++ b/app/composer.json @@ -0,0 +1,22 @@ +{ + "name": "provm/remote_ip", + "type": "project", + "require": { + "guzzlehttp/guzzle": "^7.7", + "monolog/monolog": "^3.3", + "php-di/php-di": "^7.0", + "symfony/console": "^6.3", + "thecodingmachine/safe": "^2.5" + }, + "require-dev": { + "phpunit/phpunit": "^10.2" + }, + "autoload": { + "psr-4": { + "ProVM\\": "common/" + } + }, + "config": { + "sort-packages": true + } +} diff --git a/app/crontab b/app/crontab new file mode 100644 index 0000000..388f26f --- /dev/null +++ b/app/crontab @@ -0,0 +1 @@ +*/20 * * * 1-5 /app/bin/console diff --git a/app/public/index.php b/app/public/index.php new file mode 100644 index 0000000..cd4984c --- /dev/null +++ b/app/public/index.php @@ -0,0 +1,13 @@ +run(); +} catch (Exception $e) { + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->warning($e); +} catch (Error $e) { + $app->getContainer()->get(Psr\Log\LoggerInterface::class)->error($e); +} diff --git a/app/setup/app.php b/app/setup/app.php new file mode 100644 index 0000000..b5df709 --- /dev/null +++ b/app/setup/app.php @@ -0,0 +1,40 @@ +isDir()) { + continue; + } + $builder->addDefinitions($file->getRealPath()); + } + } + $app = new App(); + $app->setContainer($builder->build()); + $folder = implode(DIRECTORY_SEPARATOR, [__DIR__, 'middlewares']); + if (file_exists($folder)) { + $files = new FilesystemIterator($folder); + foreach($files as $file) { + if ($file->isDir()) { + continue; + } + require_once $file->getRealPath(); + } + } + return $app; +} +return buildApp(); diff --git a/app/setup/composer.php b/app/setup/composer.php new file mode 100644 index 0000000..b451f96 --- /dev/null +++ b/app/setup/composer.php @@ -0,0 +1,6 @@ +add($app->getContainer()->get(ProVM\Command\Watch::class)); +$app->add($app->getContainer()->get(ProVM\Command\UpdateIp::class)); diff --git a/app/setup/middlewares/log.php b/app/setup/middlewares/log.php new file mode 100644 index 0000000..b81e521 --- /dev/null +++ b/app/setup/middlewares/log.php @@ -0,0 +1,2 @@ +getContainer()->get(Psr\Log\LoggerInterface::class)); diff --git a/app/setup/settings/01_env.php b/app/setup/settings/01_env.php new file mode 100644 index 0000000..6ceb90b --- /dev/null +++ b/app/setup/settings/01_env.php @@ -0,0 +1,22 @@ + function() { + return new DI\Container([ + 'host' => $_ENV['MYSQL_HOST'], + 'name' => $_ENV['MYSQL_DATABASE'], + 'user' => function() { + return new DI\Container([ + 'name' => $_ENV['MYSQL_USER'], + 'password' => $_ENV['MYSQL_PASSWORD'] + ]); + }, + 'table' => $_ENV['MYSQL_TABLE'] ?? 'remote_ip', + ]); + }, + 'error_logs_file' => $_ENV['ERROR_LOGS_FILE'] ?? '/logs/remote.error.log', + 'debug_logs_file' => $_ENV['DEBUG_LOGS_FILE'] ?? '/logs/remote.debug.log', + 'uri' => $_ENV['IPIFY_URI'] ?? 'https://api64.ipify.org', + 'command' => 'php /app/public/index.php', + 'period' => $_ENV['WATCH_PERIOD'] ?? 'PT20M', + 'retries' => $_ENV['CONNECTION_RETRIES'] ?? 5 +]; diff --git a/app/setup/setups/commands.php b/app/setup/setups/commands.php new file mode 100644 index 0000000..d34ca97 --- /dev/null +++ b/app/setup/setups/commands.php @@ -0,0 +1,12 @@ + function(ContainerInterface $container) { + return new ProVM\Command\Watch( + $container->get('command'), + $container->get('period'), + $container->get(Psr\Log\LoggerInterface::class) + ); + }, +]; diff --git a/app/setup/setups/services.php b/app/setup/setups/services.php new file mode 100644 index 0000000..d4e86a0 --- /dev/null +++ b/app/setup/setups/services.php @@ -0,0 +1,53 @@ + function(ContainerInterface $container) { + return new Monolog\Logger('file', [ + new Monolog\Handler\FilterHandler( + new Monolog\Handler\RotatingFileHandler($container->get('debug_logs_file')), + Monolog\Level::Debug, + Monolog\Level::Warning + ), + new Monolog\Handler\FilterHandler( + new Monolog\Handler\RotatingFileHandler($container->get('error_logs_file')), + Monolog\Level::Error + ) + ], [ + new Monolog\Processor\PsrLogMessageProcessor(), + new Monolog\Processor\IntrospectionProcessor(), + new Monolog\Processor\MemoryUsageProcessor(), + new Monolog\Processor\MemoryPeakUsageProcessor(), + ]); + }, + Psr\Http\Client\ClientInterface::class => function(ContainerInterface $container) { + return new GuzzleHttp\Client([ + 'base_uri' => $container->get('uri') + ]); + }, + ProVM\Service\Ipify::class => function(ContainerInterface $container) { + return new ProVM\Service\Ipify( + $container->get(Psr\Http\Client\ClientInterface::class), + $container->get('uri'), + $container->get(Psr\Log\LoggerInterface::class) + ); + }, + ProVM\Service\Connector::class => function(ContainerInterface $container) { + $database = $container->get('database'); + return new ProVM\Service\Connector( + $database->get('host'), + $database->get('name'), + $database->get('user')->get('name'), + $database->get('user')->get('password'), + $container->get('retries'), + $container->get(Psr\Log\LoggerInterface::class) + ); + }, + ProVM\Service\Repository::class => function(ContainerInterface $container) { + return new ProVM\Service\Repository( + $container->get(ProVM\Service\Connector::class), + $container->get('database')->get('table'), + $container->get(Psr\Log\LoggerInterface::class) + ); + } +]; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f6d5a14 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + remote_ip: + container_name: remote_ip + build: . + restart: unless-stopped + env_file: .env + volumes: + - ./app:/app + - ./logs:/logs +# - ./app/crontab:/var/spool/cron/crontabs/root