%PDF- %PDF-
| Direktori : /www/varak.net/nextcloud.varak.net/apps/app_api/lib/Fetcher/ |
| Current File : //www/varak.net/nextcloud.varak.net/apps/app_api/lib/Fetcher/ExAppArchiveFetcher.php |
<?php
declare(strict_types=1);
namespace OCA\AppAPI\Fetcher;
use Exception;
use OC\Archive\TAR;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\ITempManager;
use phpseclib\File\X509;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SimpleXMLElement;
/**
* ExApps release archive fetcher with the same logic as for default (signature check).
*/
class ExAppArchiveFetcher {
public function __construct(
private readonly ITempManager $tempManager,
private readonly IClientService $clientService,
private readonly IConfig $config,
) {
}
/**
* Based on regular app download algorithm.
* Download ExApp release archive, verify signature extract info.xml and return its object
*/
public function downloadInfoXml(array $exAppAppstoreData, string &$extractedDir): ?SimpleXMLElement {
// 1. Signature check
if (!$this->checkExAppSignature($exAppAppstoreData)) {
return null;
}
// 2. Download release archive
$downloadUrl = end($exAppAppstoreData['releases'])['download'];
$releaseSignature = end($exAppAppstoreData['releases'])['signature'];
$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
$timeout = \OC::$CLI ? 0 : 120;
$client = $this->clientService->newClient();
$client->get($downloadUrl, ['sink' => $tempFile, 'timeout' => $timeout]);
// Validate signature of downloaded archive
$certificate = openssl_get_publickey($exAppAppstoreData['certificate']);
if (openssl_verify(file_get_contents($tempFile), base64_decode($releaseSignature), $certificate, OPENSSL_ALGO_SHA512) !== 1) {
return null;
}
$extractDir = $this->tempManager->getTemporaryFolder();
$archive = new TAR($tempFile);
if (!$archive->extract($extractDir)) {
return null;
}
$allFiles = scandir($extractDir);
$folders = array_values(array_diff($allFiles, ['.', '..']));
if (count($folders) > 1) {
return null;
}
// 3. Parse info.xml and return its object
$infoXml = simplexml_load_string(file_get_contents($extractDir . '/' . $folders[0] . '/appinfo/info.xml'));
if ((string) $infoXml->id !== $exAppAppstoreData['id']) {
return null;
}
$extractedDir = $extractDir . '/' . $folders[0];
return $infoXml;
}
public function installTranslations(string $appId, string $dirTranslations): string {
if (!file_exists($dirTranslations)) {
return sprintf('Can not access directory: %s', $dirTranslations);
}
$writableAppPath = $this->getExAppFolder($appId);
if (!$writableAppPath) {
return 'Can not find writable apps path to perform installation.';
}
$installL10NPath = $writableAppPath . '/l10n';
if (file_exists($installL10NPath)) {
$this->rmdirr($installL10NPath); // Remove old l10n folder and files if exists
}
$this->copyr($dirTranslations, $installL10NPath);
return '';
}
public function getExAppFolder(string $appId): ?string {
$appsPaths = $this->config->getSystemValue('apps_paths');
$existingPath = '';
foreach ($appsPaths as $appPath) {
if ($appPath['writable'] && file_exists($appPath['path'] . '/' . $appId)) {
$existingPath = $appPath['path'] . '/' . $appId;
}
}
if (!empty($existingPath)) {
return $existingPath;
}
foreach ($appsPaths as $appPath) {
if ($appPath['writable']) {
if (mkdir($appPath['path'] . '/' . $appId)) {
return $appPath['path'] . '/' . $appId;
}
}
}
return null;
}
public function removeExAppFolder(string $appId): void {
foreach ($this->config->getSystemValue('apps_paths') as $appPath) {
if ($appPath['writable']) {
if (file_exists($appPath['path'] . '/' . $appId)) {
$this->rmdirr($appPath['path'] . '/' . $appId);
}
}
}
}
/**
* @psalm-suppress UndefinedClass
*/
private function checkExAppSignature(array $exAppAppstoreData): bool {
$appId = $exAppAppstoreData['id'];
$certificate = new X509();
$rootCrt = file_get_contents(\OC::$SERVERROOT . '/resources/codesigning/root.crt');
$rootCrts = $this->splitCerts($rootCrt);
foreach ($rootCrts as $rootCrt) {
$certificate->loadCA($rootCrt);
}
$loadedCertificate = $certificate->loadX509($exAppAppstoreData['certificate']);
// Verify if the certificate has been revoked
$crl = new X509();
foreach ($rootCrts as $rootCrt) {
$crl->loadCA($rootCrt);
}
$crl->loadCRL(file_get_contents(\OC::$SERVERROOT . '/resources/codesigning/root.crl'));
if ($crl->validateSignature() !== true) {
throw new Exception('Could not validate CRL signature');
}
$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
$revoked = $crl->getRevoked($csn);
if ($revoked !== false) {
throw new Exception(
sprintf(
'Certificate "%s" has been revoked',
$csn
)
);
}
// Verify if the certificate has been issued by the Nextcloud Code Authority CA
if ($certificate->validateSignature() !== true) {
throw new Exception(
sprintf(
'App with id %s has a certificate not issued by a trusted Code Signing Authority',
$appId
)
);
}
// Verify if the certificate is issued for the requested app id
$certInfo = openssl_x509_parse($exAppAppstoreData['certificate']);
if (!isset($certInfo['subject']['CN'])) {
throw new Exception(
sprintf(
'App with id %s has a cert with no CN',
$appId
)
);
}
if ($certInfo['subject']['CN'] !== $appId) {
throw new Exception(
sprintf(
'App with id %s has a cert issued to %s',
$appId,
$certInfo['subject']['CN']
)
);
}
return true;
}
/**
* Split the certificate file in individual certs
*
* @return string[]
*/
private function splitCerts(string $cert): array {
preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
return $matches[0];
}
public function rmdirr(string $dir, bool $deleteSelf = true): bool {
if (is_dir($dir)) {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileInfo) {
if ($fileInfo->isLink()) {
unlink($fileInfo->getPathname());
} elseif ($fileInfo->isDir()) {
rmdir($fileInfo->getRealPath());
} else {
unlink($fileInfo->getRealPath());
}
}
if ($deleteSelf) {
rmdir($dir);
}
} elseif (file_exists($dir)) {
if ($deleteSelf) {
unlink($dir);
}
}
if (!$deleteSelf) {
return true;
}
return !file_exists($dir);
}
public function copyr(string $src, string $dest): void {
if (is_dir($src)) {
if (!is_dir($dest)) {
mkdir($dest);
}
$files = scandir($src);
foreach ($files as $file) {
if ($file != "." && $file != "..") {
self::copyr("$src/$file", "$dest/$file");
}
}
} elseif (file_exists($src)) {
copy($src, $dest);
}
}
}