Initial import

This commit is contained in:
Nextcloud Team 2021-11-30 11:20:42 +01:00 committed by Lukas Reschke
commit 2295a33590
884 changed files with 93939 additions and 0 deletions

View file

@ -0,0 +1,34 @@
<?php
namespace AIO\Auth;
use AIO\Data\ConfigurationManager;
class AuthManager {
private const SESSION_KEY = 'aio_authenticated';
private ConfigurationManager $configurationManager;
public function __construct(ConfigurationManager $configurationManager) {
$this->configurationManager = $configurationManager;
}
public function CheckCredentials(string $username, string $password) : bool {
if($username === $this->configurationManager->GetUserName()) {
return hash_equals($this->configurationManager->GetPassword(), $password);
}
return false;
}
public function CheckToken(string $token) : bool {
return hash_equals($this->configurationManager->GetToken(), $token);
}
public function SetAuthState(bool $isLoggedIn) : void {
$_SESSION[self::SESSION_KEY] = $isLoggedIn;
}
public function IsAuthenticated() : bool {
return isset($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY] === true;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,112 @@
<?php
namespace AIO\Container;
use AIO\Container\State\IContainerState;
use AIO\Data\ConfigurationManager;
use AIO\Docker\DockerActionManager;
use AIO\ContainerDefinitionFetcher;
class Container {
private string $identifier;
private string $displayName;
private string $containerName;
private string $restartPolicy;
private int $maxShutdownTime;
private ContainerPorts $ports;
private ContainerInternalPorts $internalPorts;
private ContainerVolumes $volumes;
private ContainerEnvironmentVariables $containerEnvironmentVariables;
/** @var string[] */
private array $dependsOn;
/** @var string[] */
private array $secrets;
private DockerActionManager $dockerActionManager;
public function __construct(
string $identifier,
string $displayName,
string $containerName,
string $restartPolicy,
int $maxShutdownTime,
ContainerPorts $ports,
ContainerInternalPorts $internalPorts,
ContainerVolumes $volumes,
ContainerEnvironmentVariables $containerEnvironmentVariables,
array $dependsOn,
array $secrets,
DockerActionManager $dockerActionManager
) {
$this->identifier = $identifier;
$this->displayName = $displayName;
$this->containerName = $containerName;
$this->restartPolicy = $restartPolicy;
$this->maxShutdownTime = $maxShutdownTime;
$this->ports = $ports;
$this->internalPorts = $internalPorts;
$this->volumes = $volumes;
$this->containerEnvironmentVariables = $containerEnvironmentVariables;
$this->dependsOn = $dependsOn;
$this->secrets = $secrets;
$this->dockerActionManager = $dockerActionManager;
}
public function GetIdentifier() : string {
return $this->identifier;
}
public function GetDisplayName() : string {
return $this->displayName;
}
public function GetContainerName() : string {
return $this->containerName;
}
public function GetRestartPolicy() : string {
return $this->restartPolicy;
}
public function GetMaxShutdownTime() : int {
return $this->maxShutdownTime;
}
public function GetSecrets() : array {
return $this->secrets;
}
public function GetPorts() : ContainerPorts {
return $this->ports;
}
public function GetInternalPorts() : ContainerInternalPorts {
return $this->internalPorts;
}
public function GetVolumes() : ContainerVolumes {
return $this->volumes;
}
public function GetRunningState() : IContainerState {
return $this->dockerActionManager->GetContainerRunningState($this);
}
public function GetUpdateState() : IContainerState {
return $this->dockerActionManager->GetContainerUpdateState($this);
}
public function GetStartingState() : IContainerState {
return $this->dockerActionManager->GetContainerStartingState($this);
}
/**
* @return string[]
*/
public function GetDependsOn() : array {
return $this->dependsOn;
}
public function GetEnvironmentVariables() : ContainerEnvironmentVariables {
return $this->containerEnvironmentVariables;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerEnvironmentVariables {
/** @var string[] */
private array $variables = [];
public function AddVariable(string $variable) : void {
$this->variables[] = $variable;
}
/**
* @return string[]
*/
public function GetVariables() : array {
return $this->variables;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerInternalPorts {
/** @var string[] */
private array $internalPorts = [];
public function AddInternalPort(string $internalPort) : void {
$this->internalPorts[] = $internalPort;
}
/**
* @return string[]
*/
public function GetInternalPorts() : array {
return $this->internalPorts;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerPorts {
/** @var string[] */
private array $ports = [];
public function AddPort(string $port) : void {
$this->ports[] = $port;
}
/**
* @return string[]
*/
public function GetPorts() : array {
return $this->ports;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerVolume {
public string $name;
public string $mountPoint;
public bool $isWritable;
public function __construct(
string $name,
string $mountPoint,
bool $isWritable
) {
$this->name = $name;
$this->mountPoint = $mountPoint;
$this->isWritable = $isWritable;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace AIO\Container;
class ContainerVolumes {
/** @var ContainerVolume[] */
private array $volumes = [];
public function AddVolume(ContainerVolume $volume) {
$this->volumes[] = $volume;
}
/**
* @return ContainerVolume[]
*/
public function GetVolumes() : array {
return $this->volumes;
}
}

View file

@ -0,0 +1,5 @@
<?php
namespace AIO\Container\State;
interface IContainerState {}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class ImageDoesNotExistState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class RunningState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class StartingState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class StoppedState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class VersionDifferentState implements IContainerState
{}

View file

@ -0,0 +1,6 @@
<?php
namespace AIO\Container\State;
class VersionEqualState implements IContainerState
{}

View file

@ -0,0 +1,136 @@
<?php
namespace AIO;
use AIO\Container\Container;
use AIO\Container\ContainerEnvironmentVariables;
use AIO\Container\ContainerPorts;
use AIO\Container\ContainerInternalPorts;
use AIO\Container\ContainerVolume;
use AIO\Container\ContainerVolumes;
use AIO\Container\State\RunningState;
use AIO\Data\ConfigurationManager;
use AIO\Data\DataConst;
use AIO\Docker\DockerActionManager;
class ContainerDefinitionFetcher
{
private ConfigurationManager $configurationManager;
private \DI\Container $container;
public function __construct(
ConfigurationManager $configurationManager,
\DI\Container $container
)
{
$this->configurationManager = $configurationManager;
$this->container = $container;
}
public function GetContainerById(string $id): ?Container
{
$containers = $this->FetchDefinition();
foreach ($containers as $container) {
if ($container->GetIdentifier() === $id) {
return $container;
}
}
return null;
}
/**
* @return array
*/
private function GetDefinition(bool $latest): array
{
$data = json_decode(file_get_contents(__DIR__ . '/../containers.json'), true);
$containers = [];
foreach ($data['production'] as $entry) {
$ports = new ContainerPorts();
foreach ($entry['ports'] as $port) {
$ports->AddPort($port);
}
$internalPorts = new ContainerInternalPorts();
foreach ($entry['internalPorts'] as $internalPort) {
$internalPorts->AddInternalPort($internalPort);
}
$volumes = new ContainerVolumes();
foreach ($entry['volumes'] as $value) {
if($value['name'] === '%BORGBACKUP_HOST_LOCATION%') {
$value['name'] = $this->configurationManager->GetBorgBackupHostLocation();
if($value['name'] === '') {
continue;
}
}
$volumes->AddVolume(
new ContainerVolume(
$value['name'],
$value['location'],
$value['writeable']
)
);
}
$variables = new ContainerEnvironmentVariables();
foreach ($entry['environmentVariables'] as $value) {
$variables->AddVariable($value);
}
$containers[] = new Container(
$entry['identifier'],
$entry['displayName'],
$entry['containerName'],
$entry['restartPolicy'],
$entry['maxShutdownTime'],
$ports,
$internalPorts,
$volumes,
$variables,
$entry['dependsOn'],
$entry['secrets'],
$this->container->get(DockerActionManager::class)
);
}
return $containers;
}
public function FetchDefinition(): array
{
if (!file_exists(DataConst::GetDataDirectory() . '/containers.json')) {
$containers = $this->GetDefinition(true);
} else {
$containers = $this->GetDefinition(false);
}
$borgBackupMode = $this->configurationManager->GetBorgBackupMode();
$fetchLatest = false;
foreach ($containers as $container) {
if ($container->GetIdentifier() === 'nextcloud-aio-borgbackup') {
if ($container->GetRunningState() === RunningState::class) {
if ($borgBackupMode !== 'backup' && $borgBackupMode !== 'restore') {
$fetchLatest = true;
}
} else {
$fetchLatest = true;
}
} elseif ($container->GetIdentifier() === 'nextcloud-aio-watchtower' && $container->GetRunningState() === RunningState::class) {
return $containers;
}
}
if ($fetchLatest === true) {
$containers = $this->GetDefinition(true);
}
return $containers;
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace AIO\Controller;
use AIO\ContainerDefinitionFetcher;
use AIO\Data\ConfigurationManager;
use AIO\Data\InvalidSettingConfigurationException;
use AIO\Docker\DockerActionManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class ConfigurationController
{
private ConfigurationManager $configurationManager;
public function __construct(
ConfigurationManager $configurationManager
) {
$this->configurationManager = $configurationManager;
}
public function SetConfig(Request $request, Response $response, $args) : Response {
try {
if (isset($request->getParsedBody()['domain'])) {
$this->configurationManager->SetDomain($request->getParsedBody()['domain']);
}
if (isset($request->getParsedBody()['borg_backup_host_location'])) {
$this->configurationManager->SetBorgBackupHostLocation($request->getParsedBody()['borg_backup_host_location']);
}
return $response->withStatus(201)->withHeader('Location', '/');
} catch (InvalidSettingConfigurationException $ex) {
$response->getBody()->write($ex->getMessage());
return $response->withStatus(422);
}
}
}

View file

@ -0,0 +1,174 @@
<?php
namespace AIO\Controller;
use AIO\Container\State\RunningState;
use AIO\ContainerDefinitionFetcher;
use AIO\Docker\DockerActionManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use AIO\Data\ConfigurationManager;
class DockerController
{
private DockerActionManager $dockerActionManager;
private ContainerDefinitionFetcher $containerDefinitionFetcher;
private const TOP_CONTAINER = 'nextcloud-aio-apache';
private ConfigurationManager $configurationManager;
public function __construct(
DockerActionManager $dockerActionManager,
ContainerDefinitionFetcher $containerDefinitionFetcher,
ConfigurationManager $configurationManager
) {
$this->dockerActionManager = $dockerActionManager;
$this->containerDefinitionFetcher = $containerDefinitionFetcher;
$this->configurationManager = $configurationManager;
}
private function PerformRecursiveContainerStart(string $id) {
$container = $this->containerDefinitionFetcher->GetContainerById($id);
foreach($container->GetDependsOn() as $dependency) {
$this->PerformRecursiveContainerStart($dependency);
}
$this->dockerActionManager->DeleteContainer($container);
$this->dockerActionManager->CreateVolumes($container);
$this->dockerActionManager->PullContainer($container);
$this->dockerActionManager->CreateContainer($container);
$this->dockerActionManager->StartContainer($container);
$this->dockerActionManager->ConnectContainerToNetwork($container);
}
public function GetLogs(Request $request, Response $response, $args) : Response
{
$id = $request->getQueryParams()['id'];
$container = $this->containerDefinitionFetcher->GetContainerById($id);
$logs = $this->dockerActionManager->GetLogs($container);
$body = $response->getBody();
$body->write($logs);
return $response
->withStatus(200)
->withHeader('Content-Type', 'text/plain; charset=utf-8')
->withHeader('Content-Disposition', 'inline');
}
public function StartBackupContainerBackup(Request $request, Response $response, $args) : Response {
$config = $this->configurationManager->GetConfig();
$config['backup-mode'] = 'backup';
$this->configurationManager->WriteConfig($config);
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStop($id);
$id = 'nextcloud-aio-borgbackup';
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartBackupContainerCheck(Request $request, Response $response, $args) : Response {
$config = $this->configurationManager->GetConfig();
$config['backup-mode'] = 'check';
$this->configurationManager->WriteConfig($config);
$id = 'nextcloud-aio-borgbackup';
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartBackupContainerRestore(Request $request, Response $response, $args) : Response {
$config = $this->configurationManager->GetConfig();
$config['backup-mode'] = 'restore';
$this->configurationManager->WriteConfig($config);
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStop($id);
$id = 'nextcloud-aio-borgbackup';
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartContainer(Request $request, Response $response, $args) : Response
{
$uri = $request->getUri();
$host = $uri->getHost();
$port = $uri->getPort();
$config = $this->configurationManager->GetConfig();
// set AIO_URL
$config['AIO_URL'] = $host . ':' . $port;
// set wasStartButtonClicked
$config['wasStartButtonClicked'] = 1;
// set AIO_TOKEN
$config['AIO_TOKEN'] = bin2hex(random_bytes(24));
$this->configurationManager->WriteConfig($config);
$this->configurationManager->SetIsContainerUpateAvailable(false);
// Stop domaincheck since apache would not be able to start otherwise
$this->StopDomaincheckContainer();
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartWatchtowerContainer(Request $request, Response $response, $args) : Response {
$id = 'nextcloud-aio-watchtower';
$this->PerformRecursiveContainerStart($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
private function PerformRecursiveContainerStop(string $id)
{
$container = $this->containerDefinitionFetcher->GetContainerById($id);
foreach($container->GetDependsOn() as $dependency) {
$this->PerformRecursiveContainerStop($dependency);
}
$this->dockerActionManager->DisconnectContainerFromNetwork($container);
$this->dockerActionManager->StopContainer($container);
}
public function StopContainer(Request $request, Response $response, $args) : Response
{
$id = self::TOP_CONTAINER;
$this->PerformRecursiveContainerStop($id);
return $response->withStatus(201)->withHeader('Location', '/');
}
public function StartDomaincheckContainer()
{
# Don't start if domain is already set
if ($this->configurationManager->GetDomain() != '') {
return;
}
$id = 'nextcloud-aio-domaincheck';
$container = $this->containerDefinitionFetcher->GetContainerById($id);
// don't start if the domaincheck is already running
if ($container->GetIdentifier() === $id && $container->GetRunningState() instanceof RunningState) {
return;
// don't start if apache is already running
} elseif ($container->GetIdentifier() === self::TOP_CONTAINER && $container->GetRunningState() instanceof RunningState) {
return;
}
$this->PerformRecursiveContainerStart($id);
}
private function StopDomaincheckContainer()
{
$id = 'nextcloud-aio-domaincheck';
$this->PerformRecursiveContainerStop($id);
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace AIO\Controller;
use AIO\Auth\AuthManager;
use AIO\Container\Container;
use AIO\ContainerDefinitionFetcher;
use AIO\Docker\DockerActionManager;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class LoginController
{
private AuthManager $authManager;
public function __construct(AuthManager $authManager) {
$this->authManager = $authManager;
}
public function TryLogin(Request $request, Response $response, $args) : Response {
$userName = $request->getParsedBody()['username'];
$password = $request->getParsedBody()['password'];
if($this->authManager->CheckCredentials($userName, $password)) {
$this->authManager->SetAuthState(true);
return $response->withHeader('Location', '/')->withStatus(302);
}
return $response->withHeader('Location', '/')->withStatus(302);
}
public function GetTryLogin(Request $request, Response $response, $args) : Response {
$token = $request->getQueryParams()['token'];
if($this->authManager->CheckToken($token)) {
$this->authManager->SetAuthState(true);
return $response->withHeader('Location', '/')->withStatus(302);
}
return $response->withHeader('Location', '/')->withStatus(302);
}
public function Logout(Request $request, Response $response, $args) : Response
{
$this->authManager->SetAuthState(false);
return $response
->withHeader('Location', '/')
->withStatus(302);
}
}

View file

@ -0,0 +1,230 @@
<?php
namespace AIO\Data;
use AIO\Auth\PasswordGenerator;
use AIO\Controller\DockerController;
class ConfigurationManager
{
public function GetConfig() : array
{
if(file_exists(DataConst::GetConfigFile()))
{
$configContent = file_get_contents(DataConst::GetConfigFile());
return json_decode($configContent, true);
}
return [];
}
public function GetUserName() : string {
return $this->GetConfig()['username'];
}
public function GetPassword() : string {
return $this->GetConfig()['password'];
}
public function GetToken() : string {
return $this->GetConfig()['AIO_TOKEN'];
}
public function GetIsContainerUpateAvailable() : bool {
return isset($this->GetConfig()['isContainerUpateAvailable']) ? $this->GetConfig()['isContainerUpateAvailable'] : false;
}
public function SetIsContainerUpateAvailable(bool $value) : void {
$config = $this->GetConfig();
$config['isContainerUpateAvailable'] = $value;
$this->WriteConfig($config);
}
public function SetPassword(string $password) : void {
$config = $this->GetConfig();
$config['username'] = 'admin';
$config['password'] = $password;
$this->WriteConfig($config);
}
public function GetSecret(string $secretId) : string {
$config = $this->GetConfig();
if(!isset($config['secrets'][$secretId])) {
$config['secrets'][$secretId] = bin2hex(random_bytes(24));
$this->WriteConfig($config);
}
if ($secretId === 'BORGBACKUP_PASSWORD' && !file_exists(DataConst::GetBackupSecretFile())) {
$this->DoubleSafeBackupSecret($config['secrets'][$secretId]);
}
return $config['secrets'][$secretId];
}
private function DoubleSafeBackupSecret(string $borgBackupPassword) {
file_put_contents(DataConst::GetBackupSecretFile(), $borgBackupPassword);
}
public function hasBackupRunOnce() : bool {
if (!file_exists(DataConst::GetBackupKeyFile())) {
return false;
} else {
return true;
}
}
public function GetLastBackupTime() : string {
if (!file_exists(DataConst::GetBackupArchivesList())) {
return '';
}
$content = file_get_contents(DataConst::GetBackupArchivesList());
if ($content === "") {
return '';
}
$lastBackupLines = explode("\n", $content);
$lastBackupLine = $lastBackupLines[sizeof($lastBackupLines) - 2];
if ($lastBackupLine === "") {
return '';
}
$lastBackupTimes = explode(",", $lastBackupLine);
$lastBackupTime = $lastBackupTimes[1];
if ($lastBackupTime === "") {
return '';
}
return $lastBackupTime;
}
public function wasStartButtonClicked() : bool {
if (isset($this->GetConfig()['wasStartButtonClicked'])) {
return true;
} else {
return false;
}
}
/**
* @throws InvalidSettingConfigurationException
*/
public function SetDomain(string $domain) : void {
// Validate URL
if (!filter_var('http://' . $domain, FILTER_VALIDATE_URL)) {
throw new InvalidSettingConfigurationException("Domain is not in a valid format!");
}
$dnsRecordIP = gethostbyname($domain);
// Validate IP
if(!filter_var($dnsRecordIP, FILTER_VALIDATE_IP)) {
throw new InvalidSettingConfigurationException("DNS config is not set or domain is not in a valid format!");
}
$connection = @fsockopen($domain, 443, $errno, $errstr, 0.1);
if ($connection) {
fclose($connection);
} else {
throw new InvalidSettingConfigurationException("The server is not reachable on Port 443.");
}
// Get Instance ID
$instanceID = $this->GetSecret('INSTANCE_ID');
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,'http://' . $domain . ':443');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
# Get rid of trailing \n
$response = str_replace("\n", "", $response);
if($response !== $instanceID) {
throw new InvalidSettingConfigurationException("Domain does not point to this server.");
}
// Write domain
$config = $this->GetConfig();
$config['domain'] = $domain;
$this->WriteConfig($config);
}
public function GetDomain() : string {
$config = $this->GetConfig();
if(!isset($config['domain'])) {
$config['domain'] = '';
}
return $config['domain'];
}
public function GetBackupMode() : string {
$config = $this->GetConfig();
if(!isset($config['backup-mode'])) {
$config['backup-mode'] = '';
}
return $config['backup-mode'];
}
public function GetAIOURL() : string {
$config = $this->GetConfig();
if(!isset($config['AIO_URL'])) {
$config['AIO_URL'] = '';
}
return $config['AIO_URL'];
}
/**
* @throws InvalidSettingConfigurationException
*/
public function SetBorgBackupHostLocation(string $location) : void {
$allowedPrefixes = [
'/mnt/',
'/media/',
];
$isValidPath = false;
foreach($allowedPrefixes as $allowedPrefix) {
if(str_starts_with($location, $allowedPrefix)) {
$isValidPath = true;
break;
}
}
if(!$isValidPath) {
throw new InvalidSettingConfigurationException("Path must start with /mnt/ or /media/.");
}
$config = $this->GetConfig();
$config['borg_backup_host_location'] = $location;
$this->WriteConfig($config);
}
public function WriteConfig(array $config) : void {
if(!is_dir(DataConst::GetDataDirectory())) {
mkdir(DataConst::GetDataDirectory());
}
file_put_contents(DataConst::GetConfigFile(), json_encode($config));
}
public function GetBorgBackupHostLocation() : string {
$config = $this->GetConfig();
if(!isset($config['borg_backup_host_location'])) {
$config['borg_backup_host_location'] = '';
}
return $config['borg_backup_host_location'];
}
public function GetBorgBackupMode() : string {
$config = $this->GetConfig();
if(!isset($config['backup-mode'])) {
$config['backup-mode'] = '';
}
return $config['backup-mode'];
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace AIO\Data;
class DataConst {
public static function GetDataDirectory() : string {
if(is_dir('/mnt/docker-aio-config/data/')) {
return '/mnt/docker-aio-config/data/';
}
return realpath(__DIR__ . '/../../data/');
}
public static function GetSessionDirectory() : string {
if(is_dir('/mnt/docker-aio-config/session/')) {
return '/mnt/docker-aio-config/session/';
}
return realpath(__DIR__ . '/../../session/');
}
public static function GetConfigFile() : string {
return self::GetDataDirectory() . '/configuration.json';
}
public static function GetBackupSecretFile() : string {
return self::GetDataDirectory() . '/backupsecret';
}
public static function GetBackupKeyFile() : string {
return self::GetDataDirectory() . '/borg.config';
}
public static function GetBackupArchivesList() : string {
return self::GetDataDirectory() . '/backup_archives.list';
}
}

View file

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
namespace AIO\Data;
class InvalidSettingConfigurationException extends \Exception {}

32
php/src/Data/Setup.php Normal file
View file

@ -0,0 +1,32 @@
<?php
namespace AIO\Data;
use AIO\Auth\PasswordGenerator;
class Setup
{
private PasswordGenerator $passwordGenerator;
private ConfigurationManager $configurationManager;
public function __construct(
PasswordGenerator $passwordGenerator,
ConfigurationManager $configurationManager) {
$this->passwordGenerator = $passwordGenerator;
$this->configurationManager = $configurationManager;
}
public function Setup() : string {
if(!$this->CanBeInstalled()) {
return '';
}
$password = $this->passwordGenerator->GeneratePassword(6);
$this->configurationManager->SetPassword($password);
return $password;
}
public function CanBeInstalled() : bool {
return !file_exists(DataConst::GetConfigFile());
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace AIO;
use AIO\Docker\DockerHubManager;
use DI\Container;
class DependencyInjection
{
public static function GetContainer() : Container {
$container = new Container();
$container->set(
DockerHubManager::class,
new DockerHubManager()
);
$container->set(
\AIO\Data\ConfigurationManager::class,
new \AIO\Data\ConfigurationManager()
);
$container->set(
\AIO\Docker\DockerActionManager::class,
new \AIO\Docker\DockerActionManager(
$container->get(\AIO\Data\ConfigurationManager::class),
$container->get(\AIO\ContainerDefinitionFetcher::class),
$container->get(DockerHubManager::class)
)
);
$container->set(
\AIO\Auth\PasswordGenerator::class,
new \AIO\Auth\PasswordGenerator()
);
$container->set(
\AIO\Auth\AuthManager::class,
new \AIO\Auth\AuthManager($container->get(\AIO\Data\ConfigurationManager::class))
);
$container->set(
\AIO\Data\Setup::class,
new \AIO\Data\Setup(
$container->get(\AIO\Auth\PasswordGenerator::class),
$container->get(\AIO\Data\ConfigurationManager::class)
)
);
return $container;
}
}

View file

@ -0,0 +1,468 @@
<?php
namespace AIO\Docker;
use AIO\Container\Container;
use AIO\Container\State\IContainerState;
use AIO\Container\State\ImageDoesNotExistState;
use AIO\Container\State\StartingState;
use AIO\Container\State\RunningState;
use AIO\Container\State\VersionDifferentState;
use AIO\Container\State\StoppedState;
use AIO\Container\State\VersionEqualState;
use AIO\Data\ConfigurationManager;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use AIO\ContainerDefinitionFetcher;
use http\Env\Response;
class DockerActionManager
{
private const API_VERSION = 'v1.41';
private \GuzzleHttp\Client $guzzleClient;
private ConfigurationManager $configurationManager;
private ContainerDefinitionFetcher $containerDefinitionFetcher;
private DockerHubManager $dockerHubManager;
public function __construct(
ConfigurationManager $configurationManager,
ContainerDefinitionFetcher $containerDefinitionFetcher,
DockerHubManager $dockerHubManager
) {
$this->configurationManager = $configurationManager;
$this->containerDefinitionFetcher = $containerDefinitionFetcher;
$this->dockerHubManager = $dockerHubManager;
$this->guzzleClient = new \GuzzleHttp\Client(
[
'curl' => [
CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock',
],
]
);
}
private function BuildApiUrl(string $url) : string {
return sprintf('http://localhost/%s/%s', self::API_VERSION, $url);
}
private function BuildImageName(Container $container) : string {
$channel = $this->GetCurrentChannel();
if ($channel === 'develop') {
return $container->GetContainerName() . ':develop';
} else {
return $container->GetContainerName() . ':latest';
}
}
public function GetContainerRunningState(Container $container) : IContainerState
{
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier())));
try {
$response = $this->guzzleClient->get($url);
} catch (ClientException $ex) {
if($ex->getCode() === 404) {
return new ImageDoesNotExistState();
}
throw $ex;
}
$responseBody = json_decode((string)$response->getBody(), true);
if ($responseBody['State']['Running'] === true) {
return new RunningState();
} else {
return new StoppedState();
}
}
public function GetContainerUpdateState(Container $container) : IContainerState
{
$channel = $this->GetCurrentChannel();
if ($channel === 'develop') {
$tag = 'develop';
} else {
$tag = 'latest';
}
$runningDigest = $this->GetRepoDigestOfContainer($container->GetIdentifier());
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag);
if ($runningDigest === $remoteDigest) {
return new VersionEqualState();
} else {
return new VersionDifferentState();
}
}
public function GetContainerStartingState(Container $container) : IContainerState
{
$runningState = $this->GetContainerRunningState($container);
if ($runningState instanceof StoppedState) {
return new StoppedState();
} elseif ($runningState instanceof ImageDoesNotExistState) {
return new ImageDoesNotExistState();
}
$containerName = $container->GetIdentifier();
if ($container->GetInternalPorts() !== null) {
foreach($container->GetInternalPorts()->GetInternalPorts() as $internalPort) {
$connection = @fsockopen($containerName, $internalPort, $errno, $errstr, 0.1);
if ($connection) {
fclose($connection);
return new RunningState();
} else {
return new StartingState();
}
}
} else {
return new RunningState();
}
}
public function DeleteContainer(Container $container) {
$url = $this->BuildApiUrl(sprintf('containers/%s', urlencode($container->GetIdentifier())));
try {
$this->guzzleClient->delete($url);
} catch (\Exception $e) {}
}
public function GetLogs(Container $container) : string
{
$url = $this->BuildApiUrl(
sprintf(
'containers/%s/logs?stdout=true&stderr=true',
urlencode($container->GetIdentifier())
));
$responseBody = (string)$this->guzzleClient->get($url)->getBody();
$response = "";
$separator = "\r\n";
$line = strtok($responseBody, $separator);
$response = substr($line, 8) . "\n";
while ($line !== false) {
$line = strtok($separator);
$response .= substr($line, 8) . "\n";
}
return $response;
}
public function StartContainer(Container $container) {
$url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->GetIdentifier())));
$this->guzzleClient->post($url);
}
public function CreateVolumes(Container $container)
{
$url = $this->BuildApiUrl('volumes/create');
foreach($container->GetVolumes()->GetVolumes() as $volume) {
$forbiddenChars = [
'/',
];
$firstChar = substr($volume->name, 0, 1);
if(!in_array($firstChar, $forbiddenChars)) {
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'name' => $volume->name,
],
]
);
}
}
}
public function CreateContainer(Container $container) {
$volumes = [];
foreach($container->GetVolumes()->GetVolumes() as $volume) {
$volumeEntry = $volume->name . ':' . $volume->mountPoint;
if($volume->isWritable) {
$volumeEntry = $volumeEntry . ':' . 'rw';
} else {
$volumeEntry = $volumeEntry . ':' . 'ro';
}
$volumes[] = $volumeEntry;
}
$exposedPorts = [];
foreach($container->GetPorts()->GetPorts() as $port) {
$exposedPorts[$port] = null;
}
$requestBody = [
'Image' => $this->BuildImageName($container),
];
if(count($volumes) > 0) {
$requestBody['HostConfig']['Binds'] = $volumes;
}
$envs = $container->GetEnvironmentVariables()->GetVariables();
foreach($envs as $key => $env) {
$patterns = ['/%(.*)%/'];
if(preg_match($patterns[0], $env, $out) === 1) {
$replacements = array();
if($out[1] === 'NC_DOMAIN') {
$replacements[1] = $this->configurationManager->GetDomain();
} elseif ($out[1] === 'AIO_TOKEN') {
$replacements[1] = $this->configurationManager->GetToken();
} elseif ($out[1] === 'BORGBACKUP_MODE') {
$replacements[1] = $this->configurationManager->GetBackupMode();
} elseif ($out[1] === 'AIO_URL') {
$replacements[1] = $this->configurationManager->GetAIOURL();
} else {
$replacements[1] = $this->configurationManager->GetSecret($out[1]);
}
$envs[$key] = preg_replace($patterns, $replacements, $env);
}
}
if(count($envs) > 0) {
$requestBody['Env'] = $envs;
}
$requestBody['HostConfig']['RestartPolicy']['Name'] = $container->GetRestartPolicy();
if(count($exposedPorts) > 0) {
$requestBody['ExposedPorts'] = $exposedPorts;
foreach($container->GetPorts()->GetPorts() as $port) {
$portNumber = explode("/", $port);
$requestBody['HostConfig']['PortBindings'][$port] = [
[
'HostPort' => $portNumber[0],
]
];
}
}
// Special things for the backup container which should not be exposed in the containers.json
if ($container->GetIdentifier() === 'nextcloud-aio-borgbackup') {
$requestBody['HostConfig']['CapAdd'] = ["SYS_ADMIN"];
$requestBody['HostConfig']['Devices'] = [["PathOnHost" => "/dev/fuse", "PathInContainer" => "/dev/fuse", "CgroupPermissions" => "rwm"]];
$requestBody['HostConfig']['SecurityOpt'] = ["apparmor:unconfined"];
}
$url = $this->BuildApiUrl('containers/create?name=' . $container->GetIdentifier());
$this->guzzleClient->request(
'POST',
$url,
[
'json' => $requestBody
]
);
}
public function PullContainer(Container $container)
{
$url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', urlencode($this->BuildImageName($container))));
try {
$this->guzzleClient->post($url);
} catch (ClientException $ex) {
throw $ex;
}
}
private function isContainerUpdateAvailable(string $id) : string
{
$container = $this->containerDefinitionFetcher->GetContainerById($id);
$updateAvailable = "";
if ($container->GetUpdateState() instanceof VersionDifferentState) {
$updateAvailable = '1';
}
foreach ($container->GetDependsOn() as $dependency) {
$updateAvailable .= $this->isContainerUpdateAvailable($dependency);
}
return $updateAvailable;
}
public function isAnyUpdateAvailable() {
$id = 'nextcloud-aio-apache';
if ($this->configurationManager->GetIsContainerUpateAvailable()) {
return true;
}
if ($this->isContainerUpdateAvailable($id) !== "") {
$this->configurationManager->SetIsContainerUpateAvailable(true);
return true;
} else {
return false;
}
}
private function GetRepoDigestOfContainer(string $containerName) : ?string {
try {
$containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
$containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true);
$imageName = $containerOutput['Image'];
$imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName));
$imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true);
if(isset($imageOutput['RepoDigests']) && count($imageOutput['RepoDigests']) === 1) {
$fullDigest = $imageOutput['RepoDigests'][0];
return substr($fullDigest, strpos($fullDigest, "@") + 1);
}
return null;
} catch (\Exception $e) {
return null;
}
}
public function GetCurrentChannel() : string {
$cacheKey = 'aio-ChannelName';
$channelName = apcu_fetch($cacheKey);
if($channelName !== false && is_string($channelName)) {
return $channelName;
}
$containerName = 'nextcloud-aio-mastercontainer';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName));
try {
$output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true);
$containerChecksum = $output['Image'];
$tagArray = explode(':', $output['Config']['Image']);
$tag = $tagArray[1];
apcu_add($cacheKey, $tag);
return $tag;
} catch (\Exception $ex) {
}
return 'latest';
}
public function IsMastercontainerUpdateAvailable() : bool
{
$imageName = 'nextcloud/all-in-one';
$containerName = 'nextcloud-aio-mastercontainer';
$channel = $this->GetCurrentChannel();
if ($channel === 'develop') {
$tag = 'develop';
} else {
$tag = 'latest';
}
$runningDigest = $this->GetRepoDigestOfContainer($containerName);
$remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag);
if ($remoteDigest === $runningDigest) {
return false;
} else {
return true;
}
}
public function DisconnectContainerFromNetwork(Container $container)
{
$url = $this->BuildApiUrl(
sprintf('networks/%s/disconnect', 'nextcloud-aio')
);
try {
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'container' => $container->GetIdentifier(),
],
]
);
} catch (ServerException $e) {}
}
private function ConnectContainerIdToNetwork(string $id)
{
$url = $this->BuildApiUrl('networks/create');
try {
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'name' => 'nextcloud-aio',
'checkDuplicate' => true,
'internal' => true,
]
]
);
} catch (ClientException $e) {
if($e->getCode() !== 409) {
throw $e;
}
}
$url = $this->BuildApiUrl(
sprintf('networks/%s/connect', 'nextcloud-aio')
);
try {
$this->guzzleClient->request(
'POST',
$url,
[
'json' => [
'container' => $id,
]
]
);
} catch (ClientException $e) {
if($e->getCode() !== 403) {
throw $e;
}
}
}
public function ConnectMasterContainerToNetwork()
{
$this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer');
}
public function ConnectContainerToNetwork(Container $container)
{
$this->ConnectContainerIdToNetwork($container->GetIdentifier());
}
public function StopContainer(Container $container) {
$url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->GetIdentifier()), $container->GetMaxShutdownTime()));
$this->guzzleClient->post($url);
}
public function GetBackupcontainerExitCode() : int
{
$containerName = 'nextcloud-aio-borgbackup';
$url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName)));
try {
$response = $this->guzzleClient->get($url);
} catch (ClientException $ex) {
if ($ex->getCode() === 404) {
return -1;
}
throw $ex;
}
$responseBody = json_decode((string)$response->getBody(), true);
$exitCode = $responseBody['State']['ExitCode'];
if (is_int($exitCode)) {
return $exitCode;
} else {
return -1;
}
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace AIO\Docker;
use AIO\ContainerDefinitionFetcher;
use AIO\Data\ConfigurationManager;
use GuzzleHttp\Client;
class DockerHubManager
{
private Client $guzzleClient;
public function __construct()
{
$this->guzzleClient = new Client();
}
public function GetLatestDigestOfTag(string $name, string $tag) : ?string {
$cacheKey = 'dockerhub-manifest-' . $name . $tag;
$cachedVersion = apcu_fetch($cacheKey);
if($cachedVersion !== false && is_string($cachedVersion)) {
return $cachedVersion;
}
try {
$authTokenRequest = $this->guzzleClient->request(
'GET',
'https://auth.docker.io/token?service=registry.docker.io&scope=repository:' . $name . ':pull'
);
$body = $authTokenRequest->getBody()->getContents();
$decodedBody = json_decode($body, true);
if(isset($decodedBody['token'])) {
$authToken = $decodedBody['token'];
$manifestRequest = $this->guzzleClient->request(
'GET',
'https://registry-1.docker.io/v2/'.$name.'/manifests/' . $tag,
[
'headers' => [
'Accept' => 'application/vnd.docker.distribution.manifest.v2+json',
'Authorization' => 'Bearer ' . $authToken,
],
]
);
$responseHeaders = $manifestRequest->getHeader('docker-content-digest');
if(count($responseHeaders) === 1) {
$latestVersion = $responseHeaders[0];
apcu_add($cacheKey, $latestVersion, 600);
return $latestVersion;
}
}
return null;
} catch (\Exception $e) {
return null;
}
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace AIO\Middleware;
use AIO\Auth\AuthManager;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class AuthMiddleware
{
private AuthManager $authManager;
public function __construct(AuthManager $authManager) {
$this->authManager = $authManager;
}
public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$publicRoutes = [
'/api/auth/login',
'/api/auth/getlogin',
'/login',
'/setup',
'/',
];
if(!in_array($request->getUri()->getPath(), $publicRoutes)) {
if(!$this->authManager->IsAuthenticated()) {
$response = new Response();
return $response
->withHeader('Location', '/')
->withStatus(302);
}
}
$response = $handler->handle($request);
return $response;
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace AIO\Twig;
use Slim\Views\TwigExtension;
use Twig\TwigFunction;
class ClassExtension extends TwigExtension
{
public function getFunctions() : array
{
return array(
new TwigFunction('class', array($this, 'getClassName')),
);
}
public function getClassName($object) : ?string
{
if (!is_object($object)) {
return null;
}
return get_class($object);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace AIO\Twig;
use Slim\Csrf\Guard;
class CsrfExtension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
{
/**
* @var Guard
*/
protected Guard $csrf;
public function __construct(Guard $csrf)
{
$this->csrf = $csrf;
}
public function getGlobals() : array
{
// CSRF token name and value
$csrfNameKey = $this->csrf->getTokenNameKey();
$csrfValueKey = $this->csrf->getTokenValueKey();
$csrfName = $this->csrf->getTokenName();
$csrfValue = $this->csrf->getTokenValue();
return [
'csrf' => [
'keys' => [
'name' => $csrfNameKey,
'value' => $csrfValueKey
],
'name' => $csrfName,
'value' => $csrfValue
]
];
}
}