From 2b00e4b8afe860b58c934c836cc99ae28c4976e5 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 17 Dec 2025 14:12:01 +0100 Subject: [PATCH 1/4] feat: add chat provisioning --- app/index.php | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/app/index.php b/app/index.php index 3d66504..f74eab0 100644 --- a/app/index.php +++ b/app/index.php @@ -185,6 +185,106 @@ function getSipCredentials($cloudUsername, $cloudPassword, $cloudDomain, $isToke return false; } +// helper to perform POST JSON requests and return decoded JSON or false +function postJsonRequest($url, $payload = [], $headers = []) +{ + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); + + $defaultHeaders = ["Content-Type: application/json"]; + $allHeaders = array_merge($defaultHeaders, $headers); + curl_setopt($ch, CURLOPT_HTTPHEADER, $allHeaders); + + $response = curl_exec($ch); + + if ($response === false) { + error_log("ERROR: cURL POST failed: " . curl_error($ch) . " for URL: $url"); + curl_close($ch); + return false; + } + + curl_close($ch); + + $json = json_decode($response, true); + if ($json === null && json_last_error() !== JSON_ERROR_NONE) { + error_log("ERROR: Failed to decode JSON POST response: " . json_last_error_msg()); + error_log("Response: " . $response); + return false; + } + + return $json; +} + +// makeCTIRequest: login to CTI API to obtain JWT then run the actual request with Bearer token +// $cloudDomain: e.g. "cti.gs.nethserver.net" +// $username, $password: credentials for login +// $method: HTTP method for actual request (GET, POST,...) +// $path: path for actual request (e.g. '/api/chat') +// $body: optional payload for actual request (array) +function makeCTIRequest($cloudDomain, $username, $password, $method, $path, $body = null) +{ + $cloudBaseUrl = "https://$cloudDomain"; + + // Step 1: login and get token + $loginUrl = rtrim($cloudBaseUrl, '/') . '/api/login'; + debug("Logging in to $loginUrl as $username", $cloudDomain); + + $loginPayload = [ + 'username' => $username, + 'password' => $password + ]; + + $loginResp = postJsonRequest($loginUrl, $loginPayload); + if ($loginResp === false || !isset($loginResp['token'])) { + error_log("ERROR: Failed to login to CTI at $loginUrl"); + return [ 'code' => -1, 'body' => null ]; + } + + $token = $loginResp['token']; + debug("Received JWT for $username", $cloudDomain); + + // Step 2: perform actual request using Bearer token + $requestUrl = rtrim($cloudBaseUrl, '/') . '/' . ltrim($path, '/'); + $headers = ["Authorization: Bearer $token", 'Accept: application/json', 'Content-Type: application/json']; + + $ch = curl_init($requestUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + if ($body !== null) { + $payload = json_encode($body); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + } + + $response = curl_exec($ch); + + if ($response === false) { + $err = curl_error($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + error_log("ERROR: cURL CTI request failed: $err (HTTP $httpCode) for URL: $requestUrl"); + return [ 'code' => -1, 'body' => null ]; + } + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + // decode JSON if possible + $json = json_decode($response, true); + if ($json === null && json_last_error() !== JSON_ERROR_NONE) { + // return raw response if not JSON + debug("CTI request returned non-JSON or empty response (HTTP $httpCode)", $cloudDomain); + return ['code' => $httpCode, 'raw' => $response]; + } + + return ['code' => $httpCode, 'body' => $json]; +} + function handle($data) { // check if username, password and token are set @@ -276,6 +376,29 @@ function handle($data) $busylamps[] = '' . $busylamp . ''; } + // get chat info + $chatResponse = makeCTIRequest($cloudDomain, $cloudUsername, $cloudPassword, "GET", "/api/chat"); + debug("Chat API response: " . $chatResponse["code"], $cloudDomain); + if ($chatResponse && $chatResponse["code"] === 200 && isset($chatResponse["body"]["matrix"]["acrobits_url"])) { + $fetchPostData = '{ "username" : "%account[cloud_username]% ", "password" : "%account[cloud_password]%", "last_id" : "%last_known_sms_id%", "last_sent_id" : "%last_known_sent_sms_id%", "device" : "%installid%" }'; + $sendPostData = '{ "from" : "%account[cloud_username]%", "password" : "%account[cloud_password]%", "to" : "%sms_to%", "body" : "%sms_body%", "content_type" : "%content_type%" }'; + $pushTokenReporterPostData = '{ "username" : "%account[cloud_username]%", "password" : "%account[cloud_password]%", "token_calls" : "%pushTokenIncomingCall%", "token_msgs" : "%pushTokenOther%", "selector" : "%selector%", "appId_calls": "%pushappid_incoming_call%", "appId_msgs" : "%pushappid_other%" }'; + $chat = " + " . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/fetch_messages" . " + {$fetchPostData} + application/json + " . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/send_message" . " + {$sendPostData} + application/json + " . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/push_token_report" . " + {$pushTokenReporterPostData} + application/json + "; + } else { + $chat = ""; + debug("No chat configuration found for {$cloudUsername}@{$cloudDomain}", $cloudDomain); + } + // set headers header("Content-type: text/xml"); @@ -300,6 +423,7 @@ function handle($data) {$cloudDomain} tls+sip: " . implode("", $busylamps) . " + $chat "; From c759d574c3be7fd536702762c95689e2957eefc4 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 18 Dec 2025 10:49:20 +0100 Subject: [PATCH 2/4] fix: remove curl_close The function is deprecated. --- app/index.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/index.php b/app/index.php index f74eab0..25eba27 100644 --- a/app/index.php +++ b/app/index.php @@ -35,13 +35,10 @@ function makeRequest($username, $token, $url) if (curl_error($ch)) { $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); error_log("ERROR: cURL failed: $error (HTTP $httpCode) for URL: $url"); return false; } - curl_close($ch); - // read response $jsonResponse = json_decode($response, true); @@ -77,14 +74,12 @@ function getAuthToken($cloudUsername, $cloudPassword, $cloudDomain) // Add error handling for curl execution if ($response === false) { error_log("ERROR: cURL error during authentication: " . curl_error($ch)); - curl_close($ch); return false; } $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // close curl and read response - curl_close($ch); if ($httpCode !== 401) { error_log("ERROR: Authentication failed for {$cloudUsername}@{$cloudDomain}. Expected HTTP code 401, got $httpCode"); return false; @@ -152,7 +147,6 @@ function getSipCredentials($cloudUsername, $cloudPassword, $cloudDomain, $isToke // exec curl $lkcheck = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); // print debug debug("lkhash validated for {$cloudUsername}@{$cloudDomain}", $cloudDomain); @@ -202,12 +196,9 @@ function postJsonRequest($url, $payload = [], $headers = []) if ($response === false) { error_log("ERROR: cURL POST failed: " . curl_error($ch) . " for URL: $url"); - curl_close($ch); return false; } - curl_close($ch); - $json = json_decode($response, true); if ($json === null && json_last_error() !== JSON_ERROR_NONE) { error_log("ERROR: Failed to decode JSON POST response: " . json_last_error_msg()); @@ -266,13 +257,11 @@ function makeCTIRequest($cloudDomain, $username, $password, $method, $path, $bod if ($response === false) { $err = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); error_log("ERROR: cURL CTI request failed: $err (HTTP $httpCode) for URL: $requestUrl"); return [ 'code' => -1, 'body' => null ]; } $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); // decode JSON if possible $json = json_decode($response, true); From a0d65c1e8961836ab22e29fa20d9568287a93de1 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 18 Dec 2025 15:13:35 +0100 Subject: [PATCH 3/4] fix: better response if /chat endpoint is missing Allow to put the service in production even if destination server does still not have the chat endpoint --- app/index.php | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/app/index.php b/app/index.php index 25eba27..c363179 100644 --- a/app/index.php +++ b/app/index.php @@ -38,7 +38,7 @@ function makeRequest($username, $token, $url) error_log("ERROR: cURL failed: $error (HTTP $httpCode) for URL: $url"); return false; } - + // read response $jsonResponse = json_decode($response, true); @@ -194,15 +194,17 @@ function postJsonRequest($url, $payload = [], $headers = []) $response = curl_exec($ch); - if ($response === false) { - error_log("ERROR: cURL POST failed: " . curl_error($ch) . " for URL: $url"); + if (curl_error($ch) || curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400 || $response === false) { + $error = curl_error($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + error_log("ERROR: cURL POST request failed: $error (HTTP $httpCode) for URL: $url"); return false; } $json = json_decode($response, true); if ($json === null && json_last_error() !== JSON_ERROR_NONE) { error_log("ERROR: Failed to decode JSON POST response: " . json_last_error_msg()); - error_log("Response: " . $response); + error_log("ERROR: Response: " . $response); return false; } @@ -372,17 +374,16 @@ function handle($data) $fetchPostData = '{ "username" : "%account[cloud_username]% ", "password" : "%account[cloud_password]%", "last_id" : "%last_known_sms_id%", "last_sent_id" : "%last_known_sent_sms_id%", "device" : "%installid%" }'; $sendPostData = '{ "from" : "%account[cloud_username]%", "password" : "%account[cloud_password]%", "to" : "%sms_to%", "body" : "%sms_body%", "content_type" : "%content_type%" }'; $pushTokenReporterPostData = '{ "username" : "%account[cloud_username]%", "password" : "%account[cloud_password]%", "token_calls" : "%pushTokenIncomingCall%", "token_msgs" : "%pushTokenOther%", "selector" : "%selector%", "appId_calls": "%pushappid_incoming_call%", "appId_msgs" : "%pushappid_other%" }'; - $chat = " - " . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/fetch_messages" . " - {$fetchPostData} - application/json - " . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/send_message" . " - {$sendPostData} - application/json - " . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/push_token_report" . " - {$pushTokenReporterPostData} - application/json - "; + $chat = "". + "" . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/fetch_messages" . "\n". + "{$fetchPostData}\n". + "application/json\n". + "" . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/send_message" . "\n". + "{$sendPostData}\n". + "application/json\n". + "" . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/push_token_report" . "\n". + "{$pushTokenReporterPostData}\n". + "application/json\n"; } else { $chat = ""; debug("No chat configuration found for {$cloudUsername}@{$cloudDomain}", $cloudDomain); From 12f59c73a91edc348174f0614aed91c7e93a7493 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 18 Dec 2025 15:13:44 +0100 Subject: [PATCH 4/4] chore: improve doc --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 701cdcd..1aa3b9e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # ctiapp-authproxy Authentication proxy for Acrobits application. +This repository contains a PHP proxy used to authenticate against a CTI cloud and return configuration to clients (SIP credentials, phonebook, quick dials). The main logic is in `app/index.php`. -The application needs a `TOKEN` environment application on startup. +The application needs the following environment variable on startup: +- `TOKEN`: a secret token used to validate incoming requests +- `VALIDATE_LK_URL`: URL to validate user credentials against a remote server +- `DEBUG`: optional, if set to "true" enables debug logging Each request must be a POST request containing a JSON object. The object must have the following fields: @@ -10,7 +14,50 @@ The object must have the following fields: - `password` - `token`: it's a SHA256 hash, it must be the same passed to the application at startup -Example with curl: +## Local testing + +Below are minimal steps and examples to test the application locally without Docker. These examples assume you have PHP 8+ installed and are running them from the repository root. + +1) Set environment variables required by the app (example values): + +```bash +export TOKEN="localtesttoken" +export VALIDATE_LK_URL="https://httpbin.org/status/200" # This is a fake endpoint for testing +export DEBUG="true" +``` + +2) Start a built-in PHP web server to serve the `app` directory: + +```bash +php -S 127.0.0.1:8000 -t app/ +``` + +3) Healthcheck (quick): + +```bash +curl -i http://127.0.0.1:8000/index.php/healthcheck ``` -curl -d '{"username": "myuser@demo.example.com", "password": "mypass", "token": "11223344"}' https://ctiapp-authproxy.example.com + +4) Test the `login` flow (replace with real or mocked endpoints): + +```bash +curl -i -X POST http://127.0.0.1:8000/index.php \ + -H "Content-Type: application/json" \ + -d '{"username":"alice@cti.example.com","password":"secret","token":"localtesttoken","app":"login"}' +``` + +5) Test the `contacts` flow: + +```bash +curl -i -X POST http://127.0.0.1:8000/index.php \ + -H "Content-Type: application/json" \ + -d '{"username":"alice@cti.example.com","password":"secret","token":"localtesttoken","app":"contacts"}' +``` + +6) Test the `quickdial` flow: + +```bash +curl -i -X POST http://127.0.0.1:8000/index.php \ + -H "Content-Type: application/json" \ + -d '{"username":"alice@cti.example.com","password":"secret","token":"localtesttoken","app":"quickdial"}' ```