diff --git a/server/.htaccess b/server/.htaccess
index a4d5fd3..b2a112f 100644
--- a/server/.htaccess
+++ b/server/.htaccess
@@ -1,5 +1,16 @@
# SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
RewriteEngine On
+
+### Allow server-status from localhost for supervision ###
+RewriteCond %{HTTP_HOST} ^localhost$
+RewriteCond %{REQUEST_URI} ^/server-status
+RewriteRule ^(.*)$ - [L]
+
+### Normal rewrite rules for lookup-server ###
RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+
+### Exclude server-status from rewrite to index.php ###
+RewriteCond %{REQUEST_URI} !^/server-status
RewriteRule ^ index.php [QSA,L]
diff --git a/server/init.php b/server/init.php
index 3de11ff..0837523 100644
--- a/server/init.php
+++ b/server/init.php
@@ -35,3 +35,28 @@
});
$container->get('DependenciesService')->initContainer($container, $app);
+// Add error middleware for logging
+$errorMiddleware = $app->addErrorMiddleware(
+ $settings['settings']['displayErrorDetails'] ?? true,
+ true, // logErrors
+ true // logErrorDetails
+);
+
+// Set custom error handler that logs to our Logger
+$errorHandler = $errorMiddleware->getDefaultErrorHandler();
+$errorHandler->registerErrorRenderer('text/html', function ($exception, $displayErrorDetails) use ($container) {
+ /** @var \LookupServer\Logger $logger */
+ $logger = $container->get('Logger');
+ $logger->error('Exception: ' . $exception->getMessage(), [
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ 'trace' => $exception->getTraceAsString()
+ ]);
+
+ $message = $displayErrorDetails
+ ? sprintf('Error: %s in %s:%d', $exception->getMessage(), $exception->getFile(), $exception->getLine())
+ : 'An error occurred';
+
+ return "
Error
{$message}
";
+});
+
diff --git a/server/lib/Logger.php b/server/lib/Logger.php
new file mode 100644
index 0000000..80485bc
--- /dev/null
+++ b/server/lib/Logger.php
@@ -0,0 +1,111 @@
+
+ * @copyright 2025
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+namespace LookupServer;
+
+class Logger {
+ private string $logFile;
+ private bool $enabled;
+
+ public function __construct(string $logFile, bool $enabled = true) {
+ $this->logFile = $logFile;
+ $this->enabled = $enabled;
+ }
+
+ /**
+ * Log a message with a specific level
+ *
+ * @param string $level
+ * @param string $message
+ * @param array $context
+ */
+ private function log(string $level, string $message, array $context = []): void {
+ if (!$this->enabled) {
+ return;
+ }
+
+ $timestamp = date('Y-m-d H:i:s');
+ $contextString = !empty($context) ? ' ' . json_encode($context) : '';
+ $logMessage = "[{$timestamp}] [{$level}] {$message}{$contextString}\n";
+
+ // Write to file
+ @file_put_contents($this->logFile, $logMessage, FILE_APPEND | LOCK_EX);
+ }
+
+ /**
+ * Log debug message
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function debug(string $message, array $context = []): void {
+ $this->log('DEBUG', $message, $context);
+ }
+
+ /**
+ * Log info message
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function info(string $message, array $context = []): void {
+ $this->log('INFO', $message, $context);
+ }
+
+ /**
+ * Log warning message
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function warning(string $message, array $context = []): void {
+ $this->log('WARNING', $message, $context);
+ }
+
+ /**
+ * Log error message
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function error(string $message, array $context = []): void {
+ $this->log('ERROR', $message, $context);
+ }
+
+ /**
+ * Log critical message
+ *
+ * @param string $message
+ * @param array $context
+ */
+ public function critical(string $message, array $context = []): void {
+ $this->log('CRITICAL', $message, $context);
+ }
+}
+
diff --git a/server/lib/Service/DependenciesService.php b/server/lib/Service/DependenciesService.php
index 4868532..dd97bc3 100644
--- a/server/lib/Service/DependenciesService.php
+++ b/server/lib/Service/DependenciesService.php
@@ -13,6 +13,7 @@
use DI\Container;
use Exception;
use LookupServer\InstanceManager;
+use LookupServer\Logger;
use LookupServer\Replication;
use LookupServer\SignatureHandler;
use LookupServer\Tools\Traits\TArrayTools;
@@ -39,6 +40,14 @@ public function __construct(array $settings = []) {
*/
public function initContainer(Container $container, App $app): void {
+ $container->set('Logger', function (Container $c) {
+ $settings = $c->get('Settings');
+ return new Logger(
+ $this->get('settings.log_file', $settings),
+ $this->getBool('settings.log_enabled', $settings)
+ );
+ });
+
$container->set('db', function (Container $c) {
$db = $this->getArray('settings.db', $c->get('Settings'));
if (empty($db)) {
@@ -76,7 +85,8 @@ public function initContainer(Container $container, App $app): void {
$c->get('TwitterValidator'),
$c->get('InstanceManager'),
$c->get('SignatureHandler'),
- $c->get('SecurityService')
+ $c->get('SecurityService'),
+ $c->get('Logger')
);
});
diff --git a/server/lib/UserManager.php b/server/lib/UserManager.php
index f850cea..9232de4 100644
--- a/server/lib/UserManager.php
+++ b/server/lib/UserManager.php
@@ -28,7 +28,8 @@ public function __construct(
private Twitter $twitterValidator,
private InstanceManager $instanceManager,
private SignatureHandler $signatureHandler,
- private SecurityService $securityService
+ private SecurityService $securityService,
+ private ?Logger $logger
) {
}
@@ -57,6 +58,10 @@ public function search(Request $request, Response $response, array $args = []):
$params = $request->getQueryParams();
$search = urldecode($params['search'] ?? '');
+ if ($this->logger) {
+ $this->logger->info('Search request', ['search' => $search, 'params' => $params]);
+ }
+
if ($search === '') {
return $response->withStatus(400);
}
@@ -411,6 +416,9 @@ public function register(Request $request, Response $response, array $args = [])
|| !isset($body['message']['data']['federationId'])
|| !isset($body['signature'])
|| !isset($body['message']['timestamp'])) {
+ if ($this->logger) {
+ $this->logger->warning('Invalid registration request - missing required fields');
+ }
return $response->withStatus(400);
}
@@ -419,6 +427,12 @@ public function register(Request $request, Response $response, array $args = [])
try {
$verified = $this->signatureHandler->verify($cloudId, $body['message'], $body['signature']);
} catch (Exception $e) {
+ if ($this->logger) {
+ $this->logger->error('Registration signature verification failed', [
+ 'cloudId' => $cloudId,
+ 'error' => $e->getMessage()
+ ]);
+ }
return $response->withStatus(400);
}
@@ -426,10 +440,19 @@ public function register(Request $request, Response $response, array $args = [])
$result =
$this->insertOrUpdate($cloudId, $body['message']['data'], $body['message']['timestamp']);
if ($result === false) {
+ if ($this->logger) {
+ $this->logger->warning('Registration rejected - timestamp too old', ['cloudId' => $cloudId]);
+ }
return $response->withStatus(403);
}
+ if ($this->logger) {
+ $this->logger->info('User registered/updated successfully', ['cloudId' => $cloudId]);
+ }
} else {
// ERROR OUT
+ if ($this->logger) {
+ $this->logger->error('Registration signature invalid', ['cloudId' => $cloudId]);
+ }
return $response->withStatus(403);
}
@@ -542,16 +565,31 @@ public function delete(Request $request, Response $response, array $args = []):
try {
$verified = $this->signatureHandler->verify($cloudId, $body['message'], $body['signature']);
} catch (Exception $e) {
+ if ($this->logger) {
+ $this->logger->error('Delete signature verification failed', [
+ 'cloudId' => $cloudId,
+ 'error' => $e->getMessage()
+ ]);
+ }
return $response->withStatus(400);
}
if ($verified) {
$result = $this->deleteDBRecord($cloudId);
if ($result === false) {
+ if ($this->logger) {
+ $this->logger->warning('Delete failed - user not found', ['cloudId' => $cloudId]);
+ }
return $response->withStatus(404);
}
+ if ($this->logger) {
+ $this->logger->info('User deleted successfully', ['cloudId' => $cloudId]);
+ }
} else {
// ERROR OUT
+ if ($this->logger) {
+ $this->logger->error('Delete signature invalid', ['cloudId' => $cloudId]);
+ }
return $response->withStatus(403);
}