274 lines
9.7 KiB
Swift
274 lines
9.7 KiB
Swift
//
|
|
// ServerAPI.swift
|
|
// iKeyMon
|
|
//
|
|
// Created by tracer on 06.04.25.
|
|
//
|
|
import Foundation
|
|
|
|
final class ServerAPI {
|
|
private let hostname: String
|
|
private let apiKey: String
|
|
|
|
init(hostname: String, apiKey: String) {
|
|
self.hostname = hostname
|
|
self.apiKey = apiKey
|
|
}
|
|
|
|
init?(server: Server) {
|
|
guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) else {
|
|
return nil
|
|
}
|
|
self.hostname = server.hostname
|
|
self.apiKey = apiKey
|
|
}
|
|
|
|
@discardableResult
|
|
func ping() async -> Bool {
|
|
guard let url = URL(string: "https://\(hostname)/api/v2/ping") else {
|
|
print("❌ [ServerAPI] Invalid ping URL for hostname: \(hostname)")
|
|
return false
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY")
|
|
request.timeoutInterval = 10
|
|
|
|
do {
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
|
|
// Add debug info for comparison with server info request
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
if httpResponse.statusCode != 200 {
|
|
print("❌ [ServerAPI] Ping HTTP Status: \(httpResponse.statusCode) for \(hostname)")
|
|
if let responseString = String(data: data, encoding: .utf8) {
|
|
print("❌ [ServerAPI] Ping error response: \(responseString)")
|
|
}
|
|
}
|
|
}
|
|
|
|
if let result = try? JSONDecoder().decode([String: String].self, from: data), result["response"] == "pong" {
|
|
return true
|
|
} else {
|
|
print("❌ [ServerAPI] Ping failed - invalid response for \(hostname)")
|
|
if let responseString = String(data: data, encoding: .utf8) {
|
|
print("❌ [ServerAPI] Ping response: \(responseString)")
|
|
}
|
|
return false
|
|
}
|
|
} catch {
|
|
print("❌ [ServerAPI] Ping error for \(hostname): \(error)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func fetchServerInfo() async throws -> ServerInfo {
|
|
print("🔍 [ServerAPI] Starting fetchServerInfo for hostname: \(hostname)")
|
|
|
|
guard let url = URL(string: "https://\(hostname)/api/v2/server") else {
|
|
print("❌ [ServerAPI] Invalid URL for hostname: \(hostname)")
|
|
throw URLError(.badURL)
|
|
}
|
|
|
|
print("🌐 [ServerAPI] URL created: \(url.absoluteString)")
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY")
|
|
// Temporarily remove Content-Type to match ping request exactly
|
|
// request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.timeoutInterval = 30
|
|
|
|
print("📤 [ServerAPI] Making request with timeout: \(request.timeoutInterval)s")
|
|
print("📤 [ServerAPI] API Key (first 10 chars): \(String(apiKey.prefix(10)))...")
|
|
print("📤 [ServerAPI] Request headers: \(request.allHTTPHeaderFields ?? [:])")
|
|
|
|
do {
|
|
let startTime = Date()
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
let endTime = Date()
|
|
let duration = endTime.timeIntervalSince(startTime)
|
|
|
|
print("📥 [ServerAPI] Request completed in \(String(format: "%.2f", duration))s")
|
|
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
print("📥 [ServerAPI] HTTP Status: \(httpResponse.statusCode)")
|
|
|
|
// Handle 401 specifically
|
|
if httpResponse.statusCode == 401 {
|
|
print("❌ [ServerAPI] 401 Unauthorized - API key issue!")
|
|
if let responseString = String(data: data, encoding: .utf8) {
|
|
print("❌ [ServerAPI] 401 Response body: \(responseString)")
|
|
}
|
|
throw URLError(.userAuthenticationRequired)
|
|
}
|
|
|
|
// Handle other non-200 status codes
|
|
if httpResponse.statusCode != 200 {
|
|
print("❌ [ServerAPI] Non-200 status code: \(httpResponse.statusCode)")
|
|
if let responseString = String(data: data, encoding: .utf8) {
|
|
print("❌ [ServerAPI] Error response body: \(responseString)")
|
|
}
|
|
throw URLError(.badServerResponse)
|
|
}
|
|
}
|
|
|
|
print("📥 [ServerAPI] Data size: \(data.count) bytes")
|
|
|
|
print("🔄 [ServerAPI] Decoding response payload...")
|
|
|
|
let decoder = JSONDecoder()
|
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
let decodedResponse = try decoder.decode(ServerResponseEnvelope.self, from: data)
|
|
let decoded = decodedResponse.toDomain()
|
|
print("✅ [ServerAPI] JSON decoding successful")
|
|
|
|
return decoded
|
|
} catch let urlError as URLError {
|
|
print("❌ [ServerAPI] URLError: \(urlError.localizedDescription)")
|
|
print("❌ [ServerAPI] URLError code: \(urlError.code.rawValue)")
|
|
throw urlError
|
|
} catch let decodingError as DecodingError {
|
|
print("❌ [ServerAPI] Decoding error: \(decodingError)")
|
|
throw decodingError
|
|
} catch {
|
|
print("❌ [ServerAPI] Unexpected error: \(error)")
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Response Helpers
|
|
|
|
private extension ServerAPI {
|
|
struct ServerResponseEnvelope: Decodable {
|
|
let data: ServerData
|
|
let meta: Meta
|
|
|
|
struct Meta: Decodable {
|
|
let apiVersion: String
|
|
}
|
|
}
|
|
|
|
struct ServerData: Decodable {
|
|
struct Port: Decodable {
|
|
let service: String
|
|
let status: String
|
|
let port: Int
|
|
let proto: String
|
|
}
|
|
|
|
struct Load: Decodable {
|
|
let minute1: Double
|
|
let minute5: Double
|
|
let minute15: Double
|
|
let percent: Double
|
|
let cpuCount: Int
|
|
let level: String
|
|
}
|
|
|
|
struct Memory: Decodable {
|
|
let free: Int
|
|
let used: Int
|
|
let total: Int
|
|
let percent: Double
|
|
}
|
|
|
|
struct Disk: Decodable {
|
|
let free: Int
|
|
let used: Int
|
|
let total: Int
|
|
let percent: Double
|
|
}
|
|
|
|
struct PHPInterpreter: Decodable {
|
|
let version: String
|
|
let path: String?
|
|
let configFile: String?
|
|
let extensions: [String]
|
|
let memoryLimit: String?
|
|
let maxExecutionTime: String?
|
|
}
|
|
|
|
let hostname: String
|
|
let ipAddresses: [String]
|
|
let cpuCores: Int
|
|
let serverTime: String
|
|
let uptime: String
|
|
let processCount: Int
|
|
let apacheVersion: String
|
|
let phpVersion: String
|
|
let mysqlVersion: String?
|
|
let mariadbVersion: String?
|
|
let ports: [Port]?
|
|
let load: Load
|
|
let memory: Memory
|
|
let swap: Memory
|
|
let diskSpace: Disk
|
|
let panelVersion: String
|
|
let panelBuild: String
|
|
let additionalPhpInterpreters: [PHPInterpreter]?
|
|
}
|
|
|
|
}
|
|
|
|
private extension ServerAPI.ServerResponseEnvelope {
|
|
func toDomain() -> ServerInfo {
|
|
ServerInfo(
|
|
hostname: data.hostname,
|
|
ipAddresses: data.ipAddresses,
|
|
cpuCores: data.cpuCores,
|
|
serverTime: data.serverTime,
|
|
uptime: data.uptime,
|
|
processCount: data.processCount,
|
|
emailsInQueue: nil,
|
|
apacheVersion: data.apacheVersion,
|
|
phpVersion: data.phpVersion,
|
|
mysqlVersion: data.mysqlVersion,
|
|
mariadbVersion: data.mariadbVersion,
|
|
operatingSystem: nil,
|
|
ports: data.ports?.map { ServerInfo.ServicePort(service: $0.service, status: $0.status, port: $0.port, proto: $0.proto) },
|
|
load: ServerInfo.Load(
|
|
minute1: data.load.minute1,
|
|
minute5: data.load.minute5,
|
|
minute15: data.load.minute15,
|
|
percent: data.load.percent,
|
|
cpuCount: data.load.cpuCount,
|
|
level: data.load.level
|
|
),
|
|
memory: ServerInfo.Memory(
|
|
free: data.memory.free,
|
|
used: data.memory.used,
|
|
total: data.memory.total,
|
|
percent: data.memory.percent
|
|
),
|
|
swap: ServerInfo.Memory(
|
|
free: data.swap.free,
|
|
used: data.swap.used,
|
|
total: data.swap.total,
|
|
percent: data.swap.percent
|
|
),
|
|
diskSpace: ServerInfo.DiskSpace(
|
|
free: data.diskSpace.free,
|
|
used: data.diskSpace.used,
|
|
total: data.diskSpace.total,
|
|
percent: data.diskSpace.percent
|
|
),
|
|
panelVersion: data.panelVersion,
|
|
panelBuild: data.panelBuild,
|
|
apiVersion: meta.apiVersion,
|
|
additionalPHPInterpreters: data.additionalPhpInterpreters?.map {
|
|
ServerInfo.PHPInterpreter(
|
|
version: $0.version,
|
|
path: $0.path,
|
|
configFile: $0.configFile,
|
|
extensions: $0.extensions,
|
|
memoryLimit: $0.memoryLimit,
|
|
maxExecutionTime: $0.maxExecutionTime
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|