%PDF- %PDF-
Direktori : /www/varak.net/nextcloud.varak.net/apps_old/apps/app_api/lib/Controller/ |
Current File : //www/varak.net/nextcloud.varak.net/apps_old/apps/app_api/lib/Controller/ExAppProxyController.php |
<?php declare(strict_types=1); namespace OCA\AppAPI\Controller; use GuzzleHttp\Cookie\CookieJar; use GuzzleHttp\Cookie\SetCookie; use GuzzleHttp\RequestOptions; use OC\Security\CSP\ContentSecurityPolicyNonceManager; use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Db\ExApp; use OCA\AppAPI\Db\ExAppRouteAccessLevel; use OCA\AppAPI\ProxyResponse; use OCA\AppAPI\Service\AppAPIService; use OCA\AppAPI\Service\ExAppService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\NotFoundResponse; use OCP\AppFramework\Http\Response; use OCP\Files\IMimeTypeDetector; use OCP\Http\Client\IResponse; use OCP\IGroupManager; use OCP\IRequest; use OCP\Security\Bruteforce\IThrottler; use Psr\Log\LoggerInterface; class ExAppProxyController extends Controller { public function __construct( IRequest $request, private readonly AppAPIService $service, private readonly ExAppService $exAppService, private readonly IMimeTypeDetector $mimeTypeHelper, private readonly ContentSecurityPolicyNonceManager $nonceManager, private readonly ?string $userId, private readonly IGroupManager $groupManager, private readonly LoggerInterface $logger, private readonly IThrottler $throttler, ) { parent::__construct(Application::APP_ID, $request); } private function createProxyResponse(string $path, IResponse $response, $cache = true): ProxyResponse { $headersToIgnore = ['aa-version', 'ex-app-id', 'authorization-app-api', 'ex-app-version', 'aa-request-id']; $responseHeaders = []; foreach ($response->getHeaders() as $key => $value) { if (!in_array(strtolower($key), $headersToIgnore)) { $responseHeaders[$key] = $value[0]; } } $content = $response->getBody(); $isHTML = pathinfo($path, PATHINFO_EXTENSION) === 'html'; if ($isHTML) { $nonce = $this->nonceManager->getNonce(); $content = str_replace( '<script', "<script nonce=\"$nonce\"", $content ); } if (empty($response->getHeader('content-type'))) { $mime = $this->mimeTypeHelper->detectPath($path); if (pathinfo($path, PATHINFO_EXTENSION) === 'wasm') { $mime = 'application/wasm'; } if (!empty($mime) && $mime != 'application/octet-stream') { $responseHeaders['Content-Type'] = $mime; } } $proxyResponse = new ProxyResponse($response->getStatusCode(), $responseHeaders, $content); if ($cache && !$isHTML && empty($response->getHeader('cache-control')) && $response->getHeader('Content-Type') !== 'application/json' && $response->getHeader('Content-Type') !== 'application/x-tar') { $proxyResponse->cacheFor(3600); } return $proxyResponse; } #[PublicPage] #[NoAdminRequired] #[NoCSRFRequired] public function ExAppGet(string $appId, string $other): Response { $route = []; $bruteforceProtection = []; $delay = 0; $exApp = $this->prepareProxy($appId, $other, $route, $bruteforceProtection, $delay); if ($exApp === null) { return new NotFoundResponse(); } $response = $this->service->requestToExApp2( $exApp, '/' . $other, $this->userId, 'GET', queryParams: $_GET, options: [ RequestOptions::COOKIES => $this->buildProxyCookiesJar($_COOKIE, $this->service->getExAppDomain($exApp)), RequestOptions::HEADERS => $this->buildHeadersWithExclude($route, getallheaders()), RequestOptions::TIMEOUT => 0, ], request: $this->request, ); if (is_array($response)) { return (new Response())->setStatus(500); } $this->processBruteforce($bruteforceProtection, $delay, $response->getStatusCode()); return $this->createProxyResponse($other, $response); } #[PublicPage] #[NoAdminRequired] #[NoCSRFRequired] public function ExAppPost(string $appId, string $other): Response { $route = []; $bruteforceProtection = []; $delay = 0; $exApp = $this->prepareProxy($appId, $other, $route, $bruteforceProtection, $delay); if ($exApp === null) { return new NotFoundResponse(); } $options = [ RequestOptions::COOKIES => $this->buildProxyCookiesJar($_COOKIE, $this->service->getExAppDomain($exApp)), RequestOptions::HEADERS => $this->buildHeadersWithExclude($route, getallheaders()), RequestOptions::TIMEOUT => 0, ]; if (str_starts_with($this->request->getHeader('Content-Type'), 'multipart/form-data') || count($_FILES) > 0) { unset($options['headers']['Content-Type']); unset($options['headers']['Content-Length']); $options[RequestOptions::MULTIPART] = $this->buildMultipartFormData($_POST, $_FILES); } else { $options['body'] = $stream = fopen('php://input', 'r'); } $response = $this->service->requestToExApp2( $exApp, '/' . $other, $this->userId, queryParams: $_GET, options: $options, request: $this->request, ); if (isset($stream) && is_resource($stream)) { fclose($stream); } if (is_array($response)) { return (new Response())->setStatus(500); } $this->processBruteforce($bruteforceProtection, $delay, $response->getStatusCode()); return $this->createProxyResponse($other, $response); } #[PublicPage] #[NoAdminRequired] #[NoCSRFRequired] public function ExAppPut(string $appId, string $other): Response { $route = []; $bruteforceProtection = []; $delay = 0; $exApp = $this->prepareProxy($appId, $other, $route, $bruteforceProtection, $delay); if ($exApp === null) { return new NotFoundResponse(); } $stream = fopen('php://input', 'r'); $options = [ RequestOptions::COOKIES => $this->buildProxyCookiesJar($_COOKIE, $this->service->getExAppDomain($exApp)), RequestOptions::BODY => $stream, RequestOptions::HEADERS => $this->buildHeadersWithExclude($route, getallheaders()), RequestOptions::TIMEOUT => 0, ]; $response = $this->service->requestToExApp2( $exApp, '/' . $other, $this->userId, 'PUT', queryParams: $_GET, options: $options, request: $this->request, ); fclose($stream); if (is_array($response)) { return (new Response())->setStatus(500); } $this->processBruteforce($bruteforceProtection, $delay, $response->getStatusCode()); return $this->createProxyResponse($other, $response); } #[PublicPage] #[NoAdminRequired] #[NoCSRFRequired] public function ExAppDelete(string $appId, string $other): Response { $route = []; $bruteforceProtection = []; $delay = 0; $exApp = $this->prepareProxy($appId, $other, $route, $bruteforceProtection, $delay); if ($exApp === null) { return new NotFoundResponse(); } $stream = fopen('php://input', 'r'); $options = [ RequestOptions::COOKIES => $this->buildProxyCookiesJar($_COOKIE, $this->service->getExAppDomain($exApp)), RequestOptions::BODY => $stream, RequestOptions::HEADERS => $this->buildHeadersWithExclude($route, getallheaders()), RequestOptions::TIMEOUT => 0, ]; $response = $this->service->requestToExApp2( $exApp, '/' . $other, $this->userId, 'DELETE', queryParams: $_GET, options: $options, request: $this->request, ); fclose($stream); if (is_array($response)) { return (new Response())->setStatus(500); } $this->processBruteforce($bruteforceProtection, $delay, $response->getStatusCode()); return $this->createProxyResponse($other, $response); } private function prepareProxy( string $appId, string $other, array &$route, array &$bruteforceProtection, int &$delay ): ?ExApp { $delay = 0; $exApp = $this->exAppService->getExApp($appId); if ($exApp === null) { $this->logger->debug( sprintf('Returning status 404 for "%s": ExApp is not found.', $other) ); return null; } elseif (!$exApp->getEnabled()) { $this->logger->debug( sprintf('Returning status 404 for "%s": ExApp is not enabled.', $other) ); return null; } $route = $this->passesExAppProxyRoutesChecks($exApp, $other); if (empty($route)) { $this->logger->debug( sprintf('Returning status 404 for "%s": route does not pass the access check.', $other) ); return null; } $bruteforceProtection = isset($route['bruteforce_protection']) ? json_decode($route['bruteforce_protection'], true) : []; if (!empty($bruteforceProtection)) { $delay = $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), Application::APP_ID); } return $exApp; } private function processBruteforce(array $bruteforceProtection, int $delay, int $status): void { if (!empty($bruteforceProtection)) { if ($delay > 0 && ($status >= 200 && $status < 300)) { $this->throttler->resetDelay($this->request->getRemoteAddress(), Application::APP_ID, []); } elseif (in_array($status, $bruteforceProtection)) { $this->throttler->registerAttempt(Application::APP_ID, $this->request->getRemoteAddress()); } } } private function buildProxyCookiesJar(array $cookies, string $domain): CookieJar { $cookieJar = new CookieJar(); foreach ($cookies as $name => $value) { $cookieJar->setCookie(new SetCookie([ 'Domain' => $domain, 'Name' => $name, 'Value' => $value, 'Discard' => true, 'Secure' => false, 'HttpOnly' => true, ])); } return $cookieJar; } /** * Build the multipart form data from input parameters and files */ private function buildMultipartFormData(array $bodyParams, array $files): array { $multipart = []; foreach ($bodyParams as $key => $value) { $multipart[] = [ 'name' => $key, 'contents' => $value, ]; } foreach ($files as $key => $file) { $multipart[] = [ 'name' => $key, 'contents' => fopen($file['tmp_name'], 'r'), 'filename' => $file['name'], ]; } return $multipart; } private function passesExAppProxyRoutesChecks(ExApp $exApp, string $exAppRoute): array { foreach ($exApp->getRoutes() as $route) { if (preg_match('/' . $route['url'] . '/i', $exAppRoute) === 1 && str_contains(strtolower($route['verb']), strtolower($this->request->getMethod())) && $this->passesExAppProxyRouteAccessLevelCheck($route['access_level']) ) { return $route; } } return []; } private function passesExAppProxyRouteAccessLevelCheck(int $accessLevel): bool { return match ($accessLevel) { ExAppRouteAccessLevel::PUBLIC->value => true, ExAppRouteAccessLevel::USER->value => $this->userId !== null, ExAppRouteAccessLevel::ADMIN->value => $this->userId !== null && $this->groupManager->isAdmin($this->userId), default => false, }; } private function buildHeadersWithExclude(array $route, array $headers): array { $headersToExclude = json_decode($route['headers_to_exclude'], true); if (!in_array('x-origin-ip', $headersToExclude)) { $headersToExclude[] = 'x-origin-ip'; } $headersToExclude[] = 'authorization-app-api'; foreach ($headers as $key => $value) { if (in_array(strtolower($key), $headersToExclude)) { unset($headers[$key]); } } $headers['X-Origin-IP'] = $this->request->getRemoteAddress(); return $headers; } }