Skip to content

Commit 28b9319

Browse files
authored
Merge pull request #448 from nextcloud/feat/ssl-certificates-update
copy SSL certificates from NC instance to ExApp upon install/update action
2 parents 217a6f9 + c37b8eb commit 28b9319

File tree

2 files changed

+178
-6
lines changed

2 files changed

+178
-6
lines changed

.github/workflows/tests-deploy.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,15 @@ jobs:
340340
docker exec nextcloud sudo -u www-data php occ app_api:app:register app-skeleton-python docker_by_port \
341341
--info-xml https://raw.githubusercontent.com/nextcloud/app-skeleton-python/main/appinfo/info.xml
342342
docker exec nextcloud sudo -u www-data php occ app_api:app:enable app-skeleton-python
343+
344+
- name: Checking if ExApp container can access HTTPS DSP
345+
run: |
346+
docker exec nc_app_app-skeleton-python apt update
347+
docker exec nc_app_app-skeleton-python apt install curl -y
348+
docker exec nc_app_app-skeleton-python curl --resolve host.docker.internal:2375:172.17.0.1 https://host.docker.internal:2375
349+
350+
- name: Disable ExApp
351+
run: |
343352
docker exec nextcloud sudo -u www-data php occ app_api:app:disable app-skeleton-python
344353
345354
- name: Copy NC log to host

lib/DeployActions/DockerActions.php

Lines changed: 169 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,21 @@
1212
use Exception;
1313
use GuzzleHttp\Client;
1414
use GuzzleHttp\Exception\GuzzleException;
15-
1615
use OCA\AppAPI\AppInfo\Application;
1716
use OCA\AppAPI\Db\DaemonConfig;
17+
1818
use OCA\AppAPI\Db\ExApp;
1919
use OCA\AppAPI\Service\AppAPICommonService;
20-
2120
use OCA\AppAPI\Service\ExAppService;
2221
use OCP\App\IAppManager;
22+
2323
use OCP\ICertificateManager;
2424
use OCP\IConfig;
25+
use OCP\ITempManager;
2526
use OCP\IURLGenerator;
2627
use OCP\Security\ICrypto;
28+
use Phar;
29+
use PharData;
2730
use Psr\Log\LoggerInterface;
2831

