diff --git a/.gitignore b/.gitignore
index 5601611..166758b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@ vendor
.phpunit.result.cache
.phpunit.cache
.vscode/launch.json
+composer.lock
build
diff --git a/README.md b/README.md
index 48b858a..2835451 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,13 @@ composer req ldc/web-admin-secure-laravel:1.0.1
.env.example :
```dotenv
-LDC_WEB_API_KEY="MY_KEY"
+# ADMIN SECURE
+# - API ENDPOINT (Optional)
+LDC_WEB_API_ENDPOINT="https://ldc-web.ldc.fr/api/ldc-networks"
+# - API KEY
+LDC_WEB_API_KEY="CE8S52Qq7stf5k8frF4T"
+# - LIST SEPARATED WITH ',' (Optional)
+LDC_WEB_CIDR_MANUAL_ALLOWED_LIST=""
```
README.md :
diff --git a/changelog.md b/changelog.md
index c23f50e..0fba420 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,10 @@
# Changelog
+
+## **Version 2.0.0**
+ *Changed*
+ - Compatible only with **php 8.2**
+ - Allow connections from private ip ranges
+ - Allow setting Cidr list into config
## **Version 1.0.1**
*Changed*
- Run scheduled only on production envirronement
diff --git a/composer.json b/composer.json
index f599007..7286a3e 100644
--- a/composer.json
+++ b/composer.json
@@ -10,16 +10,17 @@
}
],
"require": {
- "php": "~7.3|7.4|~8",
- "illuminate/support": "~8|~9",
- "laravel/framework": "~8|~9"
+ "php": ">=8.2",
+ "illuminate/support": "~10",
+ "laravel/framework": "~10",
+ "dxw/cidr": "^3.1"
},
"require-dev": {
- "phpunit/phpunit": "~9.0",
- "squizlabs/php_codesniffer": "^3.5",
- "orchestra/testbench": "^6.24",
- "slevomat/coding-standard": "^7.0",
- "phpunit/php-code-coverage": "^9.2"
+ "phpunit/phpunit": "^9",
+ "squizlabs/php_codesniffer": "^3",
+ "orchestra/testbench": "^8",
+ "slevomat/coding-standard": "^7",
+ "phpunit/php-code-coverage": "^9"
},
"autoload": {
"psr-4": {
diff --git a/config/admin-secure.php b/config/admin-secure.php
index 0c592ad..0137c15 100644
--- a/config/admin-secure.php
+++ b/config/admin-secure.php
@@ -1,5 +1,9 @@
env('LDC_WEB_API_ENDPOINT', 'https://ldc-web.ldc.fr/api/ldc-networks'),
'apiKey' => env('LDC_WEB_API_KEY'),
+ 'cidrManualAllowedList' => collect(explode(',', strval(env(
+ 'LDC_WEB_CIDR_MANUAL_ALLOWED_LIST'
+ ))))->filter()
];
diff --git a/phpcs.xml b/phpcs.xml
index 6f641ba..2e8e8ed 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -15,6 +15,7 @@
+
@@ -114,4 +115,10 @@
+
+
+
+ */database/migrations/*
+
+
diff --git a/src/AdminSecure.php b/src/AdminSecure.php
new file mode 100644
index 0000000..dfee257
--- /dev/null
+++ b/src/AdminSecure.php
@@ -0,0 +1,249 @@
+server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
+ $ip = IPAddress::Make($ip);
+
+ if ($ip->isErr()) {
+ throw new AdminSecureException(\trans('Client IP not valid ') . $ip->getErr());
+ }
+ return $ip->unwrap();
+ }
+
+ /**
+ * Fetch Ip Adress list from LDC Web
+ *
+ * @return array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address>
+ * @throws \LDC\WebAdminSecureLaravel\AdminSecureException If Wrong env value.
+ * @throws \Illuminate\Http\Client\RequestException If calling admin secure fails.
+ * @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
+ */
+ public static function fetchIpList(): array
+ {
+ // phpcs:enable
+ $endpoint = config('admin-secure.endpoint');
+ $key = config('admin-secure.apiKey');
+ if (!$endpoint or !\filter_var($endpoint, \FILTER_VALIDATE_URL)) {
+ throw new AdminSecureException('LDC_WEB_API_ENDPOINT is required in .env to ba a valid url.');
+ }
+ if (!$key) {
+ throw new AdminSecureException('LDC_WEB_API_KEY is required in .env .');
+ }
+
+ $response = Http::get($endpoint, ['token' => $key])->throw();
+ $adresses = self::parseAdresses($response);
+
+ Log::info(\sprintf('ADMINSECURE : Fetched %d ip(s) from LDC WEB', \count($adresses)));
+
+ return $adresses;
+ }
+
+ /**
+ * Fetch Ip Adress list from LDC Web
+ *
+ * @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range>
+ * @throws \LDC\WebAdminSecureLaravel\AdminSecureException If Wrong env value.
+ * @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
+ */
+ public static function fetchConfigAllowedCidrList(): array
+ {
+ $allowed_cidr_config_list = \collect(config('admin-secure.cidrManualAllowedList'))->filter();
+ $allowed_cidr_config_list_hash = \md5($allowed_cidr_config_list->implode(','));
+ $allowed_cidr_list = $allowed_cidr_config_list
+ ->map(function (string $cidr) {
+ $ip_range = !IPv4Range::Make($cidr)->isErr() ? IPv4Range::Make($cidr) : IPv6Range::Make($cidr);
+ if ($ip_range->isErr()) {
+ throw new AdminSecureException(\trans(
+ 'LDC_WEB_CIDR_MANUAL_ALLOWED_LIST not valid (ipv4 or ipv6 cidr) :cidr',
+ [
+ 'cidr' => $cidr
+ ]
+ ));
+ }
+ return $ip_range->unwrap();
+ })->all();
+
+ // * Flush cache if hash config hash changed
+ if ($allowed_cidr_config_list_hash !== \collect(Cache::get(self::$cache_config_key))->get('hash')) {
+ Cache::flush(self::$cache_config_key);
+ }
+
+ /** @var array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> $allowed_cidr_list */
+ return \collect(Cache::rememberForever(self::$cache_config_key, fn () => [
+ 'list' => $allowed_cidr_list,
+ 'hash' => $allowed_cidr_config_list_hash
+ ]))->get('list');
+ }
+
+ /**
+ * Get ip list from cache
+ *
+ * @return array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address>
+ */
+ public static function getCacheList(): array
+ {
+ try {
+ $ip_list = Cache::get(self::$cache_key, self::fetchIpList());
+ self::validateIpList($ip_list);
+ /** @var array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address> $ip_list */
+ return $ip_list;
+ } catch (AdminSecureInvalidAdressException) {
+ $ip_list = self::fetchIpList();
+ self::validateIpList($ip_list);
+ self::setCacheList($ip_list);
+ return $ip_list;
+ }
+ }
+
+ /**
+ * Set ip list to cache
+ *
+ * @param array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address> $ip_list
+ * @return void
+ */
+ public static function setCacheList(array $ip_list): void
+ {
+ self::validateIpList($ip_list);
+ Cache::put(self::$cache_key, $ip_list);
+ }
+
+ /**
+ * Check if ipV4 is private
+ *
+ * @param \Dxw\CIDR\IPv4Address $ipv4_address
+ * @return boolean
+ */
+ private static function ipV4IsPrivate(IPv4Address $ipv4_address): bool
+ {
+ return \collect(self::$private_ip_v4_cidr_list)
+ ->concat([self::$private_ip_v4_loopback_cidr])
+ ->map(fn (string $ipv4_cidr) => IPv4Range::Make($ipv4_cidr)->unwrap())
+ ->reduce(fn (bool|null $initial, IPv4Range $ipv4_range) =>
+ $initial || $ipv4_range->containsAddress($ipv4_address));
+ }
+
+ /**
+ * Check if ipV6 is private
+ *
+ * @param \Dxw\CIDR\IPv6Address $ipv6_address
+ * @return boolean
+ */
+ private static function ipV6IsPrivate(IPv6Address $ipv6_address): bool
+ {
+ return \collect(self::$private_ip_v6_cidr_list)
+ ->concat([self::$private_ip_v6_loopback_cidr])
+ ->map(fn (string $ipv6_cidr) => IPv6Range::Make($ipv6_cidr)->unwrap())
+ ->reduce(fn (bool|null $initial, IPv6Range $ipv6_range) =>
+ $initial || $ipv6_range->containsAddress($ipv6_address));
+ }
+
+ /**
+ * Validate ip list
+ *
+ * @param array $ip_list
+ * @return void
+ * @throws \LDC\WebAdminSecureLaravel\Exceptions\AdminSecureInvalidAdressException If an IP is invalid.
+ */
+ private static function validateIpList(array $ip_list): void
+ {
+ \collect($ip_list)->each(function ($ip) {
+ if (!\is_object($ip) or !\in_array(\get_class($ip), [IPv4Address::class, IPv6Address::class])) {
+ throw new AdminSecureInvalidAdressException('Invalid adresse ' . \strval(\json_encode($ip)));
+ }
+ });
+ }
+
+ /**
+ * Parse adresses
+ *
+ * @param \Illuminate\Http\Client\Response $response
+ * @return array<\Dxw\CIDR\IPAddress>
+ */
+ private static function parseAdresses(Response $response): array
+ {
+ return \collect($response->json())
+ ->map(fn (string $ip) => IPAddress::Make($ip)->unwrap())->all();
+ }
+}
diff --git a/src/AdminSecureServiceProvider.php b/src/AdminSecureServiceProvider.php
index 9d6c7ec..ed8c5c5 100644
--- a/src/AdminSecureServiceProvider.php
+++ b/src/AdminSecureServiceProvider.php
@@ -3,6 +3,7 @@
namespace LDC\WebAdminSecureLaravel;
use Illuminate\Console\Scheduling\Schedule;
+use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Illuminate\Routing\Router;
use LDC\WebAdminSecureLaravel\Console\SyncFromLDCWeb;
@@ -21,22 +22,22 @@ class AdminSecureServiceProvider extends ServiceProvider
__DIR__ . '/../config' => config_path(),
], 'config');
- // Register the command if we are using the application via the CLI .
- if ($this->app->runningInConsole()) {
- $this->commands([
- SyncFromLDCWeb::class,
- ]);
- }
+ // * Register the command if we are using the application via the CLI .
+ $this->commands([SyncFromLDCWeb::class]);
+
+ // * Set filter condition
+ $this->app->singleton('hasToFilterIp', fn (Application $app) => $app->environment('production'));
// * Cron Report everyday
$this->app->booted(function () {
- if ($this->app->environment('production')) {
+ if ($this->app->make('hasToFilterIp')) {
/** @var \Illuminate\Console\Scheduling\Schedule */
$schedule = $this->app->make(Schedule::class);
$schedule->command('ldcweb:admin-secure-sync')->daily();
}
});
+ /** @var \Illuminate\Routing\Router */
$router = $this->app->make(Router::class);
$router->aliasMiddleware('admin-secure', AdminSecureMiddleware::class);
}
diff --git a/src/Console/SyncFromLDCWeb.php b/src/Console/SyncFromLDCWeb.php
index da62c24..7a0d7b7 100644
--- a/src/Console/SyncFromLDCWeb.php
+++ b/src/Console/SyncFromLDCWeb.php
@@ -3,12 +3,11 @@
namespace LDC\WebAdminSecureLaravel\Console;
use Illuminate\Console\Command;
-use Illuminate\Support\Facades\Http;
-use Illuminate\Support\Facades\Cache;
-use LDC\WebAdminSecureLaravel\AdminSecureException;
+use LDC\WebAdminSecureLaravel\AdminSecure;
/**
* Send Report Status To LDC Web.
+ *
* @author Vincent BRIDIER
*/
class SyncFromLDCWeb extends Command
@@ -25,36 +24,18 @@ class SyncFromLDCWeb extends Command
*
* @var string
*/
- protected $description = 'Sync IP ranges from LDC Web';
-
- /**
- * The call endpoint
- *
- * @var string
- */
- private $endpoint = 'https://ldc-web.ldc.fr/api/ldc-networks';
+ protected $description = 'Sync IP list from LDC Web';
/**
* Execute the console command.
*
* @return integer
+ * @throws \LDC\WebAdminSecureLaravel\Exceptions\AdminSecureException If missing LDC_WEB_API_KEY key.
*/
- public function handle()
+ public function handle(): int
{
- $this->info('Sync IP Ranges from LDC Web.');
-
- if (!config('admin-secure.apiKey')) {
- throw new AdminSecureException('LDC_WEB_API_KEY is required in .env');
- }
-
- $response = Http::get($this->endpoint, [
- 'token' => config('admin-secure.apiKey')
- ]);
-
- $response->throw();
-
- Cache::put('ldcweb_ip_ranges', \json_decode($response->body()));
-
+ $this->info($this->description);
+ AdminSecure::setCacheList(AdminSecure::fetchIpList());
$this->info('Done.');
return 0;
diff --git a/src/AdminSecureException.php b/src/Exceptions/AdminSecureException.php
similarity index 54%
rename from src/AdminSecureException.php
rename to src/Exceptions/AdminSecureException.php
index f333cc7..10a92a1 100644
--- a/src/AdminSecureException.php
+++ b/src/Exceptions/AdminSecureException.php
@@ -1,6 +1,6 @@
make('hasToFilterIp')) {
return $next($request);
}
- /** HA PROXY HACK */
- $ip = request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
- $restrictedIps = Cache::get('ldcweb_ip_ranges');
+ $client_ip = AdminSecure::fetchClientIp();
+ $ip_is_allowed = \collect(AdminSecure::fetchIpList())
+ ->reduce(fn (bool|null $initial, IPv4Address|IPv6Address $ip_address) => $initial || IP::contains(
+ $ip_address,
+ $client_ip
+ )->unwrap());
- if (!is_null($restrictedIps) && in_array($ip, $restrictedIps)) {
+ $ip_is_allowed_by_config = \collect(AdminSecure::fetchConfigAllowedCidrList())
+ ->reduce(fn (bool|null $initial, IPv4Range|IPv6Range $ip_range) => $initial || IP::contains(
+ $ip_range,
+ $client_ip
+ )->unwrap());
+
+ if ($ip_is_allowed or $ip_is_allowed_by_config or AdminSecure::ipIsPrivate($client_ip)) {
return $next($request);
}
- abort(403);
+
+ abort(403, \trans('Denied IP'));
}
}