import Foundation enum DNSChangeResponse: RawRepresentable, Codable { /** The update was successful. You should not attempt another update until your IP address changes. The response is followed by a list of the user’s IP addresses */ case success /** The supplied IP address is already set for this host. You should not attempt another update until your IP address changes. The response is followed by a list of the user’s IP addresses */ case noChange /** The hostname doesn't exist, or doesn't have Dynamic DNS enabled. */ case invalidHostname /** The username/password combination isn't valid for the specified host. */ case badAuthentication /** The supplied hostname isn't a valid fully-qualified domain name. */ case notFullyQualifiedDomainName /** Your Dynamic DNS client makes bad requests. Ensure the user agent is set in the request. */ case badUserAgent /** Too many hosts (more than 20) specified in an update. Also returned if trying to update a round robin (which is not allowed). */ case tooManyHosts /** Dynamic DNS access for the hostname has been blocked due to failure to interpret previous responses correctly. */ case abuse /** An error happened on the server end. Wait 5 minutes and retry. */ case unexpectedServerError /** A custom A or AAAA resource record conflicts with the update. Delete the indicated resource record within the DNS settings page and try the update again. */ case conflict case noResponseString case unknown(String) /// The IP addresses reported after update are not the same as the requested case ipMismatch init(rawValue: String) { switch rawValue { case "good": self = .success case "nochg": self = .noChange case "nohost": self = .invalidHostname case "badauth": self = .badAuthentication case "notfqdn": self = .notFullyQualifiedDomainName case "badagent": self = .badUserAgent case "abuse": self = .abuse case "numhost": self = .tooManyHosts case "911": self = .unexpectedServerError case "conflict": self = .conflict case "nostring": self = .noResponseString case "mismatch": self = .ipMismatch default: self = .unknown(rawValue) } } var rawValue: String { switch self { case .success: return "good" case .noChange: return "nochg" case .invalidHostname: return "nohost" case .badAuthentication: return "badauth" case .notFullyQualifiedDomainName: return "notfqdn" case .badUserAgent: return "badagent" case .abuse: return "abuse" case .tooManyHosts: return "numhost" case .unexpectedServerError: return "911" case .conflict: return "conflict" case .noResponseString: return "nostring" case .ipMismatch: return "mismatch" case .unknown(let string): return string } } init(line: String, expecting addresses: Set) { let parts = line.components(separatedBy: " ").compactMap { $0.trimmed } self.init(rawValue: parts[0]) if case .unknown = self { // Insert full response for better logging self = .unknown(line) return } guard isSuccessStatus else { return } guard Set(parts.dropFirst()) != addresses else { return } self = .ipMismatch } var isSuccessStatus: Bool { switch self { case .success, .noChange, .noResponseString, .ipMismatch: // Treat no response and ip mismatch as success, // otherwise we risk spamming the server return true default: return false } } var isServerError: Bool { switch self { case .invalidHostname, .badAuthentication, .notFullyQualifiedDomainName, .badUserAgent, .abuse, .unexpectedServerError, .conflict, .tooManyHosts: return true default: return false } } var isClientError: Bool { switch self { case .noResponseString, .unknown, .ipMismatch: return true default: return false } } func delayTime(previous time: TimeInterval) -> TimeInterval { switch self { case .invalidHostname, .badAuthentication, .notFullyQualifiedDomainName, .badUserAgent, .conflict, .noChange: // Increase wait time by 10 minutes, up to 24 hours return min(86400, time + 600) case .abuse: // Increase wait time by one hour, until abuse is lifted return time + 3600 case .unknown: // Don't spam on unknown errors return time + 3600 default: // Try again next time return 0 } } } extension DNSChangeResponse: CustomStringConvertible { var description: String { switch self { case .success: return "success" case .noChange: return "No change" case .invalidHostname: return "Invalid hostname" case .badAuthentication: return "Bad authentication" case .notFullyQualifiedDomainName: return "Not FQDN" case .badUserAgent: return "Bad user agent" case .tooManyHosts: return "Too many hosts" case .abuse: return "Abuse" case .unexpectedServerError: return "Server error" case .conflict: return "Conflict" case .noResponseString: return "No response string" case .unknown(let string): return string case .ipMismatch: return "IP mismatch" } } }