mirror of
https://github.com/linuxserver/Heimdall.git
synced 2026-08-07 07:16:13 +00:00
Add WebSocket support for TrueNAS JSON-RPC 2.0 API
TrueNAS is deprecating the REST API (api/v2.0/) in version 26.04, requiring migration to JSON-RPC 2.0 over WebSocket. This commit adds: - phrity/websocket dependency for WebSocket communication - TrueNASWebSocketClient helper class that handles: - Connection to ws(s)://host/api/current - Authentication via auth.login_with_api_key - JSON-RPC 2.0 request/response formatting - TLS verification toggle - Proper connection cleanup Refs: #1530
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Phrity\Net\Context;
|
||||||
|
use WebSocket\Client;
|
||||||
|
use WebSocket\ConnectionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TrueNAS JSON-RPC 2.0 WebSocket Client
|
||||||
|
*
|
||||||
|
* Handles WebSocket communication with TrueNAS using the JSON-RPC 2.0 protocol.
|
||||||
|
* Required for TrueNAS 25.04+ as the REST API is deprecated.
|
||||||
|
*
|
||||||
|
* @see https://api.truenas.com/v25.10/jsonrpc.html
|
||||||
|
*/
|
||||||
|
class TrueNASWebSocketClient
|
||||||
|
{
|
||||||
|
private ?Client $client = null;
|
||||||
|
private string $url;
|
||||||
|
private string $apiKey;
|
||||||
|
private bool $ignoreTls;
|
||||||
|
private bool $authenticated = false;
|
||||||
|
private int $requestId = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new TrueNAS WebSocket client instance.
|
||||||
|
*
|
||||||
|
* @param string $baseUrl The base URL of the TrueNAS instance (e.g., https://truenas.local)
|
||||||
|
* @param string $apiKey The API key for authentication
|
||||||
|
* @param bool $ignoreTls Whether to skip TLS certificate verification
|
||||||
|
*/
|
||||||
|
public function __construct(string $baseUrl, string $apiKey, bool $ignoreTls = false)
|
||||||
|
{
|
||||||
|
$baseUrl = rtrim($baseUrl, '/');
|
||||||
|
$scheme = parse_url($baseUrl, PHP_URL_SCHEME);
|
||||||
|
$host = parse_url($baseUrl, PHP_URL_HOST);
|
||||||
|
$port = parse_url($baseUrl, PHP_URL_PORT);
|
||||||
|
|
||||||
|
$wsScheme = ($scheme === 'https') ? 'wss' : 'ws';
|
||||||
|
$portPart = $port ? ':' . $port : '';
|
||||||
|
|
||||||
|
$this->url = "{$wsScheme}://{$host}{$portPart}/api/current";
|
||||||
|
$this->apiKey = $apiKey;
|
||||||
|
$this->ignoreTls = $ignoreTls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the TrueNAS WebSocket API and authenticate.
|
||||||
|
*
|
||||||
|
* @return bool True if connection and authentication succeeded
|
||||||
|
* @throws \Exception If connection or authentication fails
|
||||||
|
*/
|
||||||
|
public function connect(): bool
|
||||||
|
{
|
||||||
|
if ($this->client !== null && $this->authenticated) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build SSL options - always force HTTP/1.1 via ALPN for WebSocket compatibility
|
||||||
|
// TrueNAS nginx defaults to HTTP/2 which doesn't support WebSocket upgrade
|
||||||
|
$sslOptions = [
|
||||||
|
'alpn_protocols' => 'http/1.1',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->ignoreTls) {
|
||||||
|
$sslOptions['verify_peer'] = false;
|
||||||
|
$sslOptions['verify_peer_name'] = false;
|
||||||
|
$sslOptions['allow_self_signed'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context using phrity/net-stream Context class (required by phrity/websocket v3.x)
|
||||||
|
$streamContext = stream_context_create(['ssl' => $sslOptions]);
|
||||||
|
$context = new Context($streamContext);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->client = new Client($this->url);
|
||||||
|
$this->client->setTimeout(15);
|
||||||
|
$this->client->setContext($context);
|
||||||
|
|
||||||
|
$authResult = $this->call('auth.login_with_api_key', [$this->apiKey]);
|
||||||
|
|
||||||
|
if ($authResult === true) {
|
||||||
|
$this->authenticated = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception('Authentication failed: Invalid API key');
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
Log::error('TrueNAS WebSocket connection failed: ' . $e->getMessage());
|
||||||
|
$this->disconnect();
|
||||||
|
throw new \Exception('WebSocket connection failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a JSON-RPC 2.0 call to the TrueNAS API.
|
||||||
|
*
|
||||||
|
* @param string $method The JSON-RPC method name (e.g., 'system.info')
|
||||||
|
* @param array $params Optional parameters for the method
|
||||||
|
* @return mixed The result from the API call
|
||||||
|
* @throws \Exception If the call fails or returns an error
|
||||||
|
*/
|
||||||
|
public function call(string $method, array $params = [])
|
||||||
|
{
|
||||||
|
if ($this->client === null) {
|
||||||
|
throw new \Exception('WebSocket client not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = [
|
||||||
|
'jsonrpc' => '2.0',
|
||||||
|
'method' => $method,
|
||||||
|
'id' => $this->requestId++,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($params)) {
|
||||||
|
$request['params'] = $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->client->text(json_encode($request));
|
||||||
|
$response = $this->client->receive();
|
||||||
|
$decoded = json_decode($response->getContent(), true);
|
||||||
|
|
||||||
|
if (isset($decoded['error'])) {
|
||||||
|
$errorMsg = $decoded['error']['message'] ?? 'Unknown error';
|
||||||
|
$errorCode = $decoded['error']['code'] ?? 0;
|
||||||
|
throw new \Exception("API error ({$errorCode}): {$errorMsg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded['result'] ?? null;
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
Log::error('TrueNAS WebSocket call failed: ' . $e->getMessage());
|
||||||
|
throw new \Exception('WebSocket call failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the WebSocket connection.
|
||||||
|
*/
|
||||||
|
public function disconnect(): void
|
||||||
|
{
|
||||||
|
if ($this->client !== null) {
|
||||||
|
try {
|
||||||
|
$this->client->close();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::debug('Error closing WebSocket: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
$this->client = null;
|
||||||
|
$this->authenticated = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the client is connected and authenticated.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isConnected(): bool
|
||||||
|
{
|
||||||
|
return $this->client !== null && $this->authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the connection by calling core.ping.
|
||||||
|
*
|
||||||
|
* @return bool True if the ping succeeds
|
||||||
|
*/
|
||||||
|
public function ping(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->call('core.ping');
|
||||||
|
return $result === 'pong';
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up on destruction.
|
||||||
|
*/
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"laravel/ui": "^4.4",
|
"laravel/ui": "^4.4",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"nunomaduro/collision": "^8.0",
|
"nunomaduro/collision": "^8.0",
|
||||||
|
"phrity/websocket": "^3.6",
|
||||||
"spatie/laravel-html": "^3.11",
|
"spatie/laravel-html": "^3.11",
|
||||||
"spatie/laravel-ignition": "^2.4",
|
"spatie/laravel-ignition": "^2.4",
|
||||||
"symfony/yaml": "^7.0"
|
"symfony/yaml": "^7.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user