diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e163413 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Local XAMPP defaults. Do not commit real credentials. +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_NAME=fingerprint +DB_USER=root +DB_PASSWORD= + +# XAMPP + local Docker: +FP_CLIENT_SERVICE_HOST=http://127.0.0.1:5555 +FP_CLIENT_TIMEOUT=15 +DIAGNOSTICS_ENABLED=1 + +# Hosted PHP example (use a private HTTPS service): +# FP_CLIENT_SERVICE_HOST=https://fingerprint-service.example.com diff --git a/.github/ISSUE_TEMPLATE/compatibility.yml b/.github/ISSUE_TEMPLATE/compatibility.yml new file mode 100644 index 0000000..0c10872 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/compatibility.yml @@ -0,0 +1,53 @@ +name: Compatibility report +description: Report Windows, XAMPP, reader, Docker, or hosted-deployment failures +title: "[Compatibility]: " +labels: ["compatibility"] +body: + - type: markdown + attributes: + value: Do not attach fingerprint samples, database passwords, tokens, or unredacted production logs. + - type: input + id: windows + attributes: + label: Windows edition and build + description: Run winver and include the complete edition, version and OS build. + validations: + required: true + - type: input + id: php + attributes: + label: XAMPP and PHP versions + validations: + required: true + - type: dropdown + id: deployment + attributes: + label: Deployment profile + options: + - XAMPP and Docker on the same Windows PC + - Hosted PHP/MySQL and hosted fingerprint service + - Shared PHP hosting and external fingerprint service + - Other + validations: + required: true + - type: textarea + id: diagnostics + attributes: + label: Redacted diagnostics + description: Include the diagnostics endpoint, windows-preflight.ps1, docker compose ps, and relevant container logs. + render: shell + validations: + required: true + - type: textarea + id: browser + attributes: + label: Browser and reader status + description: Include browser/version, console errors, whether Device Manager sees the reader, and whether the DigitalPersona Web SDK service is running. + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + validations: + required: true diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..661fb8b --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,25 @@ +name: Compatibility checks + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + php-and-compose: + runs-on: ubuntu-latest + strategy: + matrix: + php: ['8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mysqli + - name: Lint application PHP + run: find . -path './src/core/helpers/Requests' -prune -o -path './fingerprint_service/thirdparty' -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l + - name: Validate Compose configuration + run: docker compose -f fingerprint_service/docker-compose.yml config --quiet diff --git a/.gitignore b/.gitignore index ffd2a4a..ba3046e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ *.idea +configs.local.php +.env diff --git a/README.md b/README.md index 5f92924..b11e488 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,22 @@ the only way to interact with the fingerprint software engine was to use C++, ja ### The Solution We wrap the fingerprint engine with [grpc](https://grpc.io/) and [protobuf](https://developers.google.com/protocol-buffers/). This allows us to use the engine in any language supported by grpc and there are quite a lot of them. But this is not what this codebase is about. You can find the link to the grpc codebase [here](https://github.com/Bexils/grpc-fingerprint-engine). I realized most people just needed a solution they could plugin to their codebase and forget about it and hence why this codebase was created. It's an abstraction of the grpc solution, completely containerized with docker that can be setup and ready to use with just a few commands. This solution setup a web services on your local machine or server that can be used to make enrollment and verification requests. ## Getting started +This repository contains a Windows browser-capture layer, a PHP/MySQL example +application, and a Dockerized Linux matching service. They must all be working; +installing Windows Pro or XAMPP alone is not sufficient. + +Choose a deployment profile before starting: + +- **Local XAMPP:** PHP, MySQL and Docker run on the Windows capture computer. +- **Hosted:** the Windows computer captures fingerprints, while PHP, MySQL and + the private matching service run on the server. +- **Shared hosting:** supported only when the host permits PHP to reach a + separately deployed matching service. + +See [the compatibility guide](docs/COMPATIBILITY.md) for complete setup steps +and [troubleshooting](docs/TROUBLESHOOTING.md) for diagnostic commands. Read +[the security notice](SECURITY.md) before handling real biometric data. + ### Download SDK (Mandatory) Note: the SDK below is for windows only. Considering most people will be using the scanner on windows (i.e the frontend or client side), if you are already able to capture fingerprint with the device you don't even need this. The solution for the backend enrollment and verification service doesn't require any of this, as it's already packaged in the container. @@ -34,7 +50,8 @@ SDK v3 [Download](https://drive.google.com/open?id=1Hg3HAqwLuqmi9F4eUAgb5WaeLyJv SDKPatch [Download](https://drive.google.com/file/d/1yB0oW3NtBfCBKFHSe0Blc7B7grMD854W/view?usp=sharing) ### Setup the web service (Mandatory for all users) -Regardless of where you need to set this up, you will have to install docker, which is very easy to do. +The fingerprint matching service requires Docker unless you provide a compatible +remote service. A shared hosting account generally cannot run this container. ### Install Docker for Windows (Mandatory for windows users) Download Docker Desktop for windows [here](https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe) or [visit the page](https://docs.docker.com/desktop/windows/install/) for more information. @@ -64,7 +81,7 @@ cd FingerPrint cd fingerprint_service # build and start the service -docker-compose up +docker compose up -d --build ``` ### Test the service @@ -74,24 +91,17 @@ For windows users using xampp, that will be C:\xampp\htdocs or thereabouts. For linux users, that will be /var/www/html or thereabouts. -Visit http://localhost/FingerPrint or http://localhost +Visit http://localhost/FingerPrint/ or http://localhost/ depending on your web root configuration. I do advice to change the web root directly to the FingerPrint directory. +Before opening the application, check +`http://localhost/FingerPrint/src/core/diagnostics.php` (adjust the base path if +the project is your web root). + Note: For this test we use a mysql database, but you can use any other database you want. You will need to have a database named `fingerprint`, with the below structure. -```sql -/*create fingerprint database*/ -CREATE DATABASE fingerprint; - -/*create users table, with id as primary key -username, fullname columns and also nullable index and middle finger columns*/ -CREATE TABLE `users` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `username` varchar(255) NOT NULL, - `fullname` varchar(255) NOT NULL, - `indexfinger` varchar(4000) NULL, - `middlefinger` varchar(4000) NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; -``` +Import [`database/schema.sql`](database/schema.sql), copy +`configs.local.php.example` to `configs.local.php`, and set the local database +and fingerprint-service values. The local file is ignored by Git. + Add some data and leave the index finger and middle finger columns empty. Now you should be able to enroll and verify fingerprints. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6256f13 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security + +This repository is a proof of concept and must not be deployed as a production +biometric identity system without a security review. + +## Reporting a vulnerability + +Open a GitHub security advisory for vulnerabilities. Do not post credentials, +fingerprint samples, FMD templates, personal data, or exploitable details in a +public issue. + +## Deployment baseline + +- Serve the browser application over HTTPS outside localhost. +- Keep ports 4134 and 5555 on loopback or a private authenticated network. +- Authenticate users and authorize enrollment and verification separately. +- Add CSRF protection, request-size limits and rate limiting to PHP endpoints. +- Encrypt biometric templates at rest with managed, rotated keys. +- Never log request bodies containing FMD data. +- Use a dedicated least-privileged MySQL account, not `root`. +- Define retention, deletion, consent and audit policies appropriate to the + jurisdiction in which biometric data is processed. + +The provided Compose configuration binds service ports to `127.0.0.1` by +default. Operators who deliberately expose them are responsible for adding TLS, +authentication and network policy first. diff --git a/configs.local.php.example b/configs.local.php.example new file mode 100644 index 0000000..aecc9d1 --- /dev/null +++ b/configs.local.php.example @@ -0,0 +1,10 @@ +/src/core/diagnostics.php`. + +## MySQL connection fails + +Confirm Apache and MySQL are running in XAMPP, import `database/schema.sql`, and configure `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER` and `DB_PASSWORD`. Do not use a passwordless root account outside a local development computer. + +## Information to include in a bug report + +- Output of `winver`. +- Whether Docker Desktop uses WSL 2 or Hyper-V. +- Output of `wsl --version`, `docker version`, and `docker compose ps`. +- Redacted service logs. +- XAMPP and PHP versions. +- Exact application URL. +- Browser name/version and redacted console errors. +- Diagnostic endpoint output. +- Whether Device Manager detects the reader. diff --git a/fingerprint_service/.env.example b/fingerprint_service/.env.example new file mode 100644 index 0000000..d9d7887 --- /dev/null +++ b/fingerprint_service/.env.example @@ -0,0 +1,7 @@ +# The proprietary DigitalPersona Linux libraries bundled by the engine are +# currently built for x86-64. +DOCKER_PLATFORM=linux/amd64 +FINGERPRINT_ENGINE_PORT=4134 +FINGERPRINT_CLIENT_PORT=5555 +FINGERPRINT_ENGINE_IMAGE=fingerprint-engine:local +FINGERPRINT_CLIENT_IMAGE=fingerprint-client:local diff --git a/fingerprint_service/Dockerfile b/fingerprint_service/Dockerfile index 69b1b7c..e129f10 100644 --- a/fingerprint_service/Dockerfile +++ b/fingerprint_service/Dockerfile @@ -4,10 +4,9 @@ WORKDIR /var/www/html COPY . . -RUN composer update && \ - composer install && \ - cd thirdparty/grpc-fingerprint-engine/src/php/ && \ - composer update && \ - composer install && \ - apt update && apt install -y net-tools +RUN composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader && \ + cd thirdparty/grpc-fingerprint-engine/src/php/ && \ + composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader && \ + apt-get update && apt-get install -y --no-install-recommends net-tools && \ + rm -rf /var/lib/apt/lists/* diff --git a/fingerprint_service/coreComponents/basicRequirements.php b/fingerprint_service/coreComponents/basicRequirements.php index 3ab4985..0a403ca 100644 --- a/fingerprint_service/coreComponents/basicRequirements.php +++ b/fingerprint_service/coreComponents/basicRequirements.php @@ -5,6 +5,6 @@ * About: I will tell you later */ -require_once("../coreComponents/UrlEncode.php"); -require_once("../coreComponents/fingerprint.php"); +require_once(__DIR__ . "/UrlEncode.php"); +require_once(__DIR__ . "/fingerprint.php"); diff --git a/fingerprint_service/coreComponents/enroll.php b/fingerprint_service/coreComponents/enroll.php index cf86582..6951ab6 100644 --- a/fingerprint_service/coreComponents/enroll.php +++ b/fingerprint_service/coreComponents/enroll.php @@ -6,7 +6,7 @@ * @date 2020-04-18 14:28:39 */ -require_once("../coreComponents/basicRequirements.php"); +require_once(__DIR__ . "/basicRequirements.php"); if (!empty($_POST["data"])) { $user_data = json_decode($_POST["data"]); diff --git a/fingerprint_service/coreComponents/is_duplicate.php b/fingerprint_service/coreComponents/is_duplicate.php index 3637175..fb6d999 100644 --- a/fingerprint_service/coreComponents/is_duplicate.php +++ b/fingerprint_service/coreComponents/is_duplicate.php @@ -7,7 +7,7 @@ * @version 1.0.0 */ -require_once("../coreComponents/basicRequirements.php"); +require_once(__DIR__ . "/basicRequirements.php"); if(!empty($_POST["data"])) { $user_data = json_decode($_POST["data"]); @@ -27,4 +27,4 @@ } else { echo json_encode("error! no data provided in post request"); -} \ No newline at end of file +} diff --git a/fingerprint_service/coreComponents/verify.php b/fingerprint_service/coreComponents/verify.php index 1e5e780..46cfd7f 100644 --- a/fingerprint_service/coreComponents/verify.php +++ b/fingerprint_service/coreComponents/verify.php @@ -7,7 +7,7 @@ * @version 1.0.0 */ -require_once("../coreComponents/basicRequirements.php"); +require_once(__DIR__ . "/basicRequirements.php"); if(!empty($_POST["data"])) { $user_data = json_decode($_POST["data"]); @@ -32,4 +32,4 @@ } else { echo json_encode("error! no data provided in post request"); -} \ No newline at end of file +} diff --git a/fingerprint_service/docker-compose-fingerprint.service b/fingerprint_service/docker-compose-fingerprint.service index 4fb038a..47876e0 100644 --- a/fingerprint_service/docker-compose-fingerprint.service +++ b/fingerprint_service/docker-compose-fingerprint.service @@ -9,8 +9,8 @@ After=docker.service Type=oneshot RemainAfterExit=yes WorkingDirectory=/change/this/to/directory/of/the/compose-file -ExecStart=/usr/local/bin/docker-compose up -d -ExecStop=/usr/local/bin/docker-compose down +ExecStart=/usr/bin/docker compose up -d +ExecStop=/usr/bin/docker compose down TimeoutStartSec=0 [Install] diff --git a/fingerprint_service/docker-compose.yml b/fingerprint_service/docker-compose.yml index bfde142..1b635f6 100644 --- a/fingerprint_service/docker-compose.yml +++ b/fingerprint_service/docker-compose.yml @@ -1,24 +1,37 @@ -version: '3.5' - -services: - fingerprint_engine: - image: dahirmuhammaddahir/grpc_fingerprint_engine - ports: - - 4134:4134 - restart: unless-stopped - container_name: fingerprint_engine +services: + fingerprint_engine: + image: ${FINGERPRINT_ENGINE_IMAGE:-fingerprint-engine:local} + build: + context: thirdparty/grpc-fingerprint-engine + platform: ${DOCKER_PLATFORM:-linux/amd64} + ports: + - "127.0.0.1:${FINGERPRINT_ENGINE_PORT:-4134}:4134" + restart: unless-stopped + container_name: fingerprint_engine + healthcheck: + test: ["CMD-SHELL", "timeout 2 bash -c ' $ok, + 'grpc_extension' => $grpcLoaded, + 'engine_reachable' => $engineReachable, +]); diff --git a/fingerprint_service/thirdparty/grpc-fingerprint-engine/Dockerfile b/fingerprint_service/thirdparty/grpc-fingerprint-engine/Dockerfile index 6d28f25..ca27e89 100644 --- a/fingerprint_service/thirdparty/grpc-fingerprint-engine/Dockerfile +++ b/fingerprint_service/thirdparty/grpc-fingerprint-engine/Dockerfile @@ -17,9 +17,10 @@ RUN apt update && apt install -y wget && \ rm libdpfj.tar && \ apt purge -y --auto-remove wget -WORKDIR /app -COPY src/ ./src -RUN cd src/cpp/build && cmake ../ && make -j +WORKDIR /app +COPY src/ ./src +RUN cmake -S src/cpp -B src/cpp/build && \ + cmake --build src/cpp/build --parallel FROM ubuntu:20.04 diff --git a/fingerprint_service/thirdparty/grpc-fingerprint-engine/src/cpp/fingerprint_server.cc b/fingerprint_service/thirdparty/grpc-fingerprint-engine/src/cpp/fingerprint_server.cc index 166d3e8..00249c6 100644 --- a/fingerprint_service/thirdparty/grpc-fingerprint-engine/src/cpp/fingerprint_server.cc +++ b/fingerprint_service/thirdparty/grpc-fingerprint-engine/src/cpp/fingerprint_server.cc @@ -4,13 +4,13 @@ * Author : Dahir Muhammad Dahir (dahirmuhammad3@gmail.com) * About : Compile with g++ */ - + #include #include #include #include #include - + // DigitalPersona Includes #include "dpfj.h" @@ -167,18 +167,23 @@ class FingerPrintImpl final : public FingerPrint::Service { string target_pre_fmd = base64_decode(verification_request->targetfmd().base64preenrolledfmd()); MyFMD* target_pre_fmd_ptr = (MyFMD* ) target_pre_fmd.c_str(); - map target_verify_fmd_map = convert_fmd(DPFJ_FMD_DP_PRE_REG_FEATURES, DPFJ_FMD_DP_VER_FEATURES, target_pre_fmd_ptr, (MyFMDSize ) target_pre_fmd.size()); - - map::iterator target_verify_fmd_map_it = target_verify_fmd_map.begin(); - - MyFMD* target_verify_fmd_ptr = target_verify_fmd_map_it->first; - MyFMDSize* target_verify_fmd_size = target_verify_fmd_map_it->second; + vector target_verify_fmd = convert_fmd(DPFJ_FMD_DP_PRE_REG_FEATURES, DPFJ_FMD_DP_VER_FEATURES, target_pre_fmd_ptr, (MyFMDSize ) target_pre_fmd.size()); + if (target_verify_fmd.empty()) { + VerificationResponse* conversion_failed = new VerificationResponse(); + conversion_failed->set_match(false); + return conversion_failed; + } - int candidates_count = verification_request->fmdcandidates_size(); + int candidates_count = verification_request->fmdcandidates_size(); + if (candidates_count == 0) { + VerificationResponse* no_candidates = new VerificationResponse(); + no_candidates->set_match(false); + return no_candidates; + } - string candidates_container[candidates_count]; - MyFMD* candidates_pointers_container[candidates_count]; - MyFMDSize candidates_size_container[candidates_count]; + vector candidates_container(candidates_count); + vector candidates_pointers_container(candidates_count); + vector candidates_size_container(candidates_count); DPFJ_CANDIDATE matched_candidates_container[1]; MyFMDSize expected_matches_count = 1; @@ -195,7 +200,7 @@ class FingerPrintImpl final : public FingerPrint::Service { } - int identify_result = dpfj_identify(DPFJ_FMD_DP_VER_FEATURES, target_verify_fmd_ptr, *target_verify_fmd_size, 0, DPFJ_FMD_DP_REG_FEATURES, candidates_count, candidates_pointers_container, candidates_size_container, threshold_score, &expected_matches_count, matched_candidates_container); + int identify_result = dpfj_identify(DPFJ_FMD_DP_VER_FEATURES, target_verify_fmd.data(), target_verify_fmd.size(), 0, DPFJ_FMD_DP_REG_FEATURES, candidates_count, candidates_pointers_container.data(), candidates_size_container.data(), threshold_score, &expected_matches_count, matched_candidates_container); VerificationResponse* verification_response = new VerificationResponse(); verification_response->set_match(false); @@ -225,24 +230,20 @@ class FingerPrintImpl final : public FingerPrint::Service { } - map convert_fmd(DPFJ_FMD_FORMAT input_fmd_formt, DPFJ_FMD_FORMAT output_fmd_format, MyFMD* input_fmd, MyFMDSize input_fmd_size) { - - MyFMD output_fmd[MY_MAX_FMD_SIZE]; // will hold the output fmd - MyFMDSize output_fmd_size; // will hold the original size of the output fmd - - int conversion_result = dpfj_fmd_convert(input_fmd_formt, input_fmd, input_fmd_size, output_fmd_format, output_fmd, &output_fmd_size); - - map converted_fmd_result; + vector convert_fmd(DPFJ_FMD_FORMAT input_fmd_formt, DPFJ_FMD_FORMAT output_fmd_format, MyFMD* input_fmd, MyFMDSize input_fmd_size) { + vector output_fmd(MY_MAX_FMD_SIZE); + MyFMDSize output_fmd_size = output_fmd.size(); + + int conversion_result = dpfj_fmd_convert(input_fmd_formt, input_fmd, input_fmd_size, output_fmd_format, output_fmd.data(), &output_fmd_size); if (conversion_result == DPFJ_SUCCESS) { cout << "FMD coverted successfully..." << endl; - converted_fmd_result[output_fmd] = &output_fmd_size; - - return converted_fmd_result; + output_fmd.resize(output_fmd_size); + return output_fmd; } cout << "Unknown error..." << endl; - return converted_fmd_result; + return {}; } }; @@ -272,4 +273,4 @@ void RunServer() { int main() { RunServer(); -} \ No newline at end of file +} diff --git a/scripts/windows-preflight.ps1 b/scripts/windows-preflight.ps1 new file mode 100644 index 0000000..6c14c7a --- /dev/null +++ b/scripts/windows-preflight.ps1 @@ -0,0 +1,35 @@ +$ErrorActionPreference = "Continue" + +function Report($Name, $Ok, $Details) { + $status = if ($Ok) { "OK" } else { "FAIL" } + $color = if ($Ok) { "Green" } else { "Red" } + Write-Host ("[{0}] {1}: {2}" -f $status, $Name, $Details) -ForegroundColor $color +} + +$windows = Get-CimInstance Win32_OperatingSystem +Report "Windows" ([int]$windows.BuildNumber -ge 19045) ("{0} build {1}" -f $windows.Caption, $windows.BuildNumber) + +$virtualization = Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty VirtualizationFirmwareEnabled +Report "Firmware virtualization" ([bool]$virtualization) (if ($virtualization) { "enabled" } else { "enable it in BIOS/UEFI" }) + +$wsl = & wsl.exe --version 2>&1 +Report "WSL" ($LASTEXITCODE -eq 0) ($wsl -join " ") + +$dockerVersion = & docker version --format '{{.Server.Version}}' 2>&1 +Report "Docker engine" ($LASTEXITCODE -eq 0) ($dockerVersion -join " ") + +$composeVersion = & docker compose version 2>&1 +Report "Docker Compose" ($LASTEXITCODE -eq 0) ($composeVersion -join " ") + +$reader = Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue | + Where-Object { $_.FriendlyName -match 'U\.are\.U|DigitalPersona|Fingerprint' } +Report "Fingerprint reader" ($null -ne $reader) (($reader | Select-Object -ExpandProperty FriendlyName) -join ", ") + +try { + $service = Invoke-WebRequest -UseBasicParsing -TimeoutSec 3 http://127.0.0.1:5555/health.php + Report "Fingerprint service" ($service.StatusCode -eq 200) $service.Content +} catch { + Report "Fingerprint service" $false $_.Exception.Message +} + +Write-Host "Open the application diagnostics endpoint next: /src/core/diagnostics.php" diff --git a/src/core/Database.php b/src/core/Database.php index 17f31f7..99f6b58 100644 --- a/src/core/Database.php +++ b/src/core/Database.php @@ -9,24 +9,43 @@ namespace fingerprint; use mysqli; +use RuntimeException; + +require_once(__DIR__ . '/../../configs.php'); date_default_timezone_set("Africa/Lagos"); class Database { - private const host = "localhost"; - private const user = "root"; - private const password = ""; - private const database = "fingerprint"; private $connection; function __construct(){ - $this->connection = new mysqli(self::host, self::user, self::password, self::database); - if (mysqli_connect_errno()) { - printf("Connection Failed: %s\n", mysqli_connect_errno()); - exit(); + if (!extension_loaded('mysqli')) { + throw new RuntimeException('The mysqli PHP extension is required.'); + } + + mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); + try { + $this->connection = new mysqli( + \fingerprint_config('DB_HOST', '127.0.0.1'), + \fingerprint_config('DB_USER', 'root'), + \fingerprint_config('DB_PASSWORD', ''), + \fingerprint_config('DB_NAME', 'fingerprint'), + (int) \fingerprint_config('DB_PORT', '3306') + ); + $this->connection->set_charset('utf8mb4'); + } catch (\mysqli_sql_exception $exception) { + throw new RuntimeException( + 'Unable to connect to the configured MySQL database.', + 0, + $exception + ); } } + function ping(): bool { + return $this->connection->ping(); + } + function getUserInfo($username): array { $sql_query = "SELECT * FROM users WHERE username=?"; $param_type = "s"; diff --git a/src/core/diagnostics.php b/src/core/diagnostics.php new file mode 100644 index 0000000..9777a7b --- /dev/null +++ b/src/core/diagnostics.php @@ -0,0 +1,79 @@ + false, 'error' => 'Diagnostics are disabled.']); + exit; +} + +$checks = []; +$checks['php'] = [ + 'ok' => version_compare(PHP_VERSION, '7.4.0', '>='), + 'version' => PHP_VERSION, + 'message' => version_compare(PHP_VERSION, '7.4.0', '>=') + ? 'PHP runtime is available.' + : 'PHP 7.4 or newer is required; a currently supported PHP release is recommended.', +]; + +$checks['mysqli'] = [ + 'ok' => extension_loaded('mysqli'), + 'message' => extension_loaded('mysqli') + ? 'mysqli is enabled.' + : 'Enable the mysqli extension in php.ini and restart Apache.', +]; + +$checks['database'] = ['ok' => false, 'message' => 'Database check was not run.']; +if ($checks['mysqli']['ok']) { + try { + require_once(__DIR__ . '/Database.php'); + $database = new \fingerprint\Database(); + $checks['database'] = [ + 'ok' => $database->ping(), + 'message' => 'MySQL connection succeeded.', + ]; + } catch (Throwable $exception) { + $checks['database'] = [ + 'ok' => false, + 'message' => 'MySQL connection failed. Check DB_HOST, DB_PORT, DB_NAME, DB_USER and DB_PASSWORD.', + ]; + } +} + +$serviceUrl = fingerprint_config('FP_CLIENT_SERVICE_HOST', 'http://127.0.0.1:5555'); +$parts = parse_url($serviceUrl); +$serviceHost = $parts['host'] ?? null; +$servicePort = $parts['port'] ?? (($parts['scheme'] ?? 'http') === 'https' ? 443 : 80); +$serviceOk = false; + +if ($serviceHost) { + $socket = @fsockopen($serviceHost, $servicePort, $errorNumber, $errorMessage, 3); + if (is_resource($socket)) { + $serviceOk = true; + fclose($socket); + } +} + +$checks['fingerprint_service'] = [ + 'ok' => $serviceOk, + 'target' => sprintf('%s:%d', $serviceHost ?: 'invalid-host', $servicePort), + 'message' => $serviceOk + ? 'Fingerprint service port is reachable.' + : 'Fingerprint service is unreachable. Check FP_CLIENT_SERVICE_HOST and Docker/service status.', +]; + +$allOk = !in_array(false, array_map(function ($check) { + return $check['ok']; +}, $checks), true); + +http_response_code($allOk ? 200 : 503); +echo json_encode([ + 'ok' => $allOk, + 'checks' => $checks, +], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); diff --git a/src/core/enroll.php b/src/core/enroll.php index 64e1aca..7d2d7bd 100644 --- a/src/core/enroll.php +++ b/src/core/enroll.php @@ -9,8 +9,8 @@ namespace fingerprint; -require("../core/querydb.php"); -require_once("../core/helpers/helpers.php"); +require(__DIR__ . "/querydb.php"); +require_once(__DIR__ . "/helpers/helpers.php"); if(!empty($_POST["data"])){ diff --git a/src/core/helpers/helpers.php b/src/core/helpers/helpers.php index 4f715cb..6a09fd6 100644 --- a/src/core/helpers/helpers.php +++ b/src/core/helpers/helpers.php @@ -10,6 +10,17 @@ require_once('Requests/src/Autoload.php'); WpOrg\Requests\Autoload::register(); +set_exception_handler(function (Throwable $exception) { + if (!headers_sent()) { + http_response_code(503); + header('Content-Type: application/json; charset=utf-8'); + } + echo json_encode([ + 'ok' => false, + 'error' => $exception->getMessage(), + ]); +}); + /** * $pre_registered_fmd_array has the format * [ "index_finger" => [array of 4 pre_reg fmd of index_finger], @@ -21,7 +32,7 @@ * otherwise returns 'enrollment failed' as string */ function enroll_fingerprint($pre_registered_fmd_array){ - $host = getenv('FP_CLIENT_SERVICE_HOST'); + $host = rtrim(fingerprint_config('FP_CLIENT_SERVICE_HOST', 'http://127.0.0.1:5555'), '/'); $enrollment_url = "$host/coreComponents/enroll.php"; $data = ["data" => json_encode($pre_registered_fmd_array)]; @@ -45,7 +56,7 @@ function enroll_fingerprint($pre_registered_fmd_array){ * storage you've stored the the enrollment fmd */ function verify_fingerprint($pre_registered_fmd_string, $enrolled_fingers_array){ - $host = getenv('FP_CLIENT_SERVICE_HOST'); + $host = rtrim(fingerprint_config('FP_CLIENT_SERVICE_HOST', 'http://127.0.0.1:5555'), '/'); $verify_url = "$host/coreComponents/verify.php"; $data = [ @@ -85,7 +96,7 @@ function verify_fingerprint($pre_registered_fmd_string, $enrolled_fingers_array) * the function returns true if is there is duplicate and false otherwise */ function is_duplicate_fingerprint($pre_registered_fmd_string, $enrolled_hands_array){ - $host = getenv('FP_CLIENT_SERVICE_HOST'); + $host = rtrim(fingerprint_config('FP_CLIENT_SERVICE_HOST', 'http://127.0.0.1:5555'), '/'); $is_duplicate_url = "$host/coreComponents/is_duplicate.php"; $data = [ @@ -106,7 +117,23 @@ function make_request($url, $data){ $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); - $response = WpOrg\Requests\Requests::post($url, $headers, $data); + try { + $response = WpOrg\Requests\Requests::post($url, $headers, $data, [ + 'timeout' => (int) fingerprint_config('FP_CLIENT_TIMEOUT', '15'), + ]); + } catch (Exception $exception) { + throw new RuntimeException( + 'Fingerprint service is unavailable at the configured address.', + 0, + $exception + ); + } + + if ($response->status_code < 200 || $response->status_code >= 300) { + throw new RuntimeException( + sprintf('Fingerprint service returned HTTP %d.', $response->status_code) + ); + } return $response->body; } @@ -177,4 +204,4 @@ function test_duplicate(){ // to test uncomment this lines // test_enroll(); // test_verify(); -//test_duplicate(); \ No newline at end of file +//test_duplicate(); diff --git a/src/core/querydb.php b/src/core/querydb.php index 73abbe8..8df20c9 100644 --- a/src/core/querydb.php +++ b/src/core/querydb.php @@ -7,7 +7,7 @@ */ namespace fingerprint; -require_once("../core/Database.php"); +require_once(__DIR__ . "/Database.php"); function setUserFmds($user_id, $index_finger_fmd_string, $middle_finger_fmd_string){ $myDatabase = new Database(); @@ -44,7 +44,7 @@ function getUserDetails($user_id){ function getAllFmds(){ $myDatabase = new Database(); - $sql_query = "select indexfinger, middlefinger from users where indexfinger != ''"; + $sql_query = "select indexfinger, middlefinger from users where indexfinger is not null and indexfinger != ''"; $allFmds = $myDatabase->select($sql_query); return json_encode($allFmds); -} \ No newline at end of file +} diff --git a/src/core/verify.php b/src/core/verify.php index e19489c..7145a89 100644 --- a/src/core/verify.php +++ b/src/core/verify.php @@ -8,8 +8,8 @@ namespace fingerprint; -require_once("../core/helpers/helpers.php"); -require_once("../core/querydb.php"); +require_once(__DIR__ . "/helpers/helpers.php"); +require_once(__DIR__ . "/querydb.php"); if(!empty($_POST["data"])) { $user_data = json_decode($_POST["data"]); diff --git a/src/html/home.html b/src/html/home.html index 59f3ff7..f4d7fc2 100644 --- a/src/html/home.html +++ b/src/html/home.html @@ -2,13 +2,19 @@ + + - - + + FingerPrint Web App @@ -16,12 +22,12 @@ - - - - - - + + + + + +
@@ -196,4 +202,4 @@
- \ No newline at end of file + diff --git a/src/js/custom.js b/src/js/custom.js index dafe3cc..230d6a0 100644 --- a/src/js/custom.js +++ b/src/js/custom.js @@ -15,6 +15,12 @@ let FingerprintSdkTest = (function () { this.operationToRestart = null; this.acquisitionStarted = false; // instantiating the fingerprint sdk here + this.sdk = null; + if (typeof Fingerprint === "undefined" || typeof Fingerprint.WebApi !== "function") { + this.initializationError = "DigitalPersona Web SDK is unavailable. Install/start the Windows Web SDK runtime and reload the page."; + return; + } + this.initializationError = null; this.sdk = new Fingerprint.WebApi; this.sdk.onDeviceConnected = function (e) { // Detects if the device is connected for which acquisition started @@ -40,6 +46,10 @@ let FingerprintSdkTest = (function () { // this is were finger print capture takes place FingerprintSdkTest.prototype.startCapture = function () { + if (!this.sdk) { + showMessage(this.initializationError); + return; + } if (this.acquisitionStarted) // Monitoring if already started capturing return; let _instance = this; @@ -57,6 +67,8 @@ let FingerprintSdkTest = (function () { }; FingerprintSdkTest.prototype.stopCapture = function () { + if (!this.sdk) + return; if (!this.acquisitionStarted) //Monitor if already stopped capturing return; let _instance = this; @@ -73,12 +85,16 @@ let FingerprintSdkTest = (function () { }; FingerprintSdkTest.prototype.getInfo = function () { - let _instance = this; + if (!this.sdk) { + showMessage(this.initializationError); + return Promise.resolve([]); + } return this.sdk.enumerateDevices(); }; FingerprintSdkTest.prototype.getDeviceInfoWithID = function (uid) { - let _instance = this; + if (!this.sdk) + return Promise.reject(new Error(this.initializationError)); return this.sdk.getDeviceInfo(uid); }; @@ -118,9 +134,11 @@ class Reader{ } } else{ - showMessage("Please Connect the Fingerprint Reader"); + showMessage(myReader.reader.initializationError || "No fingerprint reader was detected. Check the reader, driver, and DigitalPersona Web SDK service."); } - }) + }, function(error) { + showMessage(`Unable to enumerate fingerprint readers: ${error.message || error}`); + }); } } @@ -171,7 +189,11 @@ function showMessage(message, message_type="error"){ let statusFieldID = myReader.currentStatusField; if(statusFieldID){ let statusField = document.getElementById(statusFieldID); - statusField.innerHTML = `

${message}

`; + statusField.innerHTML = ""; + let paragraph = document.createElement("p"); + paragraph.className = `my-text7 my-pri-color my-3 ${types.get(message_type) || types.get("error")} font-weight-bold`; + paragraph.textContent = message; + statusField.appendChild(paragraph); } } @@ -375,17 +397,20 @@ function serverEnroll(){ let xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function(){ - if(this.readyState === 4 && this.status === 200){ - if(this.responseText === "success"){ + if(this.readyState === 4){ + if(this.status === 200 && this.responseText === "success"){ showMessage(successMessage, "success"); } else{ - showMessage(`${failedMessage} ${this.responseText}`); + showMessage(`${failedMessage} ${readServerError(this.responseText, this.status)}`); } } }; + xhttp.onerror = function() { + showMessage(`${failedMessage} Network request failed.`); + }; - xhttp.open("POST", "/src/core/enroll.php", true); + xhttp.open("POST", new URL("src/core/enroll.php", document.baseURI), true); xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhttp.send(payload); } @@ -423,9 +448,26 @@ function serverIdentify() { } } } + else if (this.readyState === 4) { + showMessage(`${failedMessage} ${readServerError(this.responseText, this.status)}`); + } + }; + xhttp.onerror = function() { + showMessage(`${failedMessage} Network request failed.`); }; - xhttp.open("POST", "/src/core/verify.php", true); + xhttp.open("POST", new URL("src/core/verify.php", document.baseURI), true); xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhttp.send(payload); } + +function readServerError(responseText, status) { + try { + let response = JSON.parse(responseText); + if (response && response.error) + return response.error; + } catch (error) { + // The legacy API uses plain-text responses. + } + return responseText || `Server returned HTTP ${status || 0}.`; +}