// // 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, apacheVersion: data.apacheVersion, phpVersion: data.phpVersion, mysqlVersion: data.mysqlVersion, mariadbVersion: data.mariadbVersion, 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 ) } ) } }