1307 lines
42 KiB
PHP
1307 lines
42 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Migration;
|
|
|
|
use Exception;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class CpanelApiService
|
|
{
|
|
private string $hostname;
|
|
|
|
private string $username;
|
|
|
|
private string $apiToken;
|
|
|
|
private int $port;
|
|
|
|
private bool $ssl;
|
|
|
|
public function __construct(string $hostname, string $username, string $apiToken, int $port = 2083, bool $ssl = true)
|
|
{
|
|
// Trim all string inputs to remove copy-paste whitespace/invisible chars
|
|
$this->hostname = rtrim(trim($hostname), '/');
|
|
$this->username = trim($username);
|
|
$this->apiToken = trim($apiToken);
|
|
$this->port = $port;
|
|
$this->ssl = $ssl;
|
|
}
|
|
|
|
/**
|
|
* Get the base URL for API calls
|
|
*/
|
|
private function getBaseUrl(): string
|
|
{
|
|
$protocol = $this->ssl ? 'https' : 'http';
|
|
|
|
return "{$protocol}://{$this->hostname}:{$this->port}";
|
|
}
|
|
|
|
/**
|
|
* Make a UAPI request to cPanel
|
|
*/
|
|
public function uapi(string $module, string $function, array $params = []): array
|
|
{
|
|
return $this->uapiWithTimeout($module, $function, $params, 30);
|
|
}
|
|
|
|
/**
|
|
* Make a UAPI request to cPanel with custom timeout
|
|
*/
|
|
public function uapiWithTimeout(string $module, string $function, array $params = [], int $timeout = 30): array
|
|
{
|
|
$url = $this->getBaseUrl()."/execute/{$module}/{$function}";
|
|
|
|
Log::info('cPanel UAPI request', ['url' => $url, 'module' => $module, 'function' => $function, 'timeout' => $timeout]);
|
|
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'Authorization' => "cpanel {$this->username}:{$this->apiToken}",
|
|
])
|
|
->timeout($timeout)
|
|
->connectTimeout(10)
|
|
->withoutVerifying()
|
|
->get($url, $params);
|
|
|
|
Log::info('cPanel UAPI response status', ['status' => $response->status(), 'module' => $module]);
|
|
|
|
if (! $response->successful()) {
|
|
throw new Exception('API request failed with status: '.$response->status());
|
|
}
|
|
|
|
$data = $response->json();
|
|
|
|
Log::info('cPanel UAPI response data', ['module' => $module, 'function' => $function, 'data' => $data]);
|
|
|
|
if (isset($data['status']) && $data['status'] === 0) {
|
|
throw new Exception($data['errors'][0] ?? 'Unknown API error');
|
|
}
|
|
|
|
return $data;
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel API error: '.$e->getMessage(), [
|
|
'module' => $module,
|
|
'function' => $function,
|
|
]);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make an API2 request to cPanel (legacy API)
|
|
*/
|
|
public function api2(string $module, string $function, array $params = []): array
|
|
{
|
|
$url = $this->getBaseUrl().'/json-api/cpanel';
|
|
|
|
$queryParams = array_merge([
|
|
'cpanel_jsonapi_user' => $this->username,
|
|
'cpanel_jsonapi_apiversion' => '2',
|
|
'cpanel_jsonapi_module' => $module,
|
|
'cpanel_jsonapi_func' => $function,
|
|
], $params);
|
|
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'Authorization' => "cpanel {$this->username}:{$this->apiToken}",
|
|
])
|
|
->timeout(120)
|
|
->withoutVerifying()
|
|
->get($url, $queryParams);
|
|
|
|
if (! $response->successful()) {
|
|
throw new Exception('API request failed with status: '.$response->status());
|
|
}
|
|
|
|
return $response->json();
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel API2 error: '.$e->getMessage(), [
|
|
'module' => $module,
|
|
'function' => $function,
|
|
]);
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test the connection to cPanel
|
|
*/
|
|
public function testConnection(): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('ResourceUsage', 'get_usages');
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Connection successful',
|
|
'data' => $result['result']['data'] ?? [],
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get account information
|
|
*/
|
|
public function getAccountInfo(): array
|
|
{
|
|
try {
|
|
$stats = $this->uapi('StatsBar', 'get_stats', [
|
|
'display' => 'diskusage|bandwidthusage|addondomains|subdomains|parkeddomains|sqldatabases|emailaccounts',
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'stats' => $stats['result']['data'] ?? [],
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all domains (main, addon, subdomains, parked)
|
|
*/
|
|
public function listDomains(): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('DomainInfo', 'list_domains');
|
|
|
|
// Log raw response for debugging
|
|
Log::info('cPanel listDomains raw response', ['result' => $result]);
|
|
|
|
// Handle different response structures
|
|
$data = $result['result']['data'] ?? $result['data'] ?? $result;
|
|
|
|
return [
|
|
'success' => true,
|
|
'main_domain' => $data['main_domain'] ?? '',
|
|
'addon_domains' => $data['addon_domains'] ?? [],
|
|
'sub_domains' => $data['sub_domains'] ?? [],
|
|
'parked_domains' => $data['parked_domains'] ?? [],
|
|
];
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel listDomains failed', ['error' => $e->getMessage()]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List MySQL databases
|
|
*/
|
|
public function listDatabases(): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('Mysql', 'list_databases');
|
|
|
|
// Handle different response structures
|
|
$data = $result['result']['data'] ?? $result['data'] ?? [];
|
|
|
|
return [
|
|
'success' => true,
|
|
'databases' => $data,
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List MySQL users
|
|
*/
|
|
public function listDatabaseUsers(): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('Mysql', 'list_users');
|
|
$data = $result['result']['data'] ?? $result['data'] ?? [];
|
|
|
|
return [
|
|
'success' => true,
|
|
'users' => $data,
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List email accounts
|
|
*/
|
|
public function listEmailAccounts(): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('Email', 'list_pops_with_disk');
|
|
$data = $result['result']['data'] ?? $result['data'] ?? [];
|
|
|
|
return [
|
|
'success' => true,
|
|
'accounts' => $data,
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List email forwarders
|
|
*/
|
|
public function listForwarders(): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('Email', 'list_forwarders');
|
|
$data = $result['result']['data'] ?? $result['data'] ?? [];
|
|
|
|
return [
|
|
'success' => true,
|
|
'forwarders' => $data,
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List SSL certificates
|
|
*/
|
|
public function listSslCertificates(): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('SSL', 'list_certs');
|
|
$data = $result['result']['data'] ?? $result['data'] ?? [];
|
|
|
|
return [
|
|
'success' => true,
|
|
'certificates' => $data,
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List cron jobs
|
|
*/
|
|
public function listCronJobs(): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('Cron', 'list_cron');
|
|
$data = $result['result']['data'] ?? $result['data'] ?? [];
|
|
|
|
return [
|
|
'success' => true,
|
|
'cron_jobs' => $data,
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a full backup to homedir
|
|
*/
|
|
public function createBackup(): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('Backup', 'fullbackup_to_homedir');
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Backup initiated',
|
|
'pid' => $result['result']['data']['pid'] ?? null,
|
|
'data' => $result['result']['data'] ?? [],
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List available backups in homedir using API2 (more reliable)
|
|
*/
|
|
public function listBackups(): array
|
|
{
|
|
try {
|
|
// Use API2 Backups/listfullbackups as it's more reliable
|
|
$result = $this->api2('Backups', 'listfullbackups', []);
|
|
$backups = $result['cpanelresult']['data'] ?? [];
|
|
|
|
// Format the backups consistently
|
|
$formattedBackups = [];
|
|
foreach ($backups as $backup) {
|
|
$formattedBackups[] = [
|
|
'file' => $backup['file'] ?? '',
|
|
'status' => $backup['status'] ?? 'unknown',
|
|
'time' => $backup['time'] ?? 0,
|
|
'localtime' => $backup['localtime'] ?? '',
|
|
'path' => "/home/{$this->username}/".$backup['file'],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'backups' => $formattedBackups,
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the cPanel File Manager URL for downloading a backup
|
|
*/
|
|
public function getBackupDownloadUrl(string $filename): string
|
|
{
|
|
$protocol = $this->ssl ? 'https' : 'http';
|
|
|
|
return "{$protocol}://{$this->hostname}:{$this->port}/cpsess0/frontend/jupiter/filemanager/index.html";
|
|
}
|
|
|
|
/**
|
|
* Get direct instructions for downloading a backup
|
|
*/
|
|
public function getDownloadInstructions(string $filename): array
|
|
{
|
|
$protocol = $this->ssl ? 'https' : 'http';
|
|
$loginUrl = "{$protocol}://{$this->hostname}:{$this->port}";
|
|
|
|
return [
|
|
'login_url' => $loginUrl,
|
|
'username' => $this->username,
|
|
'steps' => [
|
|
"1. Log in to cPanel at {$loginUrl}",
|
|
'2. Go to File Manager',
|
|
"3. Navigate to the home directory (/home/{$this->username})",
|
|
"4. Right-click on '{$filename}' and select 'Download'",
|
|
'5. Once downloaded, upload the file using the form below',
|
|
],
|
|
'backup_path' => "/home/{$this->username}/{$filename}",
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Check if a backup is currently in progress
|
|
*/
|
|
public function getBackupStatus(): array
|
|
{
|
|
try {
|
|
// Use API2 Fileman/listfiles as UAPI list_files returns empty on some cPanel versions
|
|
$result = $this->api2('Fileman', 'listfiles', [
|
|
'dir' => "/home/{$this->username}",
|
|
'showdotfiles' => 0,
|
|
]);
|
|
|
|
$files = $result['cpanelresult']['data'] ?? [];
|
|
$backupFiles = [];
|
|
$inProgress = false;
|
|
|
|
foreach ($files as $file) {
|
|
$name = $file['file'] ?? $file['name'] ?? '';
|
|
// cPanel backup files are named like: backup-MM.DD.YYYY_HH-mm-ss_username.tar.gz
|
|
if (preg_match('/^backup-\d+\.\d+\.\d+_\d+-\d+-\d+_.*\.tar\.gz$/', $name)) {
|
|
$backupFiles[] = [
|
|
'name' => $name,
|
|
'size' => $file['size'] ?? 0,
|
|
'mtime' => $file['mtime'] ?? 0,
|
|
'path' => "/home/{$this->username}/{$name}",
|
|
];
|
|
}
|
|
// Check for in-progress backup indicator
|
|
if (str_contains($name, 'backup') && str_ends_with($name, '.log')) {
|
|
$inProgress = true;
|
|
}
|
|
}
|
|
|
|
// Sort by modification time, newest first
|
|
usort($backupFiles, fn ($a, $b) => ($b['mtime'] ?? 0) - ($a['mtime'] ?? 0));
|
|
|
|
return [
|
|
'success' => true,
|
|
'in_progress' => $inProgress,
|
|
'backups' => $backupFiles,
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List files in a directory
|
|
*/
|
|
public function listFiles(string $dir = '/'): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('Fileman', 'list_files', [
|
|
'dir' => $dir,
|
|
'include_mime' => 0,
|
|
'include_permissions' => 1,
|
|
'include_hash' => 0,
|
|
'include_content' => 0,
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'files' => $result['result']['data'] ?? [],
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download a file from cPanel to a local path (streaming)
|
|
*/
|
|
public function downloadFileToPath(string $remotePath, string $localPath, ?callable $progressCallback = null): array
|
|
{
|
|
$url = $this->getBaseUrl().'/download?file='.urlencode($remotePath);
|
|
|
|
Log::info('cPanel download starting', ['remote' => $remotePath, 'local' => $localPath]);
|
|
|
|
try {
|
|
// First, get the file size
|
|
$files = $this->listFiles(dirname($remotePath));
|
|
$fileSize = 0;
|
|
if ($files['success']) {
|
|
foreach ($files['files'] as $file) {
|
|
if (($file['file'] ?? $file['name'] ?? '') === basename($remotePath)) {
|
|
$fileSize = (int) ($file['size'] ?? 0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create a stream context for downloading
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'GET',
|
|
'header' => "Authorization: cpanel {$this->username}:{$this->apiToken}\r\n",
|
|
'timeout' => 3600, // 1 hour timeout for large files
|
|
'ignore_errors' => true,
|
|
],
|
|
'ssl' => [
|
|
'verify_peer' => false,
|
|
'verify_peer_name' => false,
|
|
],
|
|
]);
|
|
|
|
// Open remote file
|
|
$remoteStream = @fopen($url, 'rb', false, $context);
|
|
if (! $remoteStream) {
|
|
throw new Exception("Failed to open remote file: $remotePath");
|
|
}
|
|
|
|
// Ensure local directory exists
|
|
$localDir = dirname($localPath);
|
|
if (! is_dir($localDir)) {
|
|
mkdir($localDir, 0755, true);
|
|
}
|
|
|
|
// Open local file for writing
|
|
$localStream = fopen($localPath, 'wb');
|
|
if (! $localStream) {
|
|
fclose($remoteStream);
|
|
throw new Exception("Failed to open local file for writing: $localPath");
|
|
}
|
|
|
|
// Download in chunks
|
|
$downloaded = 0;
|
|
$chunkSize = 1024 * 1024; // 1MB chunks
|
|
|
|
while (! feof($remoteStream)) {
|
|
$chunk = fread($remoteStream, $chunkSize);
|
|
if ($chunk === false) {
|
|
break;
|
|
}
|
|
fwrite($localStream, $chunk);
|
|
$downloaded += strlen($chunk);
|
|
|
|
if ($progressCallback && $fileSize > 0) {
|
|
$progressCallback($downloaded, $fileSize);
|
|
}
|
|
}
|
|
|
|
fclose($remoteStream);
|
|
fclose($localStream);
|
|
|
|
// Verify file was downloaded
|
|
if (! file_exists($localPath) || filesize($localPath) === 0) {
|
|
throw new Exception('Download failed - file is empty or missing');
|
|
}
|
|
|
|
Log::info('cPanel download completed', [
|
|
'remote' => $remotePath,
|
|
'local' => $localPath,
|
|
'size' => filesize($localPath),
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'path' => $localPath,
|
|
'size' => filesize($localPath),
|
|
];
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel download error: '.$e->getMessage(), [
|
|
'remote' => $remotePath,
|
|
'local' => $localPath,
|
|
]);
|
|
|
|
// Clean up partial download
|
|
if (file_exists($localPath)) {
|
|
@unlink($localPath);
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get homedir path
|
|
*/
|
|
public function getHomedir(): string
|
|
{
|
|
return "/home/{$this->username}";
|
|
}
|
|
|
|
/**
|
|
* Get the cPanel username
|
|
*/
|
|
public function getUsername(): string
|
|
{
|
|
return $this->username;
|
|
}
|
|
|
|
/**
|
|
* Import an SSH public key to cPanel
|
|
* Uses API2 SSH::importkey function
|
|
*
|
|
* @param string $keyName Name for the key in cPanel
|
|
* @param string $publicKey The SSH public key content
|
|
*/
|
|
public function importSshKey(string $keyName, string $publicKey): array
|
|
{
|
|
try {
|
|
Log::info('Importing SSH key to cPanel', ['key_name' => $keyName, 'key_length' => strlen($publicKey)]);
|
|
|
|
// Use API2 SSH::importkey
|
|
$result = $this->api2('SSH', 'importkey', [
|
|
'key' => $publicKey,
|
|
'name' => $keyName,
|
|
]);
|
|
|
|
Log::info('cPanel SSH import response', ['result' => $result]);
|
|
|
|
$data = $result['cpanelresult']['data'][0] ?? [];
|
|
$apiError = $result['cpanelresult']['error'] ?? '';
|
|
|
|
// Check for "already exists" which is OK (can appear in different places)
|
|
if (str_contains($apiError, 'already exists') || str_contains($data['reason'] ?? '', 'already exists')) {
|
|
Log::info('SSH key already exists on cPanel - treating as success');
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH key already exists',
|
|
];
|
|
}
|
|
|
|
// Check for API-level error
|
|
if ($apiError) {
|
|
throw new Exception($apiError);
|
|
}
|
|
|
|
// Check for success via event.result (cPanel API2 pattern)
|
|
$eventResult = $result['cpanelresult']['event']['result'] ?? null;
|
|
if ($eventResult == 1) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH key imported successfully',
|
|
];
|
|
}
|
|
|
|
// Legacy check for data[0].result
|
|
if (isset($data['result']) && $data['result'] == 1) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH key imported successfully',
|
|
];
|
|
}
|
|
|
|
// Check for alternative error locations
|
|
$errorMsg = ($data['reason'] ?? '')
|
|
?: ($data['error'] ?? '')
|
|
?: ($result['cpanelresult']['event']['reason'] ?? '')
|
|
?: 'Failed to import SSH key (unknown reason)';
|
|
|
|
throw new Exception($errorMsg);
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel SSH key import failed: '.$e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an SSH key from cPanel
|
|
* Uses API2 SSH::delkey function
|
|
*
|
|
* @param string $keyName Name of the key to delete
|
|
* @param string $keyType Type of key: 'key' for private, 'key.pub' for public
|
|
*/
|
|
public function deleteSshKey(string $keyName, string $keyType = 'key'): array
|
|
{
|
|
try {
|
|
Log::info('Deleting SSH key from cPanel', ['key_name' => $keyName, 'type' => $keyType]);
|
|
|
|
$result = $this->api2('SSH', 'delkey', [
|
|
'key' => $keyName,
|
|
'pub' => $keyType === 'key.pub' ? 1 : 0,
|
|
]);
|
|
|
|
Log::info('cPanel SSH delete response', ['result' => $result]);
|
|
|
|
$apiError = $result['cpanelresult']['error'] ?? '';
|
|
$eventResult = $result['cpanelresult']['event']['result'] ?? null;
|
|
|
|
// Check for success
|
|
if ($eventResult == 1 && empty($apiError)) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH key deleted',
|
|
];
|
|
}
|
|
|
|
// Key not found is OK
|
|
if (str_contains($apiError, 'does not exist') || str_contains($apiError, 'not found')) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Key does not exist',
|
|
];
|
|
}
|
|
|
|
if ($apiError) {
|
|
throw new Exception($apiError);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH key deleted',
|
|
];
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel SSH key delete failed: '.$e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Import an SSH private key to cPanel (for outgoing connections)
|
|
* Uses API2 SSH::importkey function
|
|
*
|
|
* @param string $keyName Name for the key in cPanel
|
|
* @param string $privateKey The SSH private key content
|
|
* @param string $passphrase Optional passphrase for the key
|
|
*/
|
|
public function importSshPrivateKey(string $keyName, string $privateKey, string $passphrase = ''): array
|
|
{
|
|
try {
|
|
Log::info('Importing SSH private key to cPanel', ['key_name' => $keyName, 'key_length' => strlen($privateKey)]);
|
|
|
|
// Use API2 SSH::importkey - private keys are detected by content
|
|
$params = [
|
|
'key' => $privateKey,
|
|
'name' => $keyName,
|
|
];
|
|
|
|
if (! empty($passphrase)) {
|
|
$params['pass'] = $passphrase;
|
|
}
|
|
|
|
$result = $this->api2('SSH', 'importkey', $params);
|
|
|
|
Log::info('cPanel SSH private key import response', ['result' => $result]);
|
|
|
|
$data = $result['cpanelresult']['data'][0] ?? [];
|
|
$apiError = $result['cpanelresult']['error'] ?? '';
|
|
|
|
// Check for "already exists" which is OK
|
|
if (str_contains($apiError, 'already exists') || str_contains($data['reason'] ?? '', 'already exists')) {
|
|
Log::info('SSH private key already exists on cPanel - treating as success');
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH key already exists',
|
|
];
|
|
}
|
|
|
|
// Check for API-level error
|
|
if ($apiError) {
|
|
throw new Exception($apiError);
|
|
}
|
|
|
|
// Check for success via event.result (cPanel API2 pattern)
|
|
$eventResult = $result['cpanelresult']['event']['result'] ?? null;
|
|
if ($eventResult == 1) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH private key imported successfully',
|
|
];
|
|
}
|
|
|
|
// Legacy check for data[0].result
|
|
if (isset($data['result']) && $data['result'] == 1) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH private key imported successfully',
|
|
];
|
|
}
|
|
|
|
// Check for alternative error locations
|
|
$errorMsg = ($data['reason'] ?? '')
|
|
?: ($data['error'] ?? '')
|
|
?: ($result['cpanelresult']['event']['reason'] ?? '')
|
|
?: 'Failed to import SSH private key (unknown reason)';
|
|
|
|
throw new Exception($errorMsg);
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel SSH private key import failed: '.$e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Authorize an SSH key in cPanel (make it usable)
|
|
* Uses API2 SSH::authkey function
|
|
*/
|
|
public function authorizeSshKey(string $keyName): array
|
|
{
|
|
try {
|
|
Log::info('Authorizing SSH key in cPanel', ['key_name' => $keyName]);
|
|
|
|
$result = $this->api2('SSH', 'authkey', [
|
|
'key' => $keyName,
|
|
'action' => 'authorize',
|
|
]);
|
|
|
|
Log::info('cPanel SSH authkey response', ['result' => $result]);
|
|
|
|
$data = $result['cpanelresult']['data'][0] ?? [];
|
|
$apiError = $result['cpanelresult']['error'] ?? '';
|
|
|
|
// Check for "already authorized" which is OK
|
|
if (str_contains($apiError, 'already authorized') || str_contains($data['reason'] ?? '', 'already authorized')) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH key already authorized',
|
|
];
|
|
}
|
|
|
|
// Check for API-level error first
|
|
if ($apiError) {
|
|
throw new Exception($apiError);
|
|
}
|
|
|
|
// Check for success via event.result (cPanel API2 pattern)
|
|
$eventResult = $result['cpanelresult']['event']['result'] ?? null;
|
|
if ($eventResult == 1) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH key authorized successfully',
|
|
];
|
|
}
|
|
|
|
// Legacy check for data[0].result
|
|
if (isset($data['result']) && $data['result'] == 1) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'SSH key authorized successfully',
|
|
];
|
|
}
|
|
|
|
$errorMsg = $data['reason'] ?? 'Failed to authorize SSH key';
|
|
throw new Exception($errorMsg);
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel SSH key authorization failed: '.$e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a full backup and upload to remote server via SCP with key authentication
|
|
* The SSH key must be previously imported to cPanel using importSshKey()
|
|
*
|
|
* @param string $remoteHost The remote server hostname/IP
|
|
* @param string $remoteUser The SSH username on the remote server
|
|
* @param string $remotePath The destination path on the remote server
|
|
* @param string $keyName Name of the SSH key stored in cPanel
|
|
* @param int $remotePort SSH port (default 22)
|
|
*/
|
|
public function createBackupToScpWithKey(
|
|
string $remoteHost,
|
|
string $remoteUser,
|
|
string $remotePath,
|
|
string $keyName,
|
|
int $remotePort = 22
|
|
): array {
|
|
try {
|
|
$params = [
|
|
'host' => $remoteHost,
|
|
'username' => $remoteUser,
|
|
'directory' => $remotePath,
|
|
'key_name' => $keyName,
|
|
'port' => $remotePort,
|
|
'key_passphrase' => '', // Empty passphrase for keys generated without one
|
|
];
|
|
|
|
Log::info('cPanel backup to SCP with key initiated', [
|
|
'host' => $remoteHost,
|
|
'user' => $remoteUser,
|
|
'path' => $remotePath,
|
|
'key_name' => $keyName,
|
|
'port' => $remotePort,
|
|
]);
|
|
|
|
// Use longer timeout for backup operations
|
|
$result = $this->uapiWithTimeout('Backup', 'fullbackup_to_scp_with_key', $params, 120);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Backup initiated with SCP transfer',
|
|
'pid' => $result['result']['data']['pid'] ?? null,
|
|
'data' => $result['result']['data'] ?? [],
|
|
];
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel backup to SCP with key failed: '.$e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Legacy method - kept for backward compatibility
|
|
*
|
|
* @deprecated Use createBackupToScpWithKey instead
|
|
*/
|
|
public function createBackupToScp(
|
|
string $remoteHost,
|
|
string $remoteUser,
|
|
string $remotePath,
|
|
string $privateKey,
|
|
string $passphrase = '',
|
|
int $remotePort = 22
|
|
): array {
|
|
// This method is deprecated - the key should be imported first
|
|
Log::warning('createBackupToScp is deprecated, use createBackupToScpWithKey instead');
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'This method is deprecated. Import the SSH key first using importSshKey(), then use createBackupToScpWithKey()',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create a full backup and upload to remote server via SCP with password authentication
|
|
*/
|
|
public function createBackupToScpWithPassword(
|
|
string $remoteHost,
|
|
string $remoteUser,
|
|
string $remotePath,
|
|
string $password,
|
|
int $remotePort = 22
|
|
): array {
|
|
try {
|
|
$params = [
|
|
'host' => $remoteHost,
|
|
'user' => $remoteUser,
|
|
'directory' => $remotePath,
|
|
'password' => $password,
|
|
'port' => $remotePort,
|
|
];
|
|
|
|
Log::info('cPanel backup to SCP (password) initiated', [
|
|
'host' => $remoteHost,
|
|
'user' => $remoteUser,
|
|
'path' => $remotePath,
|
|
'port' => $remotePort,
|
|
]);
|
|
|
|
// Use longer timeout for backup operations (120 seconds)
|
|
$result = $this->uapiWithTimeout('Backup', 'fullbackup_to_scp_with_password', $params, 120);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Backup initiated with SCP transfer',
|
|
'pid' => $result['result']['data']['pid'] ?? null,
|
|
'data' => $result['result']['data'] ?? [],
|
|
];
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel backup to SCP (password) failed: '.$e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export a database
|
|
*/
|
|
public function exportDatabase(string $database): array
|
|
{
|
|
try {
|
|
// Use mysqldump via cPanel's backup API
|
|
$result = $this->uapi('Mysql', 'dump_database', [
|
|
'dbname' => $database,
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => $result['result']['data'] ?? '',
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get SSL certificate for a domain
|
|
*/
|
|
public function getSslCertificate(string $domain): array
|
|
{
|
|
try {
|
|
$result = $this->uapi('SSL', 'fetch_best_for_domain', [
|
|
'domain' => $domain,
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'certificate' => $result['result']['data'] ?? [],
|
|
];
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download a file from cPanel
|
|
*/
|
|
public function downloadFile(string $path): ?string
|
|
{
|
|
$url = $this->getBaseUrl().'/download?file='.urlencode($path);
|
|
|
|
try {
|
|
$response = Http::withHeaders([
|
|
'Authorization' => "cpanel {$this->username}:{$this->apiToken}",
|
|
])
|
|
->timeout(300)
|
|
->withoutVerifying()
|
|
->get($url);
|
|
|
|
if ($response->successful()) {
|
|
return $response->body();
|
|
}
|
|
|
|
return null;
|
|
} catch (Exception $e) {
|
|
Log::error('cPanel download error: '.$e->getMessage());
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete/revoke the current API token from cPanel
|
|
* This should be called after migration is complete for security
|
|
*/
|
|
public function revokeApiToken(): array
|
|
{
|
|
try {
|
|
Log::info('Attempting to revoke cPanel API token');
|
|
|
|
// First, list all tokens to find the current one
|
|
$listResult = $this->uapi('Tokens', 'list');
|
|
$tokens = $listResult['result']['data'] ?? [];
|
|
|
|
Log::info('Found API tokens', ['count' => count($tokens)]);
|
|
|
|
// The current token should be one of these - we'll try to identify and revoke it
|
|
// cPanel doesn't directly tell us which token we're using, but we can revoke by name
|
|
// If the token was created with a specific name, we can target it
|
|
|
|
// Try to revoke all tokens (user should create a new one if needed)
|
|
// Or we can try to identify the token by checking which one works
|
|
$revoked = false;
|
|
foreach ($tokens as $token) {
|
|
$tokenName = $token['name'] ?? '';
|
|
if (empty($tokenName)) {
|
|
continue;
|
|
}
|
|
|
|
// Try to revoke this token
|
|
try {
|
|
$revokeResult = $this->uapi('Tokens', 'revoke', [
|
|
'name' => $tokenName,
|
|
]);
|
|
|
|
if (($revokeResult['result']['status'] ?? 0) == 1) {
|
|
Log::info('Revoked API token', ['name' => $tokenName]);
|
|
$revoked = true;
|
|
// After revoking the current token, subsequent API calls will fail
|
|
// So we should stop here
|
|
break;
|
|
}
|
|
} catch (Exception $e) {
|
|
// If we get an auth error, we probably just revoked our own token
|
|
if (str_contains($e->getMessage(), 'Authorization') || str_contains($e->getMessage(), '401')) {
|
|
Log::info('Token likely revoked (auth failed)', ['name' => $tokenName]);
|
|
$revoked = true;
|
|
break;
|
|
}
|
|
Log::warning('Failed to revoke token', ['name' => $tokenName, 'error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
if ($revoked) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'API token revoked successfully',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Could not identify token to revoke',
|
|
];
|
|
} catch (Exception $e) {
|
|
// Auth failure after revoke is actually success
|
|
if (str_contains($e->getMessage(), 'Authorization') || str_contains($e->getMessage(), '401')) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'API token revoked (connection closed)',
|
|
];
|
|
}
|
|
|
|
Log::error('Failed to revoke API token: '.$e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Revoke a specific API token by name
|
|
*/
|
|
public function revokeApiTokenByName(string $tokenName): array
|
|
{
|
|
try {
|
|
Log::info('Revoking cPanel API token by name', ['name' => $tokenName]);
|
|
|
|
$result = $this->uapi('Tokens', 'revoke', [
|
|
'name' => $tokenName,
|
|
]);
|
|
|
|
if (($result['result']['status'] ?? 0) == 1) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'API token revoked successfully',
|
|
];
|
|
}
|
|
|
|
$error = $result['result']['errors'][0] ?? 'Unknown error';
|
|
throw new Exception($error);
|
|
} catch (Exception $e) {
|
|
// Auth failure after revoke means it worked
|
|
if (str_contains($e->getMessage(), 'Authorization') || str_contains($e->getMessage(), '401')) {
|
|
return [
|
|
'success' => true,
|
|
'message' => 'API token revoked',
|
|
];
|
|
}
|
|
|
|
Log::error('Failed to revoke API token: '.$e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a comprehensive migration summary
|
|
*/
|
|
public function getMigrationSummary(): array
|
|
{
|
|
$summary = [
|
|
'success' => true,
|
|
'domains' => [
|
|
'main' => '',
|
|
'addon' => [],
|
|
'sub' => [],
|
|
'parked' => [],
|
|
],
|
|
'databases' => [],
|
|
'email_accounts' => [],
|
|
'forwarders' => [],
|
|
'ssl_certificates' => [],
|
|
'cron_jobs' => [],
|
|
'errors' => [],
|
|
];
|
|
|
|
// Get domains
|
|
try {
|
|
$domains = $this->listDomains();
|
|
if ($domains['success']) {
|
|
$summary['domains'] = [
|
|
'main' => $domains['main_domain'] ?? '',
|
|
'addon' => $domains['addon_domains'] ?? [],
|
|
'sub' => $domains['sub_domains'] ?? [],
|
|
'parked' => $domains['parked_domains'] ?? [],
|
|
];
|
|
} else {
|
|
$summary['errors'][] = 'Domains: '.($domains['message'] ?? 'Unknown error');
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::warning('cPanel migration - failed to list domains: '.$e->getMessage());
|
|
$summary['errors'][] = 'Domains: '.$e->getMessage();
|
|
}
|
|
|
|
// Get databases
|
|
try {
|
|
$databases = $this->listDatabases();
|
|
if ($databases['success']) {
|
|
$summary['databases'] = $databases['databases'] ?? [];
|
|
} else {
|
|
$summary['errors'][] = 'Databases: '.($databases['message'] ?? 'Unknown error');
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::warning('cPanel migration - failed to list databases: '.$e->getMessage());
|
|
$summary['errors'][] = 'Databases: '.$e->getMessage();
|
|
}
|
|
|
|
// Get email accounts
|
|
try {
|
|
$emails = $this->listEmailAccounts();
|
|
if ($emails['success']) {
|
|
$summary['email_accounts'] = $emails['accounts'] ?? [];
|
|
} else {
|
|
$summary['errors'][] = 'Email: '.($emails['message'] ?? 'Unknown error');
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::warning('cPanel migration - failed to list email accounts: '.$e->getMessage());
|
|
$summary['errors'][] = 'Email: '.$e->getMessage();
|
|
}
|
|
|
|
// Get forwarders
|
|
try {
|
|
$forwarders = $this->listForwarders();
|
|
if ($forwarders['success']) {
|
|
$summary['forwarders'] = $forwarders['forwarders'] ?? [];
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::warning('cPanel migration - failed to list forwarders: '.$e->getMessage());
|
|
}
|
|
|
|
// Get SSL certificates
|
|
try {
|
|
$ssl = $this->listSslCertificates();
|
|
if ($ssl['success']) {
|
|
$summary['ssl_certificates'] = $ssl['certificates'] ?? [];
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::warning('cPanel migration - failed to list SSL certificates: '.$e->getMessage());
|
|
}
|
|
|
|
// Get cron jobs
|
|
try {
|
|
$cron = $this->listCronJobs();
|
|
if ($cron['success']) {
|
|
$summary['cron_jobs'] = $cron['cron_jobs'] ?? [];
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::warning('cPanel migration - failed to list cron jobs: '.$e->getMessage());
|
|
}
|
|
|
|
// Log summary for debugging
|
|
Log::info('cPanel migration summary', [
|
|
'domains_main' => $summary['domains']['main'],
|
|
'domains_addon_count' => count($summary['domains']['addon']),
|
|
'databases_count' => count($summary['databases']),
|
|
'email_accounts_count' => count($summary['email_accounts']),
|
|
'errors' => $summary['errors'],
|
|
]);
|
|
|
|
return $summary;
|
|
}
|
|
}
|