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); }