Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,63 @@
# 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:
- `username`
- `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"}'
```
128 changes: 121 additions & 7 deletions app/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -185,6 +179,103 @@ 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 (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("ERROR: 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);
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);

// 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
Expand Down Expand Up @@ -276,6 +367,28 @@ function handle($data)
$busylamps[] = '<uri>' . $busylamp . '</uri>';
}

// 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 = "".
"<genericSmsFetchUrl>" . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/fetch_messages" . "</genericSmsFetchUrl>\n".
"<genericSmsFetchPostData>{$fetchPostData}</genericSmsFetchPostData>\n".
"<genericSmsFetchContentType>application/json</genericSmsFetchContentType>\n".
"<genericSmsSendUrl>" . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/send_message" . "</genericSmsSendUrl>\n".
"<genericSmsSendPostData>{$sendPostData}</genericSmsSendPostData>\n".
"<genericSmsSendContentType>application/json</genericSmsSendContentType>\n".
"<pushTokenReporterUrl>" . $chatResponse["body"]["matrix"]["acrobits_url"] . "/api/client/push_token_report" . "</pushTokenReporterUrl>\n".
"<pushTokenReporterPostData>{$pushTokenReporterPostData}</pushTokenReporterPostData>\n".
"<pushTokenReporterContentType>application/json</pushTokenReporterContentType>\n";
} else {
$chat = "";
debug("No chat configuration found for {$cloudUsername}@{$cloudDomain}", $cloudDomain);
}

// set headers
header("Content-type: text/xml");

Expand All @@ -300,6 +413,7 @@ function handle($data)
<host>{$cloudDomain}</host>
<transport>tls+sip:</transport>
<blf>" . implode("", $busylamps) . "</blf>
$chat
</account>
";

Expand Down