From b79da8b937e1b486116062e7d6258ba8fd8da52e Mon Sep 17 00:00:00 2001 From: Nicolas Debrigode Date: Mon, 5 Feb 2024 19:23:59 +0100 Subject: [PATCH] save --- asset/.gitignore | 1 + asstats.yml | 6 +- composer.json | 2 + config/packages/translation.yaml | 7 ++ phpstan.neon | 1 + src/Application/ConfigApplication.php | 48 +++++++++ src/Controller/AssetController.php | 70 ++++++++++++ src/Controller/BaseController.php | 4 + src/Controller/HistoryController.php | 4 + src/Controller/IXStatsController.php | 11 +- src/Controller/IndexController.php | 4 +- src/Form/SearchASSetForm.php | 41 ++++++++ src/Repository/AsSetRepository.php | 123 ++++++++++++++++++++++ src/Repository/DbAsInfoRepository.php | 13 ++- src/Util/Theme/Icon.php | 1 + symfony.lock | 13 +++ templates/base/_menu.html.twig | 11 ++ templates/core/card_top.html.twig | 123 +++++++++++----------- templates/core/legend.html.twig | 8 +- templates/pages/asset/_search.html.twig | 15 +++ templates/pages/asset/index.html.twig | 22 ++++ templates/pages/asset/show.html.twig | 60 +++++++++++ templates/pages/history/_search.html.twig | 2 +- templates/pages/index.html.twig | 4 +- translations/.gitignore | 0 translations/messages+intl-icu.en.yaml | 6 ++ translations/messages+intl-icu.fr.yaml | 6 ++ 27 files changed, 528 insertions(+), 78 deletions(-) create mode 100644 asset/.gitignore create mode 100644 config/packages/translation.yaml create mode 100644 src/Controller/AssetController.php create mode 100644 src/Form/SearchASSetForm.php create mode 100644 src/Repository/AsSetRepository.php create mode 100644 templates/pages/asset/_search.html.twig create mode 100644 templates/pages/asset/index.html.twig create mode 100644 templates/pages/asset/show.html.twig create mode 100644 translations/.gitignore create mode 100644 translations/messages+intl-icu.en.yaml create mode 100644 translations/messages+intl-icu.fr.yaml diff --git a/asset/.gitignore b/asset/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/asset/.gitignore @@ -0,0 +1 @@ +* diff --git a/asstats.yml b/asstats.yml index 9009094..404d1c0 100644 --- a/asstats.yml +++ b/asstats.yml @@ -4,6 +4,10 @@ config: knownlinksfile: '/home/nidebr/data/DEV/asstats/conf/knownlinks' asinfofile: 'ressources/asinfo.db' myasn: 34863 + asset: + whois: '/usr/bin/whois' + assetpath: 'asset' + asset_cache_life: 604800 # 604800 seconds = 7 days graph: showv6: true outispositive: false @@ -21,7 +25,7 @@ config: top_graph_width: 600 top_graph_height: 220 linksusage: - top: 3 + top: 10 color: - A6CEE3 - 1F78B4 diff --git a/composer.json b/composer.json index c263c68..d9861de 100644 --- a/composer.json +++ b/composer.json @@ -18,10 +18,12 @@ "symfony/http-client": "7.0.*", "symfony/process": "7.0.*", "symfony/runtime": "7.0.*", + "symfony/translation": "7.0.*", "symfony/twig-bundle": "7.0.*", "symfony/validator": "7.0.*", "symfony/yaml": "7.0.*", "twig/extra-bundle": "^3.0", + "twig/intl-extra": "^3.8", "twig/string-extra": "^3.0", "twig/twig": "^3.0" }, diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml new file mode 100644 index 0000000..86e19f4 --- /dev/null +++ b/config/packages/translation.yaml @@ -0,0 +1,7 @@ +framework: + default_locale: en + translator: + default_path: '%kernel.project_dir%/translations' + fallbacks: + - en + - fr diff --git a/phpstan.neon b/phpstan.neon index c0344d3..0960eef 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,3 +16,4 @@ parameters: ignoreErrors: - '#Only booleans are allowed#' - '#Cannot cast mixed to int.#' + - '#Cannot cast mixed to string.#' diff --git a/src/Application/ConfigApplication.php b/src/Application/ConfigApplication.php index fece256..159c9f4 100644 --- a/src/Application/ConfigApplication.php +++ b/src/Application/ConfigApplication.php @@ -66,6 +66,15 @@ class ConfigApplication return self::getAsStatsConfig()['top']; } + public static function getLangage(): string + { + if (false === \array_key_exists('langage', self::getAsStatsConfig())) { + return 'en'; + } + + return self::getAsStatsConfig()['langage']; + } + public static function getAsStatsConfigMyAsn(): ?int { if (false === \array_key_exists('myasn', self::getAsStatsConfig())) { @@ -174,4 +183,43 @@ class ConfigApplication return self::getAsStatsConfig()['linksusage']['top']; } + + public static function getAsStatsAssetPath(): string + { + if (false === \array_key_exists('asset', self::getAsStatsConfig())) { + throw new ConfigErrorException('Unable to found config.asset variable'); + } + + if (false === \array_key_exists('assetpath', self::getAsStatsConfig()['asset'])) { + throw new ConfigErrorException('Unable to found config.asset.assetpath variable'); + } + + return self::getRootPathApp().self::getAsStatsConfig()['asset']['assetpath']; + } + + public static function getAsStatsAssetCacheLife(): int + { + if (false === \array_key_exists('asset', self::getAsStatsConfig())) { + throw new ConfigErrorException('Unable to found config.asset variable'); + } + + if (false === \array_key_exists('asset_cache_life', self::getAsStatsConfig()['asset'])) { + throw new ConfigErrorException('Unable to found config.asset.asset_cache_life variable'); + } + + return self::getAsStatsConfig()['asset']['asset_cache_life']; + } + + public static function getAsStatsAssetWhois(): string + { + if (false === \array_key_exists('asset', self::getAsStatsConfig())) { + throw new ConfigErrorException('Unable to found config.asset variable'); + } + + if (false === \array_key_exists('whois', self::getAsStatsConfig()['asset'])) { + throw new ConfigErrorException('Unable to found config.asset.whois variable'); + } + + return self::getAsStatsConfig()['asset']['whois']; + } } diff --git a/src/Controller/AssetController.php b/src/Controller/AssetController.php new file mode 100644 index 0000000..731e723 --- /dev/null +++ b/src/Controller/AssetController.php @@ -0,0 +1,70 @@ +base_data['content_wrapper']['titre'] = 'History for AS-SET'; + + $form = $this->createForm(SearchASSetForm::class); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->data['asset']['name'] = \strtoupper((string) $form->get('asset')->getData()); + $this->data['asset']['whois'] = $asSetRepository->getAsset($this->data['asset']['name']); + + if (\array_key_exists('as_num', $this->data['asset']['whois'])) { + $this->data['asset']['data'] = $asDataRepository::get(200, '', [], $this->data['asset']['whois']['as_num']); + + $this->data['selectedLinks'] = []; + $this->data['graph_size'] = [ + 'width' => $this->configApplication::getAsStatsConfigGraph()['top_graph_width'], + 'height' => $this->configApplication::getAsStatsConfigGraph()['top_graph_height'], + ]; + + $this->data = \array_merge($this->data, $getStartEndGraph->get()); + + return $this->render('pages/asset/show.html.twig', [ + 'base_data' => $this->base_data, + 'data' => $this->data, + 'form' => $form->createView(), + 'knownlinks' => KnowlinksRepository::get(), + ]); + } + + $this->addFlash('info', \sprintf('Unable to find information about asset %s', $this->data['asset']['name'])); + } + + return $this->render('pages/asset/index.html.twig', [ + 'base_data' => $this->base_data, + 'data' => $this->data, + 'form' => $form->createView(), + ]); + } +} diff --git a/src/Controller/BaseController.php b/src/Controller/BaseController.php index ee9457c..62f25a8 100644 --- a/src/Controller/BaseController.php +++ b/src/Controller/BaseController.php @@ -10,6 +10,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormView; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Translation\LocaleSwitcher; abstract class BaseController extends AbstractController { @@ -19,9 +20,12 @@ abstract class BaseController extends AbstractController public function __construct( ConfigApplication $configApplication, RequestStack $requestStack, + LocaleSwitcher $localeSwitcher, ) { $this->configApplication = $configApplication; $this->base_data = self::getBaseData($requestStack); + + $localeSwitcher->setLocale($this->configApplication::getLangage()); } private function getBaseData( diff --git a/src/Controller/HistoryController.php b/src/Controller/HistoryController.php index d99d6ff..656e5fa 100644 --- a/src/Controller/HistoryController.php +++ b/src/Controller/HistoryController.php @@ -56,6 +56,10 @@ class HistoryController extends BaseController $this->data['as'] = $as; $this->data['asinfo'] = $asInfoRepository->getAsInfo($this->data['as']); + if ('UNKNOWN' === $this->data['asinfo']['name']) { + $this->addFlash('error', \sprintf('Unable to find AS%s in database ASInfo, please update.', $this->data['as'])); + } + $this->base_data['content_wrapper']['titre'] = \sprintf( 'History for AS%s', $this->data['as'], diff --git a/src/Controller/IXStatsController.php b/src/Controller/IXStatsController.php index ca02eb0..ffb9396 100644 --- a/src/Controller/IXStatsController.php +++ b/src/Controller/IXStatsController.php @@ -14,6 +14,7 @@ use App\Repository\KnowlinksRepository; use App\Repository\PeeringDBRepository; use App\Util\Annotation\Menu; use App\Util\GetJsonParameters; +use App\Util\GetStartEndGraph; use Doctrine\DBAL\Exception; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -43,6 +44,7 @@ class IXStatsController extends BaseController Request $request, PeeringDBRepository $peeringDBRepository, GetAsDataRepository $asDataRepository, + GetStartEndGraph $getStartEndGraph, ): Response { $this->base_data['content_wrapper']['titre'] = 'My IX Stats'; @@ -66,14 +68,14 @@ class IXStatsController extends BaseController $peeringDBRepository->getIXMembers((int) $form->get('myix')->getData()), ); - $this->data['start'] = time() - 24 * 3600; - $this->data['end'] = time(); $this->data['graph_size'] = [ 'width' => $this->configApplication::getAsStatsConfigGraph()['top_graph_width'], 'height' => $this->configApplication::getAsStatsConfigGraph()['top_graph_height'], ]; $this->data['selectedLinks'] = []; + $this->data = \array_merge($this->data, $getStartEndGraph->get()); + return $this->render('pages/ix/my_ix/show.html.twig', [ 'base_data' => $this->base_data, 'data' => $this->data, @@ -103,6 +105,7 @@ class IXStatsController extends BaseController Request $request, PeeringDBRepository $peeringDBRepository, GetAsDataRepository $asDataRepository, + GetStartEndGraph $getStartEndGraph, ): Response { $this->base_data['content_wrapper']['titre'] = 'Search IX Stats'; @@ -125,14 +128,14 @@ class IXStatsController extends BaseController $peeringDBRepository->getIXMembers((int) $form->get('ix_hidden')->getData()), ); - $this->data['start'] = time() - 24 * 3600; - $this->data['end'] = time(); $this->data['graph_size'] = [ 'width' => $this->configApplication::getAsStatsConfigGraph()['top_graph_width'], 'height' => $this->configApplication::getAsStatsConfigGraph()['top_graph_height'], ]; $this->data['selectedLinks'] = []; + $this->data = \array_merge($this->data, $getStartEndGraph->get()); + return $this->render('pages/ix/search_ix/show.html.twig', [ 'base_data' => $this->base_data, 'data' => $this->data, diff --git a/src/Controller/IndexController.php b/src/Controller/IndexController.php index be341c4..eb037e5 100644 --- a/src/Controller/IndexController.php +++ b/src/Controller/IndexController.php @@ -16,6 +16,7 @@ use Doctrine\DBAL\Exception; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Contracts\Translation\TranslatorInterface; #[Menu('top_as')] class IndexController extends BaseController @@ -37,11 +38,12 @@ class IndexController extends BaseController Request $request, GetAsDataRepository $asDataRepository, GetStartEndGraph $getStartEndGraph, + TranslatorInterface $translator, ): Response { $this->base_data['content_wrapper']['titre'] = \sprintf( 'Top %s (%s)', $this->base_data['top'], - '24 hours' + $translator->trans('hour', ['num_hours' => 24]), ); $form = $this->createForm(LegendForm::class); diff --git a/src/Form/SearchASSetForm.php b/src/Form/SearchASSetForm.php new file mode 100644 index 0000000..fef1ffe --- /dev/null +++ b/src/Form/SearchASSetForm.php @@ -0,0 +1,41 @@ + $options + */ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->setMethod('get') + ->add('asset', TextType::class, [ + 'attr' => [ + 'placeholder' => 'Search AS-SET', + ], + 'translation_domain' => false, + 'label' => false, + ]); + } + + public function getBlockPrefix(): string + { + return ''; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'csrf_protection' => false, + ]); + } +} diff --git a/src/Repository/AsSetRepository.php b/src/Repository/AsSetRepository.php new file mode 100644 index 0000000..f131589 --- /dev/null +++ b/src/Repository/AsSetRepository.php @@ -0,0 +1,123 @@ +config = $config; + } + + /** + * @throws ConfigErrorException + */ + public function getAsset(string $asset): array + { + /* sanity check */ + if (!\preg_match('/^[a-zA-Z0-9:_-]+$/', $asset)) { + return []; + } + + $cache = false; + + $assetfile = \sprintf('%s/%s.txt', $this->config::getAsStatsAssetPath(), $asset); + + if (\file_exists($assetfile)) { + $filemtime = \filemtime($assetfile); + if (!$filemtime || (\time() - $filemtime >= $this->config::getAsStatsAssetCacheLife())) { + $list = $this->getWhois($asset); + } else { + $list = $this->readCacheFile($asset); + $cache = true; + if ([] === $list) { + $list = $this->getWhois($asset); + } + } + } else { + $list = $this->getWhois($asset); + } + + return \array_merge(['cache' => $cache], $list); + } + + /** + * @throws ConfigErrorException + */ + private function getWhois(string $asset): array + { + $process = new Process([$this->config::getAsStatsAssetWhois(), '-h', 'whois.radb.net', \sprintf('!i%s', $asset)]); + $process->run(); + + if (!$process->isSuccessful()) { + throw new ProcessFailedException($process); + } + + $return_list = \explode(' ', \trim(\str_replace(PHP_EOL, ' ', $process->getOutput()))); + + /* write cache file */ + $this->writeCacheFile($asset, $process->getOutput()); + + return $this->parseOtherAsset($this->parseData($return_list)); + } + + private function parseData(array $asnlist): array + { + $return = []; + foreach ($asnlist as $asn) { + if (\str_starts_with($asn, 'AS')) { + $return[] = $asn; + } + } + + return $return; + } + + private function writeCacheFile(string $asset, string $asnlist): void + { + if ('' !== $asset && '0' !== $asset) { + \file_put_contents(\sprintf('%s/%s.txt', $this->config::getAsStatsAssetPath(), $asset), $asnlist); + } + } + + private function readCacheFile(string $asset): array + { + $return = []; + if ('' !== $asset && '0' !== $asset) { + $input = \file_get_contents(\sprintf('%s/%s.txt', $this->config::getAsStatsAssetPath(), $asset)); + + if (!$input) { + return []; + } + + $return = \explode(' ', \trim(\str_replace(PHP_EOL, ' ', $input))); + } + + return $this->parseOtherAsset($this->parseData($return)); + } + + private function parseOtherAsset(array $aslist): array + { + $return = []; + + foreach ($aslist as $as) { + $as_tmp = \substr($as, 2); + if (\is_numeric($as_tmp)) { + $return['as_num'][] = $as_tmp; + } else { + $return['as_other'][] = $as; + } + } + + return $return; + } +} diff --git a/src/Repository/DbAsInfoRepository.php b/src/Repository/DbAsInfoRepository.php index 9d89cbb..a84adc5 100644 --- a/src/Repository/DbAsInfoRepository.php +++ b/src/Repository/DbAsInfoRepository.php @@ -87,12 +87,23 @@ class DbAsInfoRepository public function getAsInfo(int $asn): array { try { - return (array) $this->cnx->createQueryBuilder() + $return = $this->cnx->createQueryBuilder() ->select('*') ->from('asinfo') ->where('asn = :asn') ->setParameter('asn', $asn) ->fetchAssociative(); + + if (false === $return) { + $return = [ + 'asn' => $asn, + 'country' => 'EU', + 'name' => 'UNKNOWN', + 'description' => 'UNKNOWN', + ]; + } + + return (array) $return; } catch (Exception) { throw new DbErrorException(\sprintf('Problem with ASInfo DB files %s', $this->dbname)); } diff --git a/src/Util/Theme/Icon.php b/src/Util/Theme/Icon.php index 70c63a4..0f9ad1f 100644 --- a/src/Util/Theme/Icon.php +++ b/src/Util/Theme/Icon.php @@ -11,6 +11,7 @@ enum Icon: string case menu_view = 'ti-chart-histogram icon'; case menu_ix = 'ti-list-details icon'; case menu_linksusage = 'ti-package icon'; + case menu_asset = 'ti-file-neutral icon'; // Form case filter = 'ti-filter'; diff --git a/symfony.lock b/symfony.lock index 0b6a4fc..4f3ceee 100644 --- a/symfony.lock +++ b/symfony.lock @@ -106,6 +106,19 @@ "config/routes.yaml" ] }, + "symfony/translation": { + "version": "7.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.3", + "ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b" + }, + "files": [ + "config/packages/translation.yaml", + "translations/.gitignore" + ] + }, "symfony/twig-bundle": { "version": "7.0", "recipe": { diff --git a/templates/base/_menu.html.twig b/templates/base/_menu.html.twig index 2f32964..73a2169 100644 --- a/templates/base/_menu.html.twig +++ b/templates/base/_menu.html.twig @@ -46,6 +46,17 @@ + +