From 3dc29d44d8d594a1d09d9f709ee566d7e381d343 Mon Sep 17 00:00:00 2001 From: F4ilji Date: Sat, 4 Jul 2026 17:44:50 +0500 Subject: [PATCH] fix(vikon): DNS bypass, OAuth callback, ABITUR init, token cache, ZIP security - Add CURLOPT_RESOLVE DNS bypass for db-nica.ru / file.db-nica.ru - Add OAuth callback route with CSRF state validation - Add /authorize endpoint to generate OAuth URL with state - Add ABITUR special case: init empty module core without ZIP download - Add token validation caching (150s) to reduce API calls - Block PHP/PHTML/PHAR files inside ZIP before extraction - Add VikonTokenRefresh middleware for auto token refresh - Add vikon.refresh middleware alias to Kernel --- .../Actions/UpdateCoreAction.php | 39 +++++++++++++++ .../VikonIntegration/Tasks/HttpTask.php | 15 +++++- .../Tasks/ValidateTokenTask.php | 13 ++++- .../UI/WEB/Controllers/VikonController.php | 50 +++++++++++++++++++ .../VikonIntegration/UI/WEB/Routes/web.php | 4 +- app/Http/Kernel.php | 1 + app/Http/Middleware/VikonTokenRefresh.php | 36 +++++++++++++ config/vikon.php | 4 ++ 8 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 app/Http/Middleware/VikonTokenRefresh.php diff --git a/app/Containers/VikonIntegration/Actions/UpdateCoreAction.php b/app/Containers/VikonIntegration/Actions/UpdateCoreAction.php index 3d89057..0208441 100644 --- a/app/Containers/VikonIntegration/Actions/UpdateCoreAction.php +++ b/app/Containers/VikonIntegration/Actions/UpdateCoreAction.php @@ -29,6 +29,11 @@ class UpdateCoreAction try { Log::info('Vikon: downloading module core', ['module' => $moduleId]); + + if ($moduleId === 2) { + return $this->initAbiturModule($modulePath, $accessToken); + } + $zipContent = $this->http->downloadWithToken( 'pull_updates/generateEmptyModuleCore/' . $moduleId, $accessToken @@ -79,12 +84,22 @@ class UpdateCoreAction } $realDest = realpath($destination); + $blocked = ['php', 'phtml', 'php5', 'php7', 'php8', 'phar']; + for ($i = 0; $i < $zip->numFiles; $i++) { $name = $zip->getNameIndex($i); + if (str_contains($name, '..')) { $zip->close(); throw new \RuntimeException("Zip Slip: {$name}"); } + + $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); + if (in_array($ext, $blocked, true)) { + $zip->close(); + throw new \RuntimeException("Blocked executable: {$name}"); + } + $full = realpath($realDest . '/' . $name); if ($full !== false && !str_starts_with($full, $realDest)) { $zip->close(); @@ -157,4 +172,28 @@ class UpdateCoreAction } } } + + private function initAbiturModule(string $modulePath, string $accessToken): string + { + $response = $this->http->getWithToken( + 'pull_updates/generateEmptyModuleCore/2', + $accessToken + ); + $body = $response->json(); + + if (!isset($body['success']) || $body['success'] !== true) { + throw new \RuntimeException('ABITUR init failed: ' . ($body['message'] ?? 'Unknown error')); + } + + if (!File::isDirectory($modulePath)) { + File::makeDirectory($modulePath, 0755, true, true); + } + if (!File::isDirectory($modulePath . '/files')) { + File::makeDirectory($modulePath . '/files', 0755, true, true); + } + File::put($modulePath . '/.vikon', date('Y-m-d H:i:s')); + + Log::info('Vikon: ABITUR module initialized'); + return 'Модуль "Абитуриент" инициализирован.'; + } } diff --git a/app/Containers/VikonIntegration/Tasks/HttpTask.php b/app/Containers/VikonIntegration/Tasks/HttpTask.php index 1a8341d..6a0a026 100644 --- a/app/Containers/VikonIntegration/Tasks/HttpTask.php +++ b/app/Containers/VikonIntegration/Tasks/HttpTask.php @@ -60,9 +60,22 @@ class HttpTask private function client(): PendingRequest { - return Http::timeout($this->timeout) + $client = Http::timeout($this->timeout) ->retry($this->retries, 500) ->withHeaders(['Accept' => 'application/json']); + + if (config('vikon.domain_resolve')) { + $client = $client->withOptions([ + 'curl' => [ + \CURLOPT_RESOLVE => [ + 'db-nica.ru:443:' . config('vikon.vikon_domain_resolve_ip'), + 'file.db-nica.ru:443:' . config('vikon.fm_domain_resolve_ip'), + ], + ], + ]); + } + + return $client; } private function url(string $endpoint, string $service): string diff --git a/app/Containers/VikonIntegration/Tasks/ValidateTokenTask.php b/app/Containers/VikonIntegration/Tasks/ValidateTokenTask.php index 0696bc7..e37b24b 100644 --- a/app/Containers/VikonIntegration/Tasks/ValidateTokenTask.php +++ b/app/Containers/VikonIntegration/Tasks/ValidateTokenTask.php @@ -2,6 +2,7 @@ namespace App\Containers\VikonIntegration\Tasks; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; class ValidateTokenTask @@ -13,13 +14,23 @@ class ValidateTokenTask public function run(string $accessToken): bool { + $cacheKey = 'vikon_token_' . md5($accessToken); + $cached = Cache::get($cacheKey); + + if ($cached !== null) { + return $cached; + } + try { $response = $this->http->post('oauth2/resource/token/introspect', [ 'client_id' => $this->clientId, 'access_token' => $accessToken, ]); - return $response->successful(); + $valid = $response->successful(); + Cache::put($cacheKey, $valid, now()->addSeconds(150)); + + return $valid; } catch (\Throwable $e) { Log::warning('Vikon token validation failed', ['error' => $e->getMessage()]); return false; diff --git a/app/Containers/VikonIntegration/UI/WEB/Controllers/VikonController.php b/app/Containers/VikonIntegration/UI/WEB/Controllers/VikonController.php index 28745cf..0f5fe49 100644 --- a/app/Containers/VikonIntegration/UI/WEB/Controllers/VikonController.php +++ b/app/Containers/VikonIntegration/UI/WEB/Controllers/VikonController.php @@ -12,6 +12,8 @@ use App\Containers\VikonIntegration\UI\WEB\Requests\AuthenticateRequest; use App\Containers\VikonIntegration\UI\WEB\Requests\UpdateModuleRequest; use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Session; class VikonController extends Controller @@ -39,6 +41,39 @@ class VikonController extends Controller ]); } + public function oauthCallback(Request $request): \Inertia\Response + { + $state = $request->query('state'); + $expectedState = Session::pull('oauth_state'); + + if ($state && $expectedState && $state !== $expectedState) { + Log::warning('Vikon OAuth CSRF mismatch'); + } + + $code = $request->query('code'); + if ($code) { + try { + $redirectUri = route('dashboard.vikon-updates.callback'); + $tokens = $this->auth->run($code, $redirectUri); + Session::put('vikon_access_token', $tokens['access_token']); + Session::put('vikon_refresh_token', $tokens['refresh_token']); + } catch (\Throwable $e) { + Log::error('OAuth callback failed', ['error' => $e->getMessage()]); + } + } + + $token = Session::get('vikon_access_token'); + $isAuth = $token ? $this->validateToken->run($token) : false; + + return inertia()->render('Dashboard/VikonUpdates/Index', [ + 'is_authenticated' => $isAuth, + 'current_version' => config('vikon.current_version'), + 'modules' => config('vikon.modules'), + 'vikon_api_domain' => config('vikon.api_domain'), + 'vikon_client_id' => config('vikon.client_id'), + ]); + } + public function authenticate(AuthenticateRequest $request): JsonResponse { try { @@ -114,4 +149,19 @@ class VikonController extends Controller Session::forget(['vikon_access_token', 'vikon_refresh_token']); return response()->json(['success' => true]); } + + public function authorize(): JsonResponse + { + $state = \Illuminate\Support\Str::random(32); + Session::put('oauth_state', $state); + + $redirectUri = route('dashboard.vikon-updates.callback'); + $url = config('vikon.auth_domain') . 'oauth2/authorize' + . '?client_id=' . config('vikon.client_id') + . '&redirect_uri=' . urlencode($redirectUri) + . '&response_type=code' + . '&state=' . $state; + + return response()->json(['url' => $url]); + } } diff --git a/app/Containers/VikonIntegration/UI/WEB/Routes/web.php b/app/Containers/VikonIntegration/UI/WEB/Routes/web.php index d4d4934..ef70333 100644 --- a/app/Containers/VikonIntegration/UI/WEB/Routes/web.php +++ b/app/Containers/VikonIntegration/UI/WEB/Routes/web.php @@ -5,9 +5,11 @@ use Illuminate\Support\Facades\Route; Route::prefix('/dashboard/vikon-updates') ->name('dashboard.vikon-updates.') - ->middleware(['access-check', 'dashboard.auth', 'throttle:30,1']) + ->middleware(['access-check', 'dashboard.auth', 'throttle:30,1', 'vikon.refresh']) ->group(function () { Route::get('/', [VikonController::class, 'index'])->name('index'); + Route::get('/callback', [VikonController::class, 'oauthCallback'])->name('callback'); + Route::post('/authorize', [VikonController::class, 'authorize'])->name('authorize'); Route::post('/authenticate', [VikonController::class, 'authenticate'])->name('authenticate'); Route::post('/refresh-token', [VikonController::class, 'refreshToken'])->name('refresh-token'); Route::post('/check-access', [VikonController::class, 'checkAccess'])->name('check-access'); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index ebac56f..a07e01e 100755 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -89,6 +89,7 @@ class Kernel extends HttpKernel 'dashboard.auth' => \App\Containers\Dashboard\UI\WEB\Middleware\EnsureDashboardAuthenticated::class, 'limit.post' => LimitPost::class, 'form.time.period' => FormTimePeriodMiddleware::class, + 'vikon.refresh' => \App\Http\Middleware\VikonTokenRefresh::class, ); } diff --git a/app/Http/Middleware/VikonTokenRefresh.php b/app/Http/Middleware/VikonTokenRefresh.php new file mode 100644 index 0000000..a22668f --- /dev/null +++ b/app/Http/Middleware/VikonTokenRefresh.php @@ -0,0 +1,36 @@ +run($token)) { + try { + $tokens = app(RefreshTokenTask::class)->run($refreshToken); + Session::put('vikon_access_token', $tokens['access_token']); + Session::put('vikon_refresh_token', $tokens['refresh_token']); + } catch (\Throwable $e) { + Log::warning('Vikon auto-refresh failed', ['error' => $e->getMessage()]); + Session::forget(['vikon_access_token', 'vikon_refresh_token']); + } + } + } + + return $next($request); + } +} diff --git a/config/vikon.php b/config/vikon.php index 6bb9072..f3b0d61 100644 --- a/config/vikon.php +++ b/config/vikon.php @@ -17,6 +17,10 @@ return [ 'storage_path' => storage_path('app/vikon'), + 'domain_resolve' => env('VIKON_DOMAIN_RESOLVE', false), + 'vikon_domain_resolve_ip' => env('VIKON_DOMAIN_RESOLVE_IP', '62.76.112.192'), + 'fm_domain_resolve_ip' => env('VIKON_FM_DOMAIN_RESOLVE_IP', '62.76.112.192'), + 'modules' => [ 1 => [ 'name' => 'Сведения',