From 36506eb99ac15635460edcc25234beed6b5c3599 Mon Sep 17 00:00:00 2001 From: Pieter van der Meulen Date: Thu, 25 Jul 2024 14:54:58 +0200 Subject: [PATCH 1/9] Use new Docker ENV syntax --- Dockerfile.testserver | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.testserver b/Dockerfile.testserver index 82a05ec..f966975 100644 --- a/Dockerfile.testserver +++ b/Dockerfile.testserver @@ -1,8 +1,8 @@ FROM php:8.2-apache AS tiqr-testserver -ENV APACHE_DOCUMENT_ROOT /var/www/TestServer +ENV APACHE_DOCUMENT_ROOT=/var/www/TestServer -ENV SERVERNAME localhost +ENV SERVERNAME=localhost # Enable mod-rewrite RUN a2enmod rewrite From 02a78ca1357e5b787118102c805d3835ce5a82a9 Mon Sep 17 00:00:00 2001 From: Pieter van der Meulen Date: Thu, 25 Jul 2024 17:04:53 +0200 Subject: [PATCH 2/9] Pull baseimage before build --- build-docker-testserver.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build-docker-testserver.sh b/build-docker-testserver.sh index beb68bc..c440567 100755 --- a/build-docker-testserver.sh +++ b/build-docker-testserver.sh @@ -1,3 +1,5 @@ #!/bin/bash +docker pull php:8.2-apache + docker build --rm -f "Dockerfile.testserver" -t tiqr-testserver:latest "." From 24b10070563ef3b8c65edff9dbe177f3bad968e1 Mon Sep 17 00:00:00 2001 From: Pieter van der Meulen Date: Thu, 25 Jul 2024 17:06:05 +0200 Subject: [PATCH 3/9] Add script to push Docker TestServer image as latest --- docker-tag-push.sh | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 docker-tag-push.sh diff --git a/docker-tag-push.sh b/docker-tag-push.sh new file mode 100755 index 0000000..28b1053 --- /dev/null +++ b/docker-tag-push.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +docker tag tiqr-testserver pmeulen/tiqr-testserver:latest +docker push pmeulen/tiqr-testserver:latest From dbc66eaae8ceaead3c67c719ef1bf9fd85fb0037 Mon Sep 17 00:00:00 2001 From: Pieter van der Meulen Date: Tue, 30 Jul 2024 17:18:48 +0200 Subject: [PATCH 4/9] TestServer: log error and stop when config file cannot be parsed --- TestServer/app.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/TestServer/app.php b/TestServer/app.php index c567de8..a2d82b5 100644 --- a/TestServer/app.php +++ b/TestServer/app.php @@ -29,6 +29,10 @@ $config = array(); if (file_exists($config_dir . '/' . $config_filename)) { $config = json_decode(file_get_contents($config_dir . '/' . $config_filename), true); + + if (json_last_error() != JSON_ERROR_NONE) { + die('Error parsing configuration file: ' . json_last_error_msg()); + } } # Directory for storing session info and user data @@ -46,9 +50,13 @@ $firebase_cacheTokens = $config['$firebase_cacheTokens'] ?? false; $firebase_tokenCacheDir = $config['firebase_tokencachedir'] ?? $storage_dir; -$psr_logger = new TestServerPsrLogger(); -$test_server = new TestServerController($psr_logger, $host_url, $tiqrauth_protocol, $tiqrenroll_protocol, $token_exchange_url, $token_exchange_appid, $apns_certificate_filename, $apns_environment, $firebase_projectId, $firebase_credentialsFile, $storage_dir, $firebase_cacheTokens, $firebase_tokenCacheDir); -$app = new TestServerApp($test_server); +$current_user = $_SERVER['Remote-User'] ?? 'anonymous'; +// Sanatise the current_user +$current_user = preg_replace('/[^a-zA-Z0-9_]/', '', $current_user); + +$psr_logger = new TestServerPsrLogger($storage_dir . '/' . $current_user . '.log'); +$test_server = new TestServerController($psr_logger, $host_url, $tiqrauth_protocol, $tiqrenroll_protocol, $token_exchange_url, $token_exchange_appid, $apns_certificate_filename, $apns_environment, $firebase_projectId, $firebase_credentialsFile, $storage_dir, $firebase_cacheTokens, $firebase_tokenCacheDir, $current_user); +$app = new TestServerApp($test_server, $psr_logger); $app->HandleHTTPRequest(); return true; From c2a5f970b9002927cfd86b11b18e0458819b6ad5 Mon Sep 17 00:00:00 2001 From: Pieter van der Meulen Date: Tue, 30 Jul 2024 17:30:43 +0200 Subject: [PATCH 5/9] TestServer: Add option to view logfile from the web UI Keep separate logfiles per "Remote-User" --- TestServer/TestServerApp.php | 18 ++- TestServer/TestServerController.php | 173 ++++++++++++++++------------ TestServer/TestServerPsrLogger.php | 30 +++++ TestServer/TestServerView.php | 36 +++++- TestServer/app.php | 2 +- 5 files changed, 173 insertions(+), 86 deletions(-) diff --git a/TestServer/TestServerApp.php b/TestServer/TestServerApp.php index d916952..a071bc7 100644 --- a/TestServer/TestServerApp.php +++ b/TestServer/TestServerApp.php @@ -8,6 +8,7 @@ namespace TestServer; use Exception; +use Psr\Log\LoggerInterface; abstract class App { @@ -63,8 +64,12 @@ class TestServerApp extends App private $BODY = ''; private $router; - public final function __construct($router) + private $logger; + + public final function __construct($router, LoggerInterface $logger) { + $this->logger = $logger; + $this->SERVER = $_SERVER; $this->GET = $_GET; $this->POST = $_POST; @@ -75,24 +80,25 @@ public final function __construct($router) } $this->router = $router; + } function HandleHTTPRequest() { - self::log_info("--== START ==--"); + $this->logger->info("--== START ==--"); $uri = $this->SERVER["REQUEST_URI"]; $method = $this->SERVER["REQUEST_METHOD"]; - self::log_info("$method $uri"); + $this->logger->info("$method $uri"); // Print HTTP headers from the request foreach ($this->SERVER as $k => $v) { if (strpos($k, "HTTP_") === 0) { // Transform back to HTTP header style $k = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($k, 5))))); - self::log_info("$k: $v"); + $this->logger->info("$k: $v"); } } if ($method == 'POST') { - self::log_info($this->BODY); + $this->logger->info($this->BODY); } if (strlen($uri) == 0) { self::error_exit(500, 'Empty REQUEST_URI'); @@ -100,7 +106,7 @@ function HandleHTTPRequest() if ($uri[0] != '/') { self::error_exit(500, 'REQUEST_URI must start with "/"'); } - self::log_info('--'); // End of the HTTP dump + $this->logger->info('----'); // End of the HTTP dump $path = parse_url($uri, PHP_URL_PATH); diff --git a/TestServer/TestServerController.php b/TestServer/TestServerController.php index 8c0c1e2..5ea8796 100644 --- a/TestServer/TestServerController.php +++ b/TestServer/TestServerController.php @@ -7,10 +7,7 @@ namespace TestServer; -use Mockery; use Psr\Log\LoggerInterface; -use Tiqr_AutoLoader; -use Tiqr_OCRAWrapper; use Tiqr_Service; use Tiqr_UserStorage; @@ -26,6 +23,7 @@ class TestServerController private $apns_environment; private $logger; private $storageDir; + private $current_user; private $supportedNotificationTypes = array( 'GCM', @@ -35,12 +33,12 @@ class TestServerController ); /** - * @param $host_url This is the URL by which the tiqr client can reach this server, including http(s):// and port. + * @param $host_url string This is the URL by which the tiqr client can reach this server, including http(s):// and port. * E.g. 'http://my-laptop.local:8000' * - * @param $authProtocol This is the app specific url for authentications of the tiqr client, without '://' + * @param $authProtocol string This is the app specific url for authentications of the tiqr client, without '://' * e.g. 'tiqrauth'. This must match what is configured in the tiqr client - * @param $enrollProtocol This is the app specific url for enrolling user accounts in the tiqr client, without '://' + * @param $enrollProtocol string This is the app specific url for enrolling user accounts in the tiqr client, without '://' * e.g. 'tiqrenroll'. This must match what is configured in the tiqr client * * @param string $token_exchange_url The URL of the tiqr token exchange server @@ -53,15 +51,18 @@ class TestServerController * @param string $storage_dir Directory to use for tiqr state storage, user storage and user sercret storage * @param bool $firebase_cacheTokens Is the cache for accesstokens enabled * @param string $firebase_tokenCacheDir Where is the cache for accesstokens located + * @param string $current_user The current user, used for getting logfile names */ - function __construct(LoggerInterface $logger, string $host_url, string $authProtocol, string $enrollProtocol, string $token_exchange_url, string $token_exchange_appid, string $apns_certificate_filename, string $apns_environment, string $firebase_projectId, string $firebase_credentialsFile, string $storage_dir, bool $firebase_cacheTokens, string $firebase_tokenCacheDir) + function __construct(LoggerInterface $logger, string $host_url, string $authProtocol, string $enrollProtocol, string $token_exchange_url, string $token_exchange_appid, string $apns_certificate_filename, string $apns_environment, string $firebase_projectId, string $firebase_credentialsFile, string $storage_dir, bool $firebase_cacheTokens, string $firebase_tokenCacheDir, string $current_user) { $this->storageDir = $storage_dir; $this->logger = $logger; $this->host_url = $host_url; $this->tiqrService = $this->createTiqrService($host_url, $authProtocol, $enrollProtocol, $token_exchange_url, $token_exchange_appid, $apns_certificate_filename, $apns_environment, $firebase_projectId, $firebase_credentialsFile, $firebase_cacheTokens, $firebase_tokenCacheDir ); $this->userStorage = $this->createUserStorage(); - $this->userSecretStorage = $this->createUserSecretStorage(); + $this->userSecretStorage = $this->createUserSecretStorage(); + + $this->current_user = $current_user; } /** @@ -172,7 +173,7 @@ public function Route(App $app, string $path) $view = new TestServerView(); try { - $app::log_info("host_url=$this->host_url"); + $this->logger->info("host_url=$this->host_url"); switch ($path) { case "/": // Test server home page $view->ShowRoot(); @@ -218,13 +219,17 @@ public function Route(App $app, string $path) $this->authentication($app); break; + case '/show-logs': + $this->show_logs($view); + break; + default: TestServerApp::error_exit(404, "Unknown route '$path'"); } } catch (\Exception $e) { - $app::log_error("Exception: " . $e->getMessage()); - $app::log_error($e); + $this->logger->error("Exception: " . $e->getMessage()); + $this->logger->error($e); $view->Exception($path, $e); } } @@ -235,13 +240,13 @@ private function start_enrollment(App $app, TestServerView $view) // the web browser displaying the enrollment interface. It is not used between the tiqr client and // this server. We do not use it. $session_id = 'session_id_' . time(); - $app::log_info("Created session $session_id"); + $this->logger->info("Created session $session_id"); // The user_id to create. Get it from the request, if it is not there use a test user ID. $user_id = $app->getGET()['user_id'] ?? 'test-user-' . time(); if ($this->userStorage->userExists($user_id)) { - $app::log_warning("$user_id already exists"); + $this->logger->warning("$user_id already exists"); } $user_display_name = $user_id . '\'s display name'; @@ -251,7 +256,7 @@ private function start_enrollment(App $app, TestServerView $view) // Note: we create the user in the userStorage later with a different display name so the displayname in the // App differs from the user's displayname on the server. $enrollment_key = $this->tiqrService->startEnrollmentSession($user_id, $user_display_name, $session_id); - $app::log_info("Started enrollment session $enrollment_key"); + $this->logger->info("Started enrollment session $enrollment_key"); $metadataUrl = $this->host_url . "/metadata"; $enroll_string = $this->tiqrService->generateEnrollString("$metadataUrl?enrollment_key=$enrollment_key"); $encoded_enroll_string = htmlentities(urlencode($enroll_string)); @@ -288,7 +293,7 @@ private function metadata(App $app) } // Generate an enrollment secret to add to the metadata URL $enrollment_secret = $this->tiqrService->getEnrollmentSecret($enrollment_key); - $app::log_info("Created enrollment secret $enrollment_secret for enrollment key $enrollment_key"); + $this->logger->info("Created enrollment secret $enrollment_secret for enrollment key $enrollment_key"); // Note: The enrollment_secret must be added manually to the enrollment URL. // This makes the process of generating the enrollment URL more complex, but gives @@ -307,16 +312,16 @@ private function metadata(App $app) foreach ($enrollment_metadata as $key1 => $value1) { if (is_array($value1)) { foreach ($value1 as $key2 => $value2) { - $app::log_info("Metadata: $key1/$key2=$value2"); + $this->logger->info("Metadata: $key1/$key2=$value2"); } } else { - $app::log_info("Metadata: $key1=$value1"); + $this->logger->info("Metadata: $key1=$value1"); } } // The enrollment metadata must be returned to the client as JSON $enrollment_metadata_json = json_encode($enrollment_metadata, JSON_UNESCAPED_SLASHES); - $app::log_info("Return: $enrollment_metadata_json"); + $this->logger->info("Return: $enrollment_metadata_json"); header("content-type: application/json"); echo $enrollment_metadata_json; } @@ -337,7 +342,7 @@ private function finish_enrollment(App $app) if (false === $userid) { $app::error_exit(404, "Invalid enrollment_secret"); } - $app::log_info("userid: $userid"); + $this->logger->info("userid: $userid"); $secret = $app->getPOST()['secret'] ?? ''; if (strlen($secret) == 0) { @@ -345,21 +350,21 @@ private function finish_enrollment(App $app) } // This is the hex encoded value of the authentication secret that the tiqr client // generated - $app::log_info("secret: $secret"); + $this->logger->info("secret: $secret"); $language = $app->getPOST()['language'] ?? ''; if (strlen($language) == 0) { - $app::log_warning("No language in POST"); + $this->logger->warning("No language in POST"); } // The iso language code e.g. "nl-NL" - $app::log_info("language: $language"); + $this->logger->info("language: $language"); $notificationType = $app->getPOST()['notificationType'] ?? ''; if (strlen($notificationType) == 0) { - $app::log_warning("No notificationType in POST"); + $this->logger->warning("No notificationType in POST"); } // The notification message type (APNS, GCM, FCM ...) - $app::log_info("notificationType: $notificationType"); + $this->logger->info("notificationType: $notificationType"); if (! in_array($notificationType, $this->supportedNotificationTypes)) { $app->log_warning("Unsupported notification type: $notificationType"); @@ -367,40 +372,40 @@ private function finish_enrollment(App $app) $notificationAddress = $app->getPOST()['notificationAddress'] ?? ''; if (strlen($notificationAddress) == 0) { - $app::log_warning("No notificationAddress in POST"); + $this->logger->warning("No notificationAddress in POST"); } // This is the notification address that the Tiqr Client got from the token exchange (e.g. tx.tiqr.org) - $app::log_info("notificationAddress: $notificationAddress"); + $this->logger->info("notificationAddress: $notificationAddress"); $version = $app->getPOST()['version'] ?? ''; if (strlen($version) == 0) { - $app::log_warning("No version in POST"); + $this->logger->warning("No version in POST"); } // ? - $app::log_info("version: $version"); + $this->logger->info("version: $version"); $operation = $app->getPOST()['operation'] ?? ''; if (strlen($operation) == 0) { - $app::log_warning("No operation in POST"); + $this->logger->warning("No operation in POST"); } // Must be "register" - $app::log_info("operation: $operation"); + $this->logger->info("operation: $operation"); if ($operation != 'register') { $app::error_exit(404, "Invalid operation: '$operation'. Expected 'register'"); } // Get User-Agent HTTP header $user_agent = urldecode($_SERVER['HTTP_USER_AGENT'] ?? ''); - $app::log_info("User-Agent: $user_agent"); + $this->logger->info("User-Agent: $user_agent"); // Create the user. Use the display name to store the version the client POSTed and the user-agent it sent // in this POST request's header. $this->userStorage->createUser($userid, "$version | $user_agent"); - $app::log_info("Created user $userid"); + $this->logger->info("Created user $userid"); // Set the user secret $this->userSecretStorage->setSecret($userid, $secret); - $app::log_info("Secret for $userid was stored"); + $this->logger->info("Secret for $userid was stored"); // Store notification type and the notification address that the client sent us $this->userStorage->setNotificationType($userid, $notificationType); @@ -408,7 +413,7 @@ private function finish_enrollment(App $app) // Finalize the enrollemnt $this->tiqrService->finalizeEnrollment($enrollment_secret); - $app::log_info("Enrollment was finalized"); + $this->logger->info("Enrollment was finalized"); // Must return "OK" to the tiqr client after a successful enrollment echo "OK"; @@ -446,7 +451,7 @@ function list_users(App $app, TestServerView $view) private function start_authenticate(App $app, TestServerView $view) { $session_id = 'session_id_' . time(); - $app::log_info("Created session $session_id"); + $this->logger->info("Created session $session_id"); // The user_id to authenticate. Get it from the request, if it is not there use an empty user ID // Both scenario's are support by tiqr: @@ -457,32 +462,32 @@ private function start_authenticate(App $app, TestServerView $view) // Get optional user ID $user_id = $app->getGET()['user_id'] ?? ''; if (strlen($user_id) > 0) { - $app::log_info("Authenticating user '$user_id'"); + $this->logger->info("Authenticating user '$user_id'"); if (!$this->userStorage->userExists($user_id)) { - $app::log_warning("'$user_id' is not known on the server"); + $this->logger->warning("'$user_id' is not known on the server"); } } // Start authentication session $session_key = $this->tiqrService->startAuthenticationSession($user_id, $session_id); - $app::log_info('Started authentication session'); - $app::log_info("session_key=$session_key"); + $this->logger->info('Started authentication session'); + $this->logger->info("session_key=$session_key"); // Get authentication URL for the tiqr client (to put in the QR code) $authentication_URL = $this->tiqrService->generateAuthURL($session_key); - $app::log_info('Started authentication URL'); - $app::log_info("authentication_url=$authentication_URL"); + $this->logger->info('Started authentication URL'); + $this->logger->info("authentication_url=$authentication_URL"); $image_url = "/qr?code=" . htmlentities(urlencode($authentication_URL)); $response = ''; if (strlen($user_id) > 0) { // Calculate response - $app::log_info("Calculating response for $user_id"); + $this->logger->info("Calculating response for $user_id"); $secret = $this->userSecretStorage->getSecret($user_id); - $app::log_info("secret=$secret"); + $this->logger->info("secret=$secret"); $challenge=''; // Parse the authentication URL to get the challenge question @@ -495,9 +500,9 @@ private function start_authenticate(App $app, TestServerView $view) $exploded = explode('/', $authentication_URL); $challenge = $exploded[4]; // 10 digit hex challenge } - $app::log_info("challenge=$challenge"); + $this->logger->info("challenge=$challenge"); $response=\OCRA::generateOCRA('OCRA-1:HOTP-SHA1-6:QH10-S', $secret, '', $challenge, '', $session_key, ''); - $app::log_info("response=$response"); + $this->logger->info("response=$response"); } $view->StartAuthenticate(htmlentities($authentication_URL), $image_url, $user_id, $response, $session_key); @@ -552,32 +557,32 @@ private function authentication(App $app) // This should be the session key from the authentication URL that we generated $sessionKey = $app->getPOST()['sessionKey'] ?? ''; if (strlen($sessionKey) == 0) { - $app::error_exit(404, "Missing sessionKey is POST"); + $app::error_exit(404, "Missing sessionKey in POST"); } - $app::log_info("sessionKey: $sessionKey"); + $this->logger->info("sessionKey: $sessionKey"); // The userId the client authenticated $userId = $app->getPOST()['userId'] ?? ''; if (strlen($userId) == 0) { - $app::error_exit(404, "Missing $userId is POST"); + $app::error_exit(404, "Missing $userId in POST"); } - $app::log_info("userId: $userId"); + $this->logger->info("userId: $userId"); // Get version from POST $version = $app->getPOST()['version'] ?? ''; if (strlen($version) == 0) { - $app::log_warning("No version in POST"); + $this->logger->warning("No version in POST"); } // ? - $app::log_info("version: $version"); + $this->logger->info("version: $version"); // Get operation from POST $operation = $app->getPOST()['operation'] ?? ''; if (strlen($operation) == 0) { - $app::log_warning("No operation in POST"); + $this->logger->warning("No operation in POST"); } // Must be "login" - $app::log_info("operation: $operation"); + $this->logger->info("operation: $operation"); if ($operation != 'login') { $app::error_exit(404, "Invalid operation: '$operation'. Expected 'login'"); } @@ -585,23 +590,23 @@ private function authentication(App $app) // Get response from POST $response = $app->getPOST()['response'] ?? ''; if (strlen($response) == 0) { - $app::log_warning("No response in POST"); + $this->logger->warning("No response in POST"); } - $app::log_info("response: $response"); + $this->logger->info("response: $response"); $language = $app->getPOST()['language'] ?? ''; if (strlen($language) == 0) { - $app::log_warning("No language in POST"); + $this->logger->warning("No language in POST"); } // The iso language code e.g. "nl-NL" - $app::log_info("language: $language"); + $this->logger->info("language: $language"); $notificationType = $app->getPOST()['notificationType'] ?? ''; if (strlen($notificationType) == 0) { - $app::log_warning("No notificationType in POST"); + $this->logger->warning("No notificationType in POST"); } // The notification message type (APNS, GCM, FCM ...) - $app::log_info("notificationType: $notificationType"); + $this->logger->info("notificationType: $notificationType"); if (! in_array($notificationType, $this->supportedNotificationTypes)) { $app->log_warning("Unsupported notification type: $notificationType"); @@ -609,40 +614,48 @@ private function authentication(App $app) $notificationAddress = $app->getPOST()['notificationAddress'] ?? ''; if (strlen($notificationAddress) == 0) { - $app::log_warning("No notificationAddress in POST"); + $this->logger->warning("No notificationAddress in POST"); } // This is the notification address that the Tiqr Client got from the token exchange (e.g. tx.tiqr.org) - $app::log_info("notificationAddress: $notificationAddress"); - - // Note: a production tiqr server will now update the notification type and notification address in the - // user storage, we do not. We only log when they are different + // or the actual notification address for APNS_DIRECT and FCM_DIRECT + $this->logger->info("notificationAddress: $notificationAddress"); $notificationType_from_userStorage = $this->userStorage->getNotificationType($userId); $notificationAddress_from_userStorage = $this->userStorage->getNotificationAddress($userId); + $bUpdateNotificationAddress = false; if ($notificationAddress != $notificationAddress_from_userStorage) { - $app::log_warning("Client sent different notification address. client=$notificationAddress, server=$notificationAddress_from_userStorage"); + $this->logger->info("Client sent different notification address. client=$notificationAddress, server=$notificationAddress_from_userStorage"); + $bUpdateNotificationAddress = true; } if ($notificationType != $notificationType_from_userStorage) { - $app::log_warning("Client sent different notification type. client=$notificationType, server=$notificationType_from_userStorage"); + $this->logger->info("Client sent different notification type. client=$notificationType, server=$notificationType_from_userStorage"); + $bUpdateNotificationAddress = true; + } + + // Update the notification address and type when the client sent different values + if ($bUpdateNotificationAddress) { + $this->logger->info("Updating notification address and type"); + $this->userStorage->setNotificationAddress($userId, $notificationAddress); + $this->userStorage->setNotificationType($userId, $notificationType); } // Get User-Agent HTTP header $user_agent = urldecode($_SERVER['HTTP_USER_AGENT'] ?? ''); - $app::log_info("User-Agent: $user_agent"); + $this->logger->info("User-Agent: $user_agent"); $result = 'ERROR'; // Result for the tiqr Client // 'OK', 'INVALID_CHALLENGE', 'INVALID_REQUEST', 'INVALID_RESPONSE', 'INVALID_USER' if (!$this->userStorage->userExists($userId)) { - $app::log_error("Unknown user: $userId "); + $this->logger->error("Unknown user: $userId "); $result = 'INVALID_USER'; } // Lookup the secret of the user by ID $userSecret = $this->userSecretStorage->getSecret($userId); // Assume this works - $app::log_info("userSercret=$userSecret"); + $this->logger->info("userSercret=$userSecret"); - $app::log_info("Authenticating user"); + $this->logger->info("Authenticating user"); $result = $this->tiqrService->authenticate($userId, $userSecret, $sessionKey, $response); $resultStr = 'ERROR'; switch ($result) { @@ -667,20 +680,30 @@ private function authentication(App $app) try { if ($notificationAddress != $notificationAddress_from_userStorage) { $this->userStorage->setNotificationAddress($userId, $notificationAddress); - log_info("Updated notification address"); + $this->logger->info("Updated notification address"); } if ($notificationType != $notificationType_from_userStorage) { $this->userStorage->setNotificationType($userId, $notificationType); - log_info("Updated notification type"); + $this->logger->info("Updated notification type"); } } catch (\Exception $e) { - $app::log_warning('Updating push notification information failed'); - $app::log_warning($e); + $this->logger->warning('Updating push notification information failed'); + $this->logger->warning($e); } } - $app::log_info("Returning authentication result '$resultStr'"); + $this->logger->info("Returning authentication result '$resultStr'"); echo $resultStr; } + + + private function show_logs($view) + { + $logFile = $this->getStorageDir() . '/' . $this->current_user . '.log'; + $logs = file_get_contents($logFile); + // Reverse order so that newest lines are shown first + $logs = array_reverse(explode("\n", $logs)); + $view->ShowLogs($logs); + } } diff --git a/TestServer/TestServerPsrLogger.php b/TestServer/TestServerPsrLogger.php index 6625b02..5ff16dd 100644 --- a/TestServer/TestServerPsrLogger.php +++ b/TestServer/TestServerPsrLogger.php @@ -8,8 +8,38 @@ class TestServerPsrLogger extends AbstractLogger { + private $logFile; + private $logid; + public function __construct(string $logFile = '') + { + // Parent has no __construct + + // Set logid to a unique value + $this->logid = substr(uniqid(), -6); + + $highwater = 500; // lines + $lowwater = 400; // lines + + // Keep the size of the logfile between $lowwater and $highwater lines by truncating it to $lowwater lines when + // it exceeds $highwater lines by removing the oldest (first) lines. + $this->logFile = $logFile; + if ((strlen($this->logFile) != 0) && file_exists($this->logFile)) { + $lines = file($this->logFile); + if (count($lines) > $highwater) { + $lines = array_slice($lines, -$lowwater); + file_put_contents($this->logFile, $lines); + } + } + } public function log($level, string|\Stringable $message, array $context = []): void { error_log(">$level $message"); + + if (strlen($this->logFile) == 0) { + return; + } + // Write the $message to our log file + $logline = date('Y-m-d H:i:s') . " [$this->logid] $level $message\n"; + file_put_contents($this->logFile, $logline, FILE_APPEND); } } \ No newline at end of file diff --git a/TestServer/TestServerView.php b/TestServer/TestServerView.php index 343916e..a154bb5 100644 --- a/TestServer/TestServerView.php +++ b/TestServer/TestServerView.php @@ -13,9 +13,10 @@ public function ShowRoot($args = array()) : void { $this->begin(); echo <<Tiqr Test Server -Enroll new user

