feat(vikon): remove broken VikonIntegration container - starting fresh

This commit is contained in:
F4ilji
2026-07-04 12:40:41 +05:00
parent 67b9aae961
commit 47aac647ac
20 changed files with 1 additions and 2563 deletions
@@ -1,171 +0,0 @@
<?php
namespace App\Containers\VikonIntegration\Tasks;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\ConnectionException;
/**
* Task: Makes secure HTTP requests to Vikon API (db-nica.ru)
*
* SSL verification is ENABLED by default (unlike old vikon_core).
* Uses Laravel Http facade with proper timeout and retry settings.
*/
class CallVikonApiTask
{
public function __construct(
private readonly string $apiDomain,
private readonly string $authDomain,
private readonly string $filemanagerDomain,
private readonly int $timeout,
private readonly int $retries,
) {}
/**
* GET request to Vikon API
*/
public function get(string $endpoint, array $headers = [], string $service = 'api'): \Illuminate\Http\Client\Response
{
$baseUrl = $this->resolveBaseUrl($service);
$url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
return $this->makeRequest('get', $url, [], $headers);
}
/**
* POST request to Vikon API
*/
public function post(string $endpoint, array $data = [], array $headers = [], string $service = 'api'): \Illuminate\Http\Client\Response
{
$baseUrl = $this->resolveBaseUrl($service);
$url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
return $this->makeRequest('post', $url, $data, $headers);
}
/**
* GET request with Bearer token authorization
*/
public function getWithToken(string $endpoint, string $token, string $service = 'api'): \Illuminate\Http\Client\Response
{
$baseUrl = $this->resolveBaseUrl($service);
$url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
return $this->makeRequest('get', $url, [], [
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
]);
}
/**
* POST request with Bearer token authorization
*/
public function postWithToken(string $endpoint, string $token, array $data = [], string $service = 'api'): \Illuminate\Http\Client\Response
{
$baseUrl = $this->resolveBaseUrl($service);
$url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
return $this->makeRequest('post', $url, $data, [
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
]);
}
/**
* Download binary content (ZIP archive) with Bearer token
*/
public function downloadWithToken(string $endpoint, string $token, string $service = 'api'): string
{
$baseUrl = $this->resolveBaseUrl($service);
$url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept-Encoding' => 'zip, gzip',
'Accept' => 'application/json',
])
->timeout($this->timeout)
->retry($this->retries, 1000, function ($exception, $response) {
if ($response && $response->successful()) {
return false;
}
return true;
})
->get($url);
if ($response->failed()) {
throw new \RuntimeException(
'Vikon API error: ' . $this->extractErrorMessage($response) . ' (HTTP ' . $response->status() . ')'
);
}
return $response->body();
}
/**
* Resolve base URL by service type
*/
private function resolveBaseUrl(string $service): string
{
return match ($service) {
'auth' => $this->authDomain,
'filemanager' => $this->filemanagerDomain,
default => $this->apiDomain,
};
}
/**
* Make HTTP request with common settings
*
* @throws ConnectionException
* @throws \RuntimeException
*/
private function makeRequest(string $method, string $url, array $data, array $headers): \Illuminate\Http\Client\Response
{
$http = Http::withHeaders(array_merge([
'Accept' => 'application/json',
], $headers))
->timeout($this->timeout)
->retry($this->retries, 1000, function ($exception, $response) {
if ($response && $response->successful()) {
return false;
}
return true;
});
$response = match ($method) {
'get' => $data ? $http->get($url, $data) : $http->get($url),
'post' => $http->post($url, $data),
};
if ($response->failed()) {
throw new \RuntimeException(
'Vikon API error: ' . $this->extractErrorMessage($response) . ' (HTTP ' . $response->status() . ')'
);
}
return $response;
}
/**
* Extract error message from API response
*/
private function extractErrorMessage(\Illuminate\Http\Client\Response $response): string
{
$body = $response->json();
if (isset($body['message'])) {
return $body['message'];
}
if (isset($body['error'])) {
return $body['error'];
}
if (isset($body['messages']) && is_array($body['messages'])) {
return implode('; ', $body['messages']);
}
return 'Unknown error';
}
}
@@ -1,35 +0,0 @@
<?php
namespace App\Containers\VikonIntegration\Tasks;
/**
* Task: Check if current entry point is allowed in Vikon settings
*/
class CheckVikonEntryPointTask
{
public function __construct(
private readonly CallVikonApiTask $callVikonApiTask,
private readonly string $clientId,
) {}
/**
* Verify that the current URL is registered as valid entry point
*
* @return bool
* @throws \RuntimeException
*/
public function run(string $entryPoint): bool
{
$response = $this->callVikonApiTask->get(
'oauth2/checkEntryPoint',
[
'client_id' => $this->clientId,
'entry_point' => $entryPoint,
]
);
$body = $response->json();
return isset($body['success']) && $body['success'] === true;
}
}
@@ -1,119 +0,0 @@
<?php
namespace App\Containers\VikonIntegration\Tasks;
use ZipArchive;
/**
* Task: Safely extracts ZIP archives with Zip Slip protection
*
* Unlike old vikon_core which used unpackZip() without path validation,
* this task validates every entry to prevent directory traversal attacks.
*/
class ExtractZipArchiveTask
{
/**
* Extract ZIP archive to destination with security checks
*
* @param string $zipPath Absolute path to ZIP file
* @param string $destination Absolute path to extraction directory
* @return bool
* @throws \RuntimeException
*/
public function run(string $zipPath, string $destination): bool
{
if (!file_exists($zipPath)) {
throw new \RuntimeException("ZIP file not found: {$zipPath}");
}
if (!is_writable(dirname($destination))) {
throw new \RuntimeException("Destination directory is not writable: " . dirname($destination));
}
$zip = new ZipArchive;
$openResult = $zip->open($zipPath);
if ($openResult !== true) {
throw new \RuntimeException("Failed to open ZIP archive (code: {$openResult})");
}
// Zip Slip protection: validate every entry
$this->validateZipEntries($zip, $destination);
// Extract with overwrite
$extractResult = $zip->extractTo($destination);
$zip->close();
if (!$extractResult) {
throw new \RuntimeException('Failed to extract ZIP archive');
}
return true;
}
/**
* Validate all ZIP entries to prevent Zip Slip attack
*
* @throws \RuntimeException
*/
private function validateZipEntries(ZipArchive $zip, string $destination): void
{
$realDestination = realpath($destination);
if ($realDestination === false) {
throw new \RuntimeException('Destination directory does not exist');
}
for ($i = 0; $i < $zip->numFiles; $i++) {
$filename = $zip->getNameIndex($i);
// Check for path traversal patterns
if ($this->containsPathTraversal($filename)) {
throw new \RuntimeException(
"Potentially malicious ZIP entry detected: {$filename}"
);
}
// Resolve full path and verify it's within destination
$fullPath = realpath($realDestination . '/' . $filename);
if ($fullPath === false) {
// File doesn't exist yet (will be created), check parent directory
$parentDir = dirname($realDestination . '/' . $filename);
if (strpos($parentDir, $realDestination) !== 0) {
throw new \RuntimeException(
"ZIP entry escapes destination directory: {$filename}"
);
}
} elseif (strpos($fullPath, $realDestination) !== 0) {
throw new \RuntimeException(
"ZIP entry escapes destination directory: {$filename}"
);
}
}
}
/**
* Check if filename contains path traversal patterns
*/
private function containsPathTraversal(string $filename): bool
{
// Check for directory traversal sequences
if (strpos($filename, '..') !== false) {
return true;
}
// Check for absolute paths on Windows
if (preg_match('/^[a-zA-Z]:/', $filename)) {
return true;
}
// Check for absolute paths on Unix
if (strpos($filename, '/') === 0) {
return true;
}
return false;
}
}
@@ -1,223 +0,0 @@
<?php
namespace App\Containers\VikonIntegration\Tasks;
use Illuminate\Support\Facades\File;
/**
* Task: Manage module files (scan, remove, create directories)
*
* Replaces old vikon_core Filesystem class with Laravel's File facade.
* All operations are scoped to specific module to prevent accidental deletion.
*/
class ManageModuleFilesTask
{
public function __construct(
private readonly string $basePath,
) {}
/**
* Get module root path
*/
public function getModulePath(int $moduleId, string $moduleFolder): string
{
return $this->basePath . '/' . $moduleFolder;
}
/**
* Safely remove directory with path validation
*
* @param string $path Path to remove
* @param string $allowedBasePath Base path constraint
* @param bool $recursive Remove recursively
* @return bool
*/
public function safeRemove(string $path, string $allowedBasePath, bool $recursive = false): bool
{
// Prevent path traversal
if (!$this->isPathWithinBase($path, $allowedBasePath)) {
\Illuminate\Support\Facades\Log::warning('Attempted to remove path outside allowed base', [
'path' => $path,
'allowed_base' => $allowedBasePath,
]);
return false;
}
if (!file_exists($path)) {
return true;
}
if (!is_dir($path)) {
return File::delete($path);
}
return File::deleteDirectory($path);
}
/**
* Safely create directory
*/
public function safeMkdir(string $path, int $mode = 0755): bool
{
if (File::isDirectory($path)) {
return true;
}
return File::makeDirectory($path, $mode, true, true);
}
/**
* Safely create file with content
*/
public function safeCreateFile(string $path, string $content = '', int $mode = 0644): bool
{
if (File::exists($path)) {
return true;
}
$directory = dirname($path);
if (!File::isDirectory($directory)) {
File::makeDirectory($directory, 0755, true, true);
}
$result = File::put($path, $content);
if ($result === false) {
return false;
}
chmod($path, $mode);
return true;
}
/**
* Scan directory safely (excludes . and ..)
*
* @return array|false
*/
public function safeScandir(string $path): array|false
{
if (!File::isDirectory($path) || !File::isReadable($path)) {
return false;
}
$entries = File::directories($path);
$files = File::files($path);
return array_merge(
array_map('basename', $entries),
array_map('basename', $files)
);
}
/**
* Rename file with path validation
*/
public function safeRename(string $oldPath, string $newPath, string $allowedBasePath): bool
{
if (!$this->isPathWithinBase($oldPath, $allowedBasePath)) {
return false;
}
if (!$this->isPathWithinBase($newPath, $allowedBasePath)) {
return false;
}
if (!File::exists($oldPath)) {
return false;
}
if (File::exists($newPath)) {
return false;
}
$newDir = dirname($newPath);
if (!File::isDirectory($newDir)) {
return false;
}
return File::move($oldPath, $newPath);
}
/**
* Replace directory with rename (atomic operation)
*
* Old -> _old
* New -> Old
*/
public function replaceWithRename(string $sourceDir, string $targetDir, string $allowedBasePath): bool
{
if (!$this->isPathWithinBase($sourceDir, $allowedBasePath)) {
return false;
}
if (!$this->isPathWithinBase($targetDir, $allowedBasePath)) {
return false;
}
if (!File::exists($sourceDir)) {
return false;
}
if (File::exists($targetDir)) {
return false;
}
$parentDir = dirname($targetDir);
if (!is_writable($parentDir)) {
return false;
}
if (!File::makeDirectory($targetDir, 0755, true, true)) {
return false;
}
$entries = $this->safeScandir($sourceDir);
if (empty($entries)) {
return File::deleteDirectory($sourceDir);
}
foreach ($entries as $entry) {
$item = $sourceDir . '/' . $entry;
$newPath = $targetDir . '/' . $entry;
if (File::isDirectory($item)) {
if (!$this->replaceWithRename($item, $newPath, $allowedBasePath)) {
return false;
}
} else {
if (!File::move($item, $newPath)) {
return false;
}
}
}
return File::deleteDirectory($sourceDir);
}
/**
* Check if path is within allowed base directory
*/
private function isPathWithinBase(string $path, string $basePath): bool
{
$realPath = realpath($path);
$realBase = realpath($basePath);
// If base directory doesn't exist, path cannot be valid
if ($realBase === false) {
\Illuminate\Support\Facades\Log::error('Base path does not exist', [
'base' => $basePath,
]);
return false;
}
if ($realPath === false) {
// For non-existent paths, check parent directory
$parentDir = dirname($path);
$realParent = realpath($parentDir);
if ($realParent === false) {
return false;
}
return str_starts_with($realParent . '/', $realBase . '/');
}
return str_starts_with($realPath . '/', $realBase . '/') || $realPath === $realBase;
}
}
@@ -1,50 +0,0 @@
<?php
namespace App\Containers\VikonIntegration\Tasks;
/**
* Task: Refresh Vikon access token using refresh_token
*
* Old vikon_core used refresh_token.php with SSL verification disabled.
* This task uses secure Http client with proper error handling.
*/
class RefreshVikonTokenTask
{
public function __construct(
private readonly CallVikonApiTask $callVikonApiTask,
private readonly string $clientId,
private readonly string $clientSecret,
) {}
/**
* Exchange refresh_token for new access_token and refresh_token
*
* Uses api_domain (db-nica.ru) as per old update/refresh_access_token.php
* NOT auth_domain (auth.db-nica.ru)
*
* @return array{access_token: string, refresh_token: string}
* @throws \RuntimeException
*/
public function run(string $refreshToken): array
{
$response = $this->callVikonApiTask->post('oauth2/RefreshToken', [
'refresh_token' => $refreshToken,
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'grant_type' => 'refresh_token',
], [], 'api'); // <-- api domain (db-nica.ru), NOT auth domain
$body = $response->json();
if (!isset($body['access_token']) || !isset($body['refresh_token'])) {
throw new \RuntimeException(
'Invalid token refresh response: ' . ($body['message'] ?? 'Unknown error')
);
}
return [
'access_token' => $body['access_token'],
'refresh_token' => $body['refresh_token'],
];
}
}
@@ -1,40 +0,0 @@
<?php
namespace App\Containers\VikonIntegration\Tasks;
/**
* Task: Validate Vikon access token by checking with remote API
*/
class ValidateVikonTokenTask
{
public function __construct(
private readonly CallVikonApiTask $callVikonApiTask,
) {}
/**
* Check if access_token is valid
*
* Uses auth_domain (auth.db-nica.ru) + api/profile_applicant/check_access_token
* as per old scripts/check_token.php
*
* @return bool
*/
public function run(string $accessToken): bool
{
try {
$response = $this->callVikonApiTask->getWithToken(
'api/profile_applicant/check_access_token',
$accessToken,
'auth' // <-- auth domain (auth.db-nica.ru)
);
return $response->successful();
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::warning('Vikon token validation failed', [
'error' => $e->getMessage(),
]);
return false;
}
}
}