docker: gd extension + 2FA Bugfix

This commit is contained in:
Roman Hergenreder 2022-11-27 15:58:44 +01:00
parent 26a22f5299
commit c9a7da688f
13 changed files with 241 additions and 182 deletions

@ -3,12 +3,11 @@
namespace Core\API {
use Core\API\Routes\GenerateCache;
use Core\Driver\SQL\Condition\Compare;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Route;
abstract class RoutesAPI extends Request {
const ACTIONS = array("redirect_temporary", "redirect_permanently", "static", "dynamic");
const ROUTER_CACHE_CLASS = "\\Core\\Cache\\RouterCache";
protected string $routerCachePath;
@ -18,38 +17,19 @@ namespace Core\API {
$this->routerCachePath = getClassPath(self::ROUTER_CACHE_CLASS);
}
protected function routeExists($uid): bool {
protected function toggleRoute(int $id, bool $active): bool {
$sql = $this->context->getSQL();
$res = $sql->select($sql->count())
->from("Route")
->whereEq("id", $uid)
->execute();
$this->success = ($res !== false);
$this->lastError = $sql->getLastError();
if ($this->success) {
if ($res[0]["count"] === 0) {
return $this->createError("Route not found");
}
}
return $this->success;
}
protected function toggleRoute($uid, $active): bool {
if (!$this->routeExists($uid)) {
$route = Route::find($sql, $id);
if ($route === false) {
return false;
} else if ($route === null) {
return $this->createError("Route not found");
}
$sql = $this->context->getSQL();
$this->success = $sql->update("Route")
->set("active", $active)
->whereEq("id", $uid)
->execute();
$route->setActive($active);
$this->success = $route->save($sql);
$this->lastError = $sql->getLastError();
$this->success = $this->success && $this->regenerateCache();
return $this->success;
return $this->success && $this->regenerateCache();
}
protected function regenerateCache(): bool {
@ -58,6 +38,27 @@ namespace Core\API {
$this->lastError = $req->getLastError();
return $this->success;
}
protected function createRoute(string $type, string $pattern, string $target,
?string $extra, bool $exact, bool $active = true): ?Route {
$routeClass = Route::ROUTE_TYPES[$type] ?? null;
if (!$routeClass) {
$this->createError("Invalid type: $type");
return null;
}
try {
$routeClass = new \ReflectionClass($routeClass);
$routeObj = $routeClass->newInstance($pattern, $exact, $target);
$routeObj->setExtra($extra);
$routeObj->setActive($active);
return $routeObj;
} catch (\ReflectionException $exception) {
$this->createError("Error instantiating route class: " . $exception->getMessage());
return null;
}
}
}
}
@ -68,6 +69,7 @@ namespace Core\API\Routes {
use Core\API\RoutesAPI;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondBool;
use Core\Driver\SQL\Query\StartTransaction;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Route;
use Core\Objects\Router\DocumentRoute;
@ -86,31 +88,15 @@ namespace Core\API\Routes {
public function _execute(): bool {
$sql = $this->context->getSQL();
$res = $sql
->select("id", "request", "action", "target", "extra", "active", "exact")
->from("Route")
->orderBy("id")
->ascending()
->execute();
$routes = Route::findAll($sql);
$this->lastError = $sql->getLastError();
$this->success = ($res !== FALSE);
$this->success = ($routes !== FALSE);
if ($this->success) {
$routes = array();
foreach ($res as $row) {
$routes[] = array(
"id" => intval($row["id"]),
"request" => $row["request"],
"action" => $row["action"],
"target" => $row["target"],
"extra" => $row["extra"] ?? "",
"active" => intval($sql->parseBool($row["active"])),
"exact" => intval($sql->parseBool($row["exact"])),
);
$this->result["routes"] = [];
foreach ($routes as $route) {
$this->result["routes"][$route->getId()] = $route->jsonSerialize();
}
$this->result["routes"] = $routes;
}
return $this->success;
@ -133,32 +119,35 @@ namespace Core\API\Routes {
}
$sql = $this->context->getSQL();
$sql->startTransaction();
// DELETE old rules
// DELETE old rules;
$this->success = ($sql->truncate("Route")->execute() !== FALSE);
$this->lastError = $sql->getLastError();
// INSERT new routes
if ($this->success) {
$stmt = $sql->insert("Route", array("request", "action", "target", "extra", "active", "exact"));
foreach ($this->routes as $route) {
$stmt->addRow($route["request"], $route["action"], $route["target"], $route["extra"], $route["active"], $route["exact"]);
}
$this->success = ($stmt->execute() !== FALSE);
$insertStatement = Route::getHandler($sql)->getInsertQuery($this->routes);
$this->success = ($insertStatement->execute() !== FALSE);
$this->lastError = $sql->getLastError();
}
$this->success = $this->success && $this->regenerateCache();
return $this->success;
if ($this->success) {
$sql->commit();
return $this->regenerateCache();
} else {
$sql->rollback();
return false;
}
}
private function validateRoutes(): bool {
$this->routes = array();
$keys = array(
"request" => [Parameter::TYPE_STRING, Parameter::TYPE_INT],
"action" => Parameter::TYPE_STRING,
"id" => Parameter::TYPE_INT,
"pattern" => [Parameter::TYPE_STRING, Parameter::TYPE_INT],
"type" => Parameter::TYPE_STRING,
"target" => Parameter::TYPE_STRING,
"extra" => Parameter::TYPE_STRING,
"active" => Parameter::TYPE_BOOLEAN,
@ -168,7 +157,11 @@ namespace Core\API\Routes {
foreach ($this->getParam("routes") as $index => $route) {
foreach ($keys as $key => $expectedType) {
if (!array_key_exists($key, $route)) {
return $this->createError("Route $index missing key: $key");
if ($key !== "id") { // id is optional
return $this->createError("Route $index missing key: $key");
} else {
continue;
}
}
$value = $route[$key];
@ -191,13 +184,13 @@ namespace Core\API\Routes {
}
}
$action = $route["action"];
if (!in_array($action, self::ACTIONS)) {
return $this->createError("Invalid action: $action");
$type = $route["type"];
if (!isset(Route::ROUTE_TYPES[$type])) {
return $this->createError("Invalid type: $type");
}
if (empty($route["request"])) {
return $this->createError("Request cannot be empty.");
if (empty($route["pattern"])) {
return $this->createError("Pattern cannot be empty.");
}
if (empty($route["target"])) {
@ -215,33 +208,33 @@ namespace Core\API\Routes {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, array(
"request" => new StringType("request", 128),
"action" => new StringType("action"),
"pattern" => new StringType("pattern", 128),
"type" => new StringType("type"),
"target" => new StringType("target", 128),
"extra" => new StringType("extra", 64, true, ""),
"exact" => new Parameter("exact", Parameter::TYPE_BOOLEAN),
"active" => new Parameter("active", Parameter::TYPE_BOOLEAN, true, true),
));
$this->isPublic = false;
}
public function _execute(): bool {
$request = $this->getParam("request");
$action = $this->getParam("action");
$pattern = $this->getParam("pattern");
$type = $this->getParam("type");
$target = $this->getParam("target");
$extra = $this->getParam("extra");
if (!in_array($action, self::ACTIONS)) {
return $this->createError("Invalid action: $action");
$exact = $this->getParam("exact");
$active = $this->getParam("active");
$route = $this->createRoute($type, $pattern, $target, $extra, $exact, $active);
if ($route === null) {
return false;
}
$sql = $this->context->getSQL();
$this->success = $sql->insert("Route", ["request", "action", "target", "extra"])
->addRow($request, $action, $target, $extra)
->execute();
$this->success = $route->save($sql) !== false;
$this->lastError = $sql->getLastError();
$this->success = $this->success && $this->regenerateCache();
return $this->success;
return $this->success && $this->regenerateCache();
}
}
@ -249,8 +242,8 @@ namespace Core\API\Routes {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, array(
"id" => new Parameter("id", Parameter::TYPE_INT),
"request" => new StringType("request", 128),
"action" => new StringType("action"),
"pattern" => new StringType("pattern", 128),
"type" => new StringType("type"),
"target" => new StringType("target", 128),
"extra" => new StringType("extra", 64, true, ""),
));
@ -260,30 +253,36 @@ namespace Core\API\Routes {
public function _execute(): bool {
$id = $this->getParam("id");
if (!$this->routeExists($id)) {
return false;
$sql = $this->context->getSQL();
$route = Route::find($sql, $id);
if ($route === false) {
return $this->createError("Error fetching route: " . $sql->getLastError());
} else if ($route === null) {
return $this->createError("Route not found");
}
$request = $this->getParam("request");
$action = $this->getParam("action");
$target = $this->getParam("target");
$extra = $this->getParam("extra");
if (!in_array($action, self::ACTIONS)) {
return $this->createError("Invalid action: $action");
$type = $this->getParam("type");
$pattern = $this->getParam("pattern");
$exact = $this->getParam("exact");
$active = $this->getParam("active");
if ($route->getType() !== $type) {
$route = $this->createRoute($type, $pattern, $target, $extra, $exact, $active);
if ($route === null) {
return false;
}
} else {
$route->setPattern($pattern);
$route->setActive($active);
$route->setExtra($extra);
$route->setTarget($target);
$route->setExact($exact);
}
$sql = $this->context->getSQL();
$this->success = $sql->update("Route")
->set("request", $request)
->set("action", $action)
->set("target", $target)
->set("extra", $extra)
->whereEq("id", $id)
->execute();
$this->success = $route->save($sql) !== false;
$this->lastError = $sql->getLastError();
$this->success = $this->success && $this->regenerateCache();
return $this->success;
return $this->success && $this->regenerateCache();
}
}
@ -297,19 +296,18 @@ namespace Core\API\Routes {
public function _execute(): bool {
$sql = $this->context->getSQL();
$id = $this->getParam("id");
if (!$this->routeExists($id)) {
return false;
$route = Route::find($sql, $id);
if ($route === false) {
return $this->createError("Error fetching route: " . $sql->getLastError());
} else if ($route === null) {
return $this->createError("Route not found");
}
$sql = $this->context->getSQL();
$this->success = $sql->delete("Route")
->where("id", $id)
->execute();
$this->success = $route->delete($sql) !== false;
$this->lastError = $sql->getLastError();
$this->success = $this->success && $this->regenerateCache();
return $this->success;
return $this->success && $this->regenerateCache();
}
}
@ -322,8 +320,8 @@ namespace Core\API\Routes {
}
public function _execute(): bool {
$uid = $this->getParam("id");
return $this->toggleRoute($uid, true);
$id = $this->getParam("id");
return $this->toggleRoute($id, true);
}
}
@ -336,8 +334,8 @@ namespace Core\API\Routes {
}
public function _execute(): bool {
$uid = $this->getParam("id");
return $this->toggleRoute($uid, false);
$id = $this->getParam("id");
return $this->toggleRoute($id, false);
}
}

@ -50,10 +50,13 @@ namespace Core\API\Template {
$valid = false;
foreach ($baseDirs as $baseDir) {
$path = realpath(implode("/", [WEBROOT, $baseDir, "Templates", $templateFile]));
if ($path && is_file($path)) {
$valid = true;
break;
$templateDir = realpath(implode("/", [WEBROOT, $baseDir, "Templates"]));
if ($templateDir) {
$path = realpath(implode("/", [$templateDir, $templateFile]));
if ($path && is_file($path)) {
$valid = true;
break;
}
}
}
@ -61,7 +64,7 @@ namespace Core\API\Template {
return $this->createError("Template file not found or not inside template directory");
}
$twigLoader = new FilesystemLoader(dirname($path));
$twigLoader = new FilesystemLoader($templateDir);
$twigEnvironment = new Environment($twigLoader, [
'cache' => $templateCache,
'auto_reload' => true

@ -84,27 +84,14 @@ namespace Core\API\TFA {
$sql = $this->context->getSQL();
$password = $this->getParam("password");
if ($password) {
$res = $sql->select("password")
->from("User")
->whereEq("id", $currentUser->getId())
->execute();
$this->success = !empty($res);
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;
} else if (!password_verify($password, $res[0]["password"])) {
if (!password_verify($password, $currentUser->password)) {
return $this->createError("Wrong password");
}
} else if ($token->isConfirmed()) {
// if the token is fully confirmed, require a password to remove it
return $this->createError("Missing parameter: password");
}
$res = $sql->delete("2FA")
->whereEq("id", $token->getId())
->execute();
$this->success = $res !== false;
$this->success = $token->delete($sql) !== false;
$this->lastError = $sql->getLastError();
if ($this->success && $token->isConfirmed()) {
@ -157,16 +144,11 @@ namespace Core\API\TFA {
} else if (!($twoFactorToken instanceof TimeBasedTwoFactorToken)) {
$twoFactorToken = new TimeBasedTwoFactorToken(generateRandomString(32, "base32"));
$sql = $this->context->getSQL();
$this->success = $sql->insert("2FA", ["type", "data"])
->addRow("totp", $twoFactorToken->getData())
->returning("id")
->execute() !== false;
$this->success = $twoFactorToken->save($sql) !== false;
$this->lastError = $sql->getLastError();
if ($this->success) {
$this->success = $sql->update("User")
->set("2fa_id", $sql->getLastInsertId())
->whereEq("id", $currentUser->getId())
->execute() !== false;
$currentUser->setTwoFactorToken($twoFactorToken);
$this->success = $currentUser->save($sql);
$this->lastError = $sql->getLastError();
}
@ -194,11 +176,12 @@ namespace Core\API\TFA {
return $this->createError("Your two factor token is already confirmed.");
}
if (!parent::_execute()) {
return false;
}
$sql = $this->context->getSQL();
$this->success = $sql->update("2FA")
->set("confirmed", true)
->whereEq("id", $twoFactorToken->getId())
->execute() !== false;
$this->success = $twoFactorToken->confirm($sql) !== false;
$this->lastError = $sql->getLastError();
return $this->success;
}
@ -271,20 +254,15 @@ namespace Core\API\TFA {
}
} else {
$challenge = base64_encode(generateRandomString(32, "raw"));
$res = $sql->insert("2FA", ["type", "data"])
->addRow("fido", $challenge)
->returning("id")
->execute();
$this->success = ($res !== false);
$twoFactorToken = new KeyBasedTwoFactorToken($challenge);
$this->success = ($twoFactorToken->save($sql) !== false);
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;
}
$this->success = $sql->update("User")
->set("2fa_id", $sql->getLastInsertId())
->whereEq("id", $currentUser->getId())
->execute() !== false;
$currentUser->setTwoFactorToken($twoFactorToken);
$this->success = $currentUser->save($sql) !== false;
$this->lastError = $sql->getLastError();
if (!$this->success) {
return false;
@ -321,16 +299,7 @@ namespace Core\API\TFA {
return $this->createError("Unsupported key type. Expected: -7");
}
$data = [
"credentialID" => base64_encode($authData->getCredentialID()),
"publicKey" => $publicKey->jsonSerialize()
];
$this->success = $sql->update("2FA")
->set("data", json_encode($data))
->set("confirmed", true)
->whereEq("id", $twoFactorToken->getId())
->execute() !== false;
$this->success = $twoFactorToken->confirmKeyBased($sql, base64_encode($authData->getCredentialID()), $publicKey) !== false;
$this->lastError = $sql->getLastError();
}

@ -20,7 +20,6 @@ namespace Documents\Install {
use Core\Configuration\Configuration;
use Core\Configuration\CreateDatabase;
use Core\Driver\SQL\Query\Commit;
use Core\Driver\SQL\Query\RollBack;
use Core\Driver\SQL\Query\StartTransaction;
use Core\Driver\SQL\SQL;
use Core\Elements\Body;
@ -349,7 +348,7 @@ namespace Documents\Install {
}
} finally {
if (!$success) {
(new RollBack($sql))->execute();
$sql->rollback();
}
}

@ -23,6 +23,7 @@ use Core\Driver\SQL\Expression\CurrentTimeStamp;
use Core\Driver\SQL\Expression\Expression;
use Core\Driver\SQL\Expression\Sum;
use Core\Driver\SQL\Query\AlterTable;
use Core\Driver\SQL\Query\Commit;
use Core\Driver\SQL\Query\CreateProcedure;
use Core\Driver\SQL\Query\CreateTable;
use Core\Driver\SQL\Query\CreateTrigger;
@ -30,7 +31,9 @@ use Core\Driver\SQL\Query\Delete;
use Core\Driver\SQL\Query\Drop;
use Core\Driver\SQL\Query\Insert;
use Core\Driver\SQL\Query\Query;
use Core\Driver\SQL\Query\RollBack;
use Core\Driver\SQL\Query\Select;
use Core\Driver\SQL\Query\StartTransaction;
use Core\Driver\SQL\Query\Truncate;
use Core\Driver\SQL\Query\Update;
use Core\Driver\SQL\Strategy\CascadeStrategy;
@ -99,6 +102,18 @@ abstract class SQL {
return new Drop($this, $table);
}
public function startTransaction(): bool {
return (new StartTransaction($this))->execute();
}
public function commit(): bool {
return (new Commit($this))->execute();
}
public function rollback(): bool {
return (new RollBack($this))->execute();
}
public function alterTable($tableName): AlterTable {
return new AlterTable($this, $tableName);
}

@ -3,6 +3,7 @@
namespace Core\Objects\DatabaseEntity;
use Core\API\Parameter\Parameter;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum;
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
@ -16,11 +17,16 @@ use Core\Objects\Router\StaticFileRoute;
abstract class Route extends DatabaseEntity {
const PARAMETER_PATTERN = "/^{([^:]+)(:(.*?)(\?)?)?}$/";
const TYPE_DYNAMIC = "dynamic";
const TYPE_STATIC = "static";
const TYPE_REDIRECT_PERMANENTLY = "redirect_permanently";
const TYPE_REDIRECT_TEMPORARY = "redirect_temporary";
const ROUTE_TYPES = [
"redirect_temporary" => RedirectRoute::class,
"redirect_permanently" => RedirectRoute::class,
"static" => StaticFileRoute::class,
"dynamic" => DocumentRoute::class
self::TYPE_REDIRECT_TEMPORARY => RedirectRoute::class,
self::TYPE_REDIRECT_PERMANENTLY => RedirectRoute::class,
self::TYPE_STATIC => StaticFileRoute::class,
self::TYPE_DYNAMIC => DocumentRoute::class
];
#[MaxLength(128)]
@ -77,6 +83,13 @@ abstract class Route extends DatabaseEntity {
public abstract function call(Router $router, array $params): string;
protected function readExtra() { }
public function postFetch(SQL $sql, array $row) {
parent::postFetch($sql, $row);
$this->readExtra();
}
protected function getArgs(): array {
return [$this->pattern, $this->exact];
}
@ -204,4 +217,28 @@ abstract class Route extends DatabaseEntity {
"active" => $this->active,
];
}
public function setActive(bool $active) {
$this->active = $active;
}
public function getType(): string {
return $this->type;
}
public function setPattern(string $pattern) {
$this->pattern = $pattern;
}
public function setExtra(string $extra) {
$this->extra = $extra;
}
public function setTarget(string $target) {
$this->target = $target;
}
public function setExact(bool $exact) {
$this->exact = $exact;
}
}

@ -19,7 +19,7 @@ abstract class TwoFactorToken extends DatabaseEntity {
#[ExtendingEnum(self::TWO_FACTOR_TOKEN_TYPES)] private string $type;
private bool $confirmed;
private bool $authenticated;
#[MaxLength(512)] private string $data;
#[MaxLength(512)] private ?string $data;
public function __construct(string $type, ?int $id = null, bool $confirmed = false) {
parent::__construct($id);
@ -27,6 +27,7 @@ abstract class TwoFactorToken extends DatabaseEntity {
$this->type = $type;
$this->confirmed = $confirmed;
$this->authenticated = $_SESSION["2faAuthenticated"] ?? false;
$this->data = null;
}
public function jsonSerialize(): array {
@ -63,11 +64,12 @@ abstract class TwoFactorToken extends DatabaseEntity {
return $this->confirmed;
}
public function getId(): int {
return $this->id;
}
public function isAuthenticated(): bool {
return $this->authenticated;
}
public function confirm(SQL $sql): bool {
$this->confirmed = true;
return $this->save($sql) !== false;
}
}

@ -93,4 +93,8 @@ class User extends DatabaseEntity {
$this->lastOnline = new \DateTime();
return $this->save($sql, ["last_online", "language_id"]);
}
public function setTwoFactorToken(TwoFactorToken $twoFactorToken) {
$this->twoFactorToken = $twoFactorToken;
}
}

@ -24,11 +24,16 @@ class DocumentRoute extends Route {
$this->extra = json_encode($args);
}
public function postFetch(SQL $sql, array $row) {
parent::postFetch($sql, $row);
protected function readExtra() {
parent::readExtra();
$this->args = json_decode($this->extra);
}
public function preInsert(array &$row) {
parent::preInsert($row);
$this->extra = json_encode($this->args);
}
#[Pure] private function getClassName(): string {
return $this->getTarget();
}

@ -22,11 +22,16 @@ class StaticFileRoute extends Route {
$this->extra = json_encode($this->code);
}
public function postFetch(SQL $sql, array $row) {
parent::postFetch($sql, $row);
protected function readExtra() {
parent::readExtra();
$this->code = json_decode($this->extra);
}
public function preInsert(array &$row) {
parent::preInsert($row);
$this->extra = json_encode($this->code);
}
public function call(Router $router, array $params): string {
http_response_code($this->code);
$this->serveStatic($this->getAbsolutePath(), $router);

@ -2,6 +2,7 @@
namespace Core\Objects\TwoFactor;
use Core\Driver\SQL\SQL;
use Cose\Algorithm\Signature\ECDSA\ECSignature;
use Core\Objects\DatabaseEntity\TwoFactorToken;
@ -13,8 +14,13 @@ class KeyBasedTwoFactorToken extends TwoFactorToken {
private ?string $credentialId;
private ?PublicKey $publicKey;
public function __construct(string $challenge) {
parent::__construct(self::TYPE);
$this->challenge = $challenge;
}
protected function readData(string $data) {
if ($this->isConfirmed()) {
if (!$this->isConfirmed()) {
$this->challenge = base64_decode($data);
$this->credentialId = null;
$this->publicKey = null;
@ -27,9 +33,23 @@ class KeyBasedTwoFactorToken extends TwoFactorToken {
}
public function getData(): string {
return $this->challenge;
if ($this->isConfirmed()) {
return base64_encode($this->challenge);
} else {
return json_encode([
"credentialId" => $this->credentialId,
"publicKey" => $this->publicKey->jsonSerialize()
]);
}
}
public function confirmKeyBased(SQL $sql, string $credentialId, PublicKey $publicKey): bool {
$this->credentialId = $credentialId;
$this->publicKey = $publicKey;
return parent::confirm($sql);
}
public function getPublicKey(): ?PublicKey {
return $this->publicKey;
}

@ -5,6 +5,7 @@ namespace Core\Objects\TwoFactor;
use Base32\Base32;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Core\Driver\SQL\SQL;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\TwoFactorToken;

@ -7,9 +7,10 @@ RUN mkdir -p /application/core/Configuration /var/www/.gnupg && \
# YAML + dev dependencies
RUN apt-get update -y && \
apt-get install -y libyaml-dev libzip-dev libgmp-dev gnupg2 && \
apt-get install -y libyaml-dev libzip-dev libgmp-dev libpng-dev gnupg2d && \
apt-get clean && \
pecl install yaml && docker-php-ext-enable yaml
pecl install yaml && docker-php-ext-enable yaml && \
docker-php-ext-install gd
# Runkit (no stable release available)
RUN pecl install runkit7-4.0.0a3 && docker-php-ext-enable runkit7 && \