2932
class DockerActions implements IDeployActions {
@@ -42,6 +45,7 @@ public function __construct(
4245
private readonly IURLGenerator $urlGenerator,
4346
private readonly AppAPICommonService $service,
4447
private readonly ExAppService $exAppService,
48+
private readonly ITempManager $tempManager,
4549
private readonly ICrypto $crypto,
4650
) {
4751
}
@@ -69,9 +73,10 @@ public function deployExApp(ExApp $exApp, DaemonConfig $daemonConfig, array $par
6973
}
7074

7175
$this->exAppService->setAppDeployProgress($exApp, 95);
72-
$containerInfo = $this->inspectContainer($dockerUrl, $this->buildExAppContainerName($params['container_params']['name']));
76+
$containerName = $this->buildExAppContainerName($params['container_params']['name']);
77+
$containerInfo = $this->inspectContainer($dockerUrl, $containerName);
7378
if (isset($containerInfo['Id'])) {
74-
$result = $this->removeContainer($dockerUrl, $this->buildExAppContainerName($params['container_params']['name']));
79+
$result = $this->removeContainer($dockerUrl, $containerName);
7580
if ($result) {
7681
return $result;
7782
}
@@ -82,18 +87,176 @@ public function deployExApp(ExApp $exApp, DaemonConfig $daemonConfig, array $par
8287
return $result['error'];
8388
}
8489
$this->exAppService->setAppDeployProgress($exApp, 97);
85-
$result = $this->startContainer($dockerUrl, $this->buildExAppContainerName($params['container_params']['name']));
90+
91+
$this->updateCerts($dockerUrl, $containerName);
92+
$this->exAppService->setAppDeployProgress($exApp, 98);
93+
94+
$result = $this->startContainer($dockerUrl, $containerName);
8695
if (isset($result['error'])) {
8796
return $result['error'];
8897
}
8998
$this->exAppService->setAppDeployProgress($exApp, 99);
90-
if (!$this->waitTillContainerStart($this->buildExAppContainerName($exApp->getAppid()), $daemonConfig)) {
99+
if (!$this->waitTillContainerStart($containerName, $daemonConfig)) {
91100
return 'container startup failed';
92101
}
93102
$this->exAppService->setAppDeployProgress($exApp, 100);
94103
return '';
95104
}
96105

106+
private function updateCerts(string $dockerUrl, string $containerName): void {
107+
try {
108+
$this->startContainer($dockerUrl, $containerName);
109+
110+
$osInfo = $this->getContainerOsInfo($dockerUrl, $containerName);
111+
if (!$this->isSupportedOs($osInfo)) {
112+
$this->logger->warning(sprintf(
113+
"Unsupported OS detected for container: %s. OS info: %s",
114+
$containerName,
115+
$osInfo
116+
));
117+
return;
118+
}
119+
120+
$bundlePath = $this->certificateManager->getAbsoluteBundlePath();
121+
$targetDir = $this->getTargetCertDir($osInfo); // Determine target directory based on OS
122+
$this->executeCommandInContainer($dockerUrl, $containerName, ['mkdir', '-p', $targetDir]);
123+
$this->installParsedCertificates($dockerUrl, $containerName, $bundlePath, $targetDir);
124+
125+
$updateCommand = $this->getCertificateUpdateCommand($osInfo);
126+
$this->executeCommandInContainer($dockerUrl, $containerName, $updateCommand);
127+
} catch (Exception $e) {
128+
$this->logger->warning(sprintf(
129+
"Failed to update certificates in container: %s. Error: %s",
130+
$containerName,
131+
$e->getMessage()
132+
));
133+
} finally {
134+
$this->stopContainer($dockerUrl, $containerName);
135+
}
136+
}
137+
138+
private function parseCertificatesFromBundle(string $bundlePath): array {
139+
$contents = file_get_contents($bundlePath);
140+
141+
// Match only certificates
142+
preg_match_all('/-----BEGIN CERTIFICATE-----(.+?)-----END CERTIFICATE-----/s', $contents, $matches);
143+
144+
return $matches[0] ?? [];
145+
}
146+
147+
private function installParsedCertificates(string $dockerUrl, string $containerId, string $bundlePath, string $targetDir): void {
148+
$certificates = $this->parseCertificatesFromBundle($bundlePath);
149+
$tempDir = sys_get_temp_dir();
150+
151+
foreach ($certificates as $index => $certificate) {
152+
$tempFile = $tempDir . "/{$containerId}_cert_{$index}.crt";
153+
if (file_exists($tempFile)) {
154+
unlink($tempFile);
155+
}
156+
file_put_contents($tempFile, $certificate);
157+
158+
// Build the path in the container
159+
$pathInContainer = $targetDir . "/custom_cert_$index.crt";
160+
161+
$this->dockerCopy($dockerUrl, $containerId, $tempFile, $pathInContainer);
162+
unlink($tempFile);
163+
}
164+
}
165+
166+
private function dockerCopy(string $dockerUrl, string $containerId, string $sourcePath, string $pathInContainer): void {
167+
$archivePath = $this->createTarArchive($sourcePath, $pathInContainer);
168+
$url = $this->buildApiUrl($dockerUrl, sprintf('containers/%s/archive?path=%s', $containerId, urlencode('/')));
169+
170+
try {
171+
$archiveData = file_get_contents($archivePath);
172+
$this->guzzleClient->put($url, [
173+
'body' => $archiveData,
174+
'headers' => ['Content-Type' => 'application/x-tar']
175+
]);
176+
} catch (Exception $e) {
177+
throw new Exception(sprintf("Failed to copy %s to container %s: %s", $sourcePath, $containerId, $e->getMessage()));
178+
} finally {
179+
if (file_exists($archivePath)) {
180+
unlink($archivePath);
181+
}
182+
}
183+
}
184+
185+
private function getTargetCertDir(string $osInfo): string {
186+
if (stripos($osInfo, 'alpine') !== false) {
187+
return '/usr/local/share/ca-certificates'; // Alpine Linux
188+
}
189+
190+
if (stripos($osInfo, 'debian') !== false || stripos($osInfo, 'ubuntu') !== false) {
191+
return '/usr/local/share/ca-certificates'; // Debian and Ubuntu
192+
}
193+
194+
if (stripos($osInfo, 'centos') !== false || stripos($osInfo, 'almalinux') !== false) {
195+
return '/etc/pki/ca-trust/source/anchors'; // CentOS and AlmaLinux
196+
}
197+
198+
throw new Exception(sprintf('Unsupported OS: %s', $osInfo));
199+
}
200+
201+
private function createTarArchive(string $filePath, string $pathInContainer): string {
202+
$tempFile = $this->tempManager->getTemporaryFile('.tar');
203+
204+
try {
205+
if (file_exists($tempFile)) {
206+
unlink($tempFile);
207+
}
208+
209+
$archive = new PharData($tempFile, 0, null, Phar::TAR);
210+
$relativePathInArchive = ltrim($pathInContainer, '/');
211+
$archive->addFile($filePath, $relativePathInArchive);
212+
} catch (\Exception $e) {
213+
// Clean up the temporary file in case of an error
214+
if (file_exists($tempFile)) {
215+
unlink($tempFile);
216+
}
217+
throw new Exception(sprintf("Failed to create tar archive: %s", $e->getMessage()));
218+
}
219+
return $tempFile; // Return the path to the TAR archive
220+
}
221+
222+
private function getCertificateUpdateCommand(string $osInfo): string {
223+
if (stripos($osInfo, 'alpine') !== false) {
224+
return 'update-ca-certificates';
225+
}
226+
if (stripos($osInfo, 'debian') !== false || stripos($osInfo, 'ubuntu') !== false) {
227+
return 'update-ca-certificates';
228+
}
229+
if (stripos($osInfo, 'centos') !== false || stripos($osInfo, 'almalinux') !== false) {
230+
return 'update-ca-trust extract';
231+
}
232+
throw new Exception('Unsupported OS');
233+
}
234+
235+
private function getContainerOsInfo(string $dockerUrl, string $containerId): string {
236+
$command = ['cat', '/etc/os-release'];
237+
return $this->executeCommandInContainer($dockerUrl, $containerId, $command);
238+
}
239+
240+
private function isSupportedOs(string $osInfo): bool {
241+
return (bool) preg_match('/(alpine|debian|ubuntu|centos|almalinux)/i', $osInfo);
242+
}
243+
244+
private function executeCommandInContainer(string $dockerUrl, string $containerId, $command): string {
245+
$url = $this->buildApiUrl($dockerUrl, sprintf('containers/%s/exec', $containerId));
246+
$payload = [
247+
'Cmd' => is_array($command) ? $command : explode(' ', $command),
248+
'AttachStdout' => true,
249+
'AttachStderr' => true,
250+
];
251+
$response = $this->guzzleClient->post($url, ['json' => $payload]);
252+
$execId = json_decode((string) $response->getBody(), true)['Id'];
253+
254+
// Start the exec process
255+
$startUrl = $this->buildApiUrl($dockerUrl, sprintf('exec/%s/start', $execId));
256+
$startResponse = $this->guzzleClient->post($startUrl, ['json' => ['Detach' => false, 'Tty' => false]]);
257+
return (string) $startResponse->getBody();
258+
}
259+
97260
public function buildApiUrl(string $dockerUrl, string $route): string {
98261
return sprintf('%s/%s/%s', $dockerUrl, self::DOCKER_API_VERSION, $route);
99262
}

0 commit comments

Comments
 (0)