-Authenticate user

-list users

+Enroll a new user

+Start authentication

+list users, authenticate a specific user, send push notification

+Show logs

HTML; $this->end(); } @@ -24,7 +25,8 @@ public function ListUsers($users) { $this->begin(); echo <<List of users -

This is the list of user IDs that are registered on this server. Click a user ID to start an authentication for that user.

+

This is the list of user IDs that are registered on this server. Click a user ID to start an authentication for that user. +This also gives you the option to start the authentication by sending a push notification.

@@ -162,7 +164,33 @@ public function Exception(string $path, \Exception $e) $this->end(); } + /* + * @param array $logs Array of strings with log entries to show. Entries are ordered newest first + */ + public function ShowLogs($logs) + { + $this->begin(); + echo '

Logs

'; + + foreach ($logs as $log) { + if (strpos($log, '--== START ==--')) { + echo '' . htmlentities($log) . '

'; + } else if (stripos($log, 'error')) { + echo '' . htmlentities($log) . '
'; + } else if (stripos($log, 'warning')) { + echo '' . htmlentities($log) . '
'; + } else if (stripos($log, 'notice')) { + echo '' . htmlentities($log) . '
'; + } else if (stripos($log, 'debug')) { + echo '' . htmlentities($log) . '
'; + } + else { + echo htmlentities($log) . '
'; + } + } + $this->end(); + } } diff --git a/TestServer/app.php b/TestServer/app.php index a2d82b5..d7807ed 100644 --- a/TestServer/app.php +++ b/TestServer/app.php @@ -50,7 +50,7 @@ $firebase_cacheTokens = $config['$firebase_cacheTokens'] ?? false; $firebase_tokenCacheDir = $config['firebase_tokencachedir'] ?? $storage_dir; -$current_user = $_SERVER['Remote-User'] ?? 'anonymous'; +$current_user = $_SERVER['HTTP_REMOTE_USER'] ?? 'anonymous'; // Sanatise the current_user $current_user = preg_replace('/[^a-zA-Z0-9_]/', '', $current_user); From 4cf67665ca6bac363ef3700d9702631fe0c9801e Mon Sep 17 00:00:00 2001 From: Pieter van der Meulen Date: Tue, 30 Jul 2024 17:31:47 +0200 Subject: [PATCH 6/9] APNS2: Log openssl errors when APNS certificate cannot be parsed --- library/tiqr/Tiqr/Message/APNS2.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/tiqr/Tiqr/Message/APNS2.php b/library/tiqr/Tiqr/Message/APNS2.php index 571969d..cbbba46 100644 --- a/library/tiqr/Tiqr/Message/APNS2.php +++ b/library/tiqr/Tiqr/Message/APNS2.php @@ -49,6 +49,10 @@ public function send() $cert=openssl_x509_parse( $cert_file_contents ); if (false === $cert) { + // Log openssl error information + while ($msg = openssl_error_string()) { + $this->logger->error('openssl_x509_parse(): ' . $msg); + } throw new RuntimeException('Error parsing APNS client certificate'); } $bundle_id = $cert['subject']['UID'] ?? NULL; From af78cb9fd0b60bcf45adb3007568091cb359fd82 Mon Sep 17 00:00:00 2001 From: Pieter van der Meulen Date: Thu, 1 Aug 2024 12:57:57 +0200 Subject: [PATCH 7/9] Change more TestServer log statements to use the PSR logger This should be all of them --- TestServer/TestServerController.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/TestServer/TestServerController.php b/TestServer/TestServerController.php index 5ea8796..73c12bb 100644 --- a/TestServer/TestServerController.php +++ b/TestServer/TestServerController.php @@ -367,7 +367,7 @@ private function finish_enrollment(App $app) $this->logger->info("notificationType: $notificationType"); if (! in_array($notificationType, $this->supportedNotificationTypes)) { - $app->log_warning("Unsupported notification type: $notificationType"); + $this->logger->warning("Unsupported notification type: $notificationType"); } $notificationAddress = $app->getPOST()['notificationAddress'] ?? ''; @@ -514,20 +514,20 @@ private function send_push_notification(App $app, TestServerView $view) { if (strlen($user_id) == 0) { $app::error_exit(404, "Missing user_id in POST"); } - $app->log_info("user_id = $user_id"); + $this->logger->info("user_id = $user_id"); $session_key = $app->getGET()['session_key'] ?? ''; if (strlen($session_key) == 0) { $app::error_exit(404, "Missing session_key in POST"); } - $app->log_info("session_key = $session_key"); + $this->logger->info("session_key = $session_key"); // Get Notification address and type from userid $notificationType=$this->userStorage->getNotificationType($user_id); - $app->log_info("notificationType = $notificationType"); + $this->logger->info("notificationType = $notificationType"); if (! in_array($notificationType, $this->supportedNotificationTypes)) { - $app->log_warning("Unsupported notification type: $notificationType"); + $this->logger->warning("Unsupported notification type: $notificationType"); } $notificationAddress=$this->userStorage->getNotificationAddress($user_id); @@ -536,7 +536,7 @@ private function send_push_notification(App $app, TestServerView $view) { // translateNotificationAddress does not translate the new APNS_DIRECT and FCM_DIRECT notificationType, it // only translates APNS, GCM and FCM. For any other types it returns the unmodified $notificationAddress $deviceNotificationAddress = $this->tiqrService->translateNotificationAddress($notificationType, $notificationAddress); - $app->log_info("deviceNotificationAddress (from token exchange) = $deviceNotificationAddress"); + $this->logger->info("deviceNotificationAddress (from token exchange) = $deviceNotificationAddress"); // Note that the current Tiqr app returns notification type 'APNS' or 'GCM'. // The Google Cloud Messaging (GCM) API - implemented in the Tiqr_Message_GCM class - is deprecated and has @@ -544,9 +544,9 @@ private function send_push_notification(App $app, TestServerView $view) { // So even though the tiqr app returns GCM we actually use FCM implemented by Tiqr_Message_FCM // sendAuthNotification() accepts GCM, FCM_DIRECT and knows to use Tiqr_Message_FCM instead. For both APNS and // APNS_DIRECT Tiqr_Message_APNS will be used. - $app->log_info("Sending push notification using $notificationType to $deviceNotificationAddress"); + $this->logger->info("Sending push notification using $notificationType to $deviceNotificationAddress"); $this->tiqrService->sendAuthNotification($session_key, $notificationType, $deviceNotificationAddress); - $app->log_info("Push notification sent"); + $this->logger->info("Push notification sent"); $view->PushResult("Sent $notificationType to $deviceNotificationAddress"); } @@ -609,7 +609,7 @@ private function authentication(App $app) $this->logger->info("notificationType: $notificationType"); if (! in_array($notificationType, $this->supportedNotificationTypes)) { - $app->log_warning("Unsupported notification type: $notificationType"); + $this->logger->warning("Unsupported notification type: $notificationType"); } $notificationAddress = $app->getPOST()['notificationAddress'] ?? ''; From d89e8deb6e7d0fc9c044c4b94b33ae6c73169688 Mon Sep 17 00:00:00 2001 From: Pieter van der Meulen Date: Thu, 1 Aug 2024 22:24:08 +0200 Subject: [PATCH 8/9] TestServer: Allow users to override selected configuration options For now: allow user to switch between APNS production and sandbox --- TestServer/TestServerController.php | 110 +++++++++++++++++++++++++++- TestServer/TestServerView.php | 31 ++++++++ 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/TestServer/TestServerController.php b/TestServer/TestServerController.php index 73c12bb..ee202b8 100644 --- a/TestServer/TestServerController.php +++ b/TestServer/TestServerController.php @@ -7,6 +7,7 @@ namespace TestServer; +use http\Exception\RuntimeException; use Psr\Log\LoggerInterface; use Tiqr_Service; use Tiqr_UserStorage; @@ -19,12 +20,31 @@ class TestServerController private $userStorage; private $userSecretStorage; private $host_url; - private $apns_certificate_filename; - private $apns_environment; private $logger; private $storageDir; private $current_user; + // define the configuration options that a user may change (override) + private $allowed_user_config = array( + 'apns_environment', + // 'some_other_option', + ); + + // Note: user_config, global_config and current_config are only used for displaying the current configuration + // and allowing the user to update it. The actual configuration that is used is locked in place in the constructor + // Updating the *_config arrays does not affect the actual configuration of tiqrService, userStorage etc until these + // are constructed again (i.e. at the next HTTP request) + + // User configuration options overrides + // Update the $current_user.config file in the storage directory to change these + private $user_config; + + // List of configuration options without taking the user configuration into account + private $global_config; + + // List of configuration options that are currently in effect (i.e. global_config with user_config overrides) + private $current_config; + private $supportedNotificationTypes = array( 'GCM', 'APNS', @@ -58,6 +78,45 @@ function __construct(LoggerInterface $logger, string $host_url, string $authProt $this->storageDir = $storage_dir; $this->logger = $logger; $this->host_url = $host_url; + + // Store configuration options that were used for displaying to the user + $this->global_config = array( + 'host_url' => $host_url, + 'current_user' => $current_user, + 'authProtocol' => $authProtocol, + 'enrollProtocol' => $enrollProtocol, + 'token_exchange_url' => $token_exchange_url, + 'token_exchange_appid' => $token_exchange_appid, + 'apns_environment' => $apns_environment, + 'firebase_projectId' => $firebase_projectId, + 'firebase_cacheTokens' => $firebase_cacheTokens ? 'true' : 'false', + ); + + // Load user config if it exists, and make a current config from the global config with user config overrides + $this->current_config = $this->global_config; + if (file_exists($storage_dir . '/' . $current_user . '.config')) { + $this->user_config = json_decode(file_get_contents($storage_dir . '/' . $current_user . '.config'), true); + if (json_last_error() != JSON_ERROR_NONE) { + $this->logger->error('Error parsing user configuration file: ' . json_last_error_msg()); + } + + // Override configuration options + if (isset($this->user_config['apns_environment'])) { + $apns_environment = $this->user_config['apns_environment']; + $this->logger->info("Overriding default apns_environment to $apns_environment from user configuration"); + } + + /* + if (isset($this->user_config['some_other_option'])) { + $some_other_option = $this->user_config['some_other_option']; + $this->logger->info("Overriding default some_other_option to $some_other_option from user configuration"); + } + */ + } + else { + $this->user_config = array(); + } + $this->tiqrService = $this->createTiqrService($host_url, $authProtocol, $enrollProtocol, $token_exchange_url, $token_exchange_appid, $apns_certificate_filename, $apns_environment, $firebase_projectId, $firebase_credentialsFile, $firebase_cacheTokens, $firebase_tokenCacheDir ); $this->userStorage = $this->createUserStorage(); $this->userSecretStorage = $this->createUserSecretStorage(); @@ -223,6 +282,14 @@ public function Route(App $app, string $path) $this->show_logs($view); break; + case '/show-config': + $this->show_config($view); + break; + + case '/update-config': + $this->update_config($app, $view); + break; + default: TestServerApp::error_exit(404, "Unknown route '$path'"); } @@ -698,7 +765,7 @@ private function authentication(App $app) } - private function show_logs($view) + private function show_logs(TestServerView $view) { $logFile = $this->getStorageDir() . '/' . $this->current_user . '.log'; $logs = file_get_contents($logFile); @@ -706,4 +773,41 @@ private function show_logs($view) $logs = array_reverse(explode("\n", $logs)); $view->ShowLogs($logs); } + + + private function show_config(TestServerView $view) + { + $view->ShowConfig($this->current_config, $this->user_config); + } + + private function update_config(App $app, TestServerView $view) + { + $user_config = array(); + + // Get the allowed keys from the POST'ed user configuration and remove any empty ones (== "default") + $post = $app->getPOST(); + foreach ($this->allowed_user_config as $key) { + if (isset($post[$key])) { + $user_config[$key] = $post[$key]; + if ($post[$key] == '') { + $this->current_config[$key] = $this->global_config[$key]; // Reset to default from global config + } + else { + $this->current_config[$key] = $post[$key]; // Update option from POST + } + } + } + $this->user_config = $user_config; + + // Write the user configuration to the storage directory + $storageDir = $this->getStorageDir(); + $user_config_file = $storageDir . '/' . $this->current_user . '.config'; + if (false === file_put_contents($user_config_file, json_encode($user_config, JSON_PRETTY_PRINT)) ) { + $this->logger->error("Error writing user configuration to $user_config_file"); + throw new RuntimeException("Error writing user configuration to $user_config_file"); + } + $this->logger->info("Wrote updated user configuration to $user_config_file"); + + $view->ShowConfig($this->current_config, $this->user_config); + } } diff --git a/TestServer/TestServerView.php b/TestServer/TestServerView.php index a154bb5..f9583bd 100644 --- a/TestServer/TestServerView.php +++ b/TestServer/TestServerView.php @@ -17,6 +17,7 @@ public function ShowRoot($args = array()) : void { Start authentication

list users, authenticate a specific user, send push notification

Show logs

+Show config

HTML; $this->end(); } @@ -191,6 +192,36 @@ public function ShowLogs($logs) $this->end(); } + + public function ShowConfig($config, $user_config) { + $this->begin(); + echo '

Configuration

'; + echo '

Current configuration

'; + foreach ($config as $key => $value) { + echo "".htmlentities($key).": ".htmlentities($value)."
"; + } + + echo '

User configuration

'; + // Show input form with the user configuration options to allow the user to change them + echo '
'; + $apns_environment = $user_config['apns_environment'] ?? ''; + echo '
'; + echo ' Default
'; + echo ' Sandbox
'; + echo ' Production
'; + + /* + $some_other_option = $user_config['some_other_option'] ?? ''; + echo '
'; + echo '
'; + */ + + echo '
'; + echo ''; + echo ''; + + $this->end(); + } } From 587fc022a35ffc3f6cdce1b2a6db227af5486ca4 Mon Sep 17 00:00:00 2001 From: Pieter van der Meulen Date: Thu, 1 Aug 2024 22:27:43 +0200 Subject: [PATCH 9/9] TestServer: add footer with a little Tiqr project info --- TestServer/TestServerView.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TestServer/TestServerView.php b/TestServer/TestServerView.php index f9583bd..23b1206 100644 --- a/TestServer/TestServerView.php +++ b/TestServer/TestServerView.php @@ -90,6 +90,12 @@ private function end() { echo << Home
+
+
+ +This is a Tiqr TestServer. The TestServer is a simple web application aimed at Developers and Tester for testing a Tiqr client with the Tiqr tiqr-server-libphp library.
+See tiqr.org for more information about the Tiqr project. +
HTML;
userId