netgescon-day0/app/Console/Commands/PanasonicCstaBridgeCommand.php

196 lines
7.4 KiB
PHP
Executable File

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class PanasonicCstaBridgeCommand extends Command
{
protected $signature = 'cti:bridge-panasonic
{--host=192.168.0.101 : Panasonic host/IP}
{--port=33333 : Panasonic CSTA TCP port}
{--timeout=10 : Connect and read timeout in seconds}
{--read-bytes=2048 : Bytes to read per frame/chunk}
{--reconnect=1 : Auto reconnect when socket closes}
{--reconnect-delay=5 : Seconds before reconnect}
{--max-idle-timeouts=30 : Reconnect after N consecutive read timeouts (0 = never)}
{--startup-send= : Optional ASCII payload sent right after connect}
{--startup-send-hex= : Optional HEX payload sent right after connect}
{--log-file=storage/logs/cti-panasonic-raw.ndjson : NDJSON file where raw frames are appended}
{--print-hex=1 : Print each frame as hex in console output}
{--print-text=1 : Print best-effort text rendering in console output}';
protected $description = 'Minimal Panasonic CSTA bridge daemon: connects to the PBX, captures raw frames and keeps the socket alive for protocol analysis.';
public function handle(): int
{
$host = (string) $this->option('host');
$port = max(1, (int) $this->option('port'));
$timeout = max(1, (int) $this->option('timeout'));
$readBytes = max(64, min(65535, (int) $this->option('read-bytes')));
$reconnect = (bool) $this->option('reconnect');
$reconnectDelay = max(1, (int) $this->option('reconnect-delay'));
$maxIdleTimeouts = max(0, (int) $this->option('max-idle-timeouts'));
$startupSend = (string) ($this->option('startup-send') ?? '');
$startupSendHex = strtoupper(preg_replace('/[^0-9A-Fa-f]/', '', (string) ($this->option('startup-send-hex') ?? '')) ?? '');
$logFile = (string) $this->option('log-file');
$printHex = (bool) $this->option('print-hex');
$printText = (bool) $this->option('print-text');
if ($startupSend !== '' && $startupSendHex !== '') {
$this->error('Usa solo uno tra --startup-send e --startup-send-hex.');
return self::FAILURE;
}
if ($startupSendHex !== '' && strlen($startupSendHex) % 2 !== 0) {
$this->error('Il payload HEX deve avere un numero pari di caratteri.');
return self::FAILURE;
}
$startupPayload = $startupSendHex !== '' ? (hex2bin($startupSendHex) ?: ''): $startupSend;
$absoluteLogFile = $this->normalizeLogFilePath($logFile);
$this->info("Bridge Panasonic CSTA verso {$host}:{$port}");
$this->line('Log raw frames: ' . $absoluteLogFile);
do {
$socket = @stream_socket_client("tcp://{$host}:{$port}", $errno, $errstr, $timeout);
if (! is_resource($socket)) {
$this->error("Connessione fallita ({$errno}): {$errstr}");
if (! $reconnect) {
return self::FAILURE;
}
sleep($reconnectDelay);
continue;
}
stream_set_timeout($socket, $timeout);
stream_set_blocking($socket, true);
$this->info('Connessione CSTA stabilita.');
$this->appendFrameLog($absoluteLogFile, [
'type' => 'connection.opened',
'host' => $host,
'port' => $port,
'ts' => now()->toIso8601String(),
]);
if ($startupPayload !== '') {
@fwrite($socket, $startupPayload);
$this->appendFrameLog($absoluteLogFile, [
'type' => 'connection.startup_payload',
'host' => $host,
'port' => $port,
'ts' => now()->toIso8601String(),
'payload_hex' => strtoupper(bin2hex($startupPayload)),
'payload_text' => $this->renderPrintable($startupPayload),
]);
}
$idleTimeouts = 0;
while (! feof($socket)) {
$chunk = fread($socket, $readBytes);
$chunk = is_string($chunk) ? $chunk : '';
if ($chunk === '') {
$meta = stream_get_meta_data($socket);
if (($meta['timed_out'] ?? false) === true) {
$idleTimeouts++;
$this->warn("Timeout lettura CSTA ({$idleTimeouts})");
if ($maxIdleTimeouts > 0 && $idleTimeouts >= $maxIdleTimeouts) {
$this->warn('Soglia timeout raggiunta, riconnessione in corso.');
break;
}
continue;
}
break;
}
$idleTimeouts = 0;
$hex = strtoupper(bin2hex($chunk));
$text = $this->renderPrintable($chunk);
$record = [
'type' => 'frame.received',
'host' => $host,
'port' => $port,
'ts' => now()->toIso8601String(),
'size' => strlen($chunk),
'payload_hex' => $hex,
'payload_text' => $text,
];
$this->appendFrameLog($absoluteLogFile, $record);
if ($printHex) {
$this->line('HEX ' . implode(' ', str_split($hex, 2)));
}
if ($printText) {
$this->line('TEXT ' . ($text !== '' ? $text : '[binary/non-printable]'));
}
}
fclose($socket);
$this->appendFrameLog($absoluteLogFile, [
'type' => 'connection.closed',
'host' => $host,
'port' => $port,
'ts' => now()->toIso8601String(),
]);
$this->warn('Socket CSTA chiusa.');
if (! $reconnect) {
return self::SUCCESS;
}
sleep($reconnectDelay);
} while (true);
}
private function normalizeLogFilePath(string $path): string
{
if (str_starts_with($path, '/')) {
return $path;
}
return base_path($path);
}
/**
* @param array<string,mixed> $payload
*/
private function appendFrameLog(string $logFile, array $payload): void
{
$dir = dirname($logFile);
if (! is_dir($dir)) {
@mkdir($dir, 0775, true);
}
$json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if (! is_string($json) || $json === '') {
return;
}
@file_put_contents($logFile, $json . PHP_EOL, FILE_APPEND);
Log::info('Panasonic CSTA bridge', [
'event' => $payload['type'] ?? 'unknown',
'host' => $payload['host'] ?? null,
'port' => $payload['port'] ?? null,
'size' => $payload['size'] ?? null,
]);
}
private function renderPrintable(string $payload): string
{
$printable = preg_replace('/[^\P{C}\n\r\t]/u', '.', $payload);
return is_string($printable) ? trim($printable) : '';
}
}