// // NTPConnection.swift // TrueTime // // Created by Michael Sanders on 8/10/16. // Copyright © 2016 Instacart. All rights reserved. // import Foundation typealias NTPConnectionCallback = (NTPConnection, FrozenNetworkTimeResult) -> Void final class NTPConnection { let address: SocketAddress let timeout: TimeInterval let maxRetries: Int var logger: LogCallback? static func query(addresses: [SocketAddress], config: NTPConfig, logger: LogCallback?, callbackQueue: DispatchQueue, progress: @escaping NTPConnectionCallback) -> [NTPConnection] { let connections = addresses.flatMap { address in (0.. Void)? let onComplete: NTPConnectionCallback = { connection, result in progress(connection, result) throttleConnections?() } throttleConnections = { let remainingConnections = connections.filter { $0.canRetry } let activeConnections = Array(remainingConnections[0.. String) { #if DEBUG_LOGGING logger?(message()) #endif } private let dataCallback: CFSocketCallBack = { socket, type, address, data, info in guard let info = info else { return } let client = Unmanaged.fromOpaque(info).takeUnretainedValue() guard let socket = socket, CFSocketIsValid(socket) else { return } // Can't use switch here as these aren't defined as an enum. if type == .dataCallBack { let data = unsafeBitCast(data, to: CFData.self) as Data client.handleResponse(data) } else if type == .writeCallBack { client.debugLog("Buffer \(client.address) writable - requesting time") client.requestTime() } else { assertionFailure("Unexpected socket callback") } } var timer: DispatchSourceTimer? private static let callbackTypes: [CFSocketCallBackType] = [.dataCallBack, .writeCallBack] private static let callbackFlags: CFOptionFlags = callbackTypes.map { $0.rawValue }.reduce(0, |) private let lockQueue = DispatchQueue(label: "com.instacart.ntp.connection") private var attempts: Int = 0 private var callbackQueue: DispatchQueue? private var didTimeout: Bool = false private var onComplete: NTPConnectionCallback? private var requestTicks: timeval? private var socket: CFSocket? private var source: CFRunLoopSource? private var startTime: ntp_time_t? private var finished: Bool = false } extension NTPConnection: TimedOperation { var timerQueue: DispatchQueue { return lockQueue } var started: Bool { return self.socket != nil } func timeoutError(_ error: NSError) { self.didTimeout = true complete(.failure(error)) } } private extension NTPConnection { func complete(_ result: FrozenNetworkTimeResult) { guard let callbackQueue = callbackQueue, let onComplete = onComplete else { assertionFailure("Completion callback not initialized") return } close() switch result { case let .failure(error) where attempts < maxRetries && !didTimeout: debugLog("Got error from \(address) (attempt \(attempts)), " + "trying again. \(error)") start(callbackQueue, onComplete: onComplete) case .failure, .success: finished = true callbackQueue.async { onComplete(self, result) } } } func requestTime() { lockQueue.async { guard let socket = self.socket else { self.debugLog("Socket closed") return } self.startTime = ntp_time_t(timeSince1970: .now()) self.requestTicks = .uptime() if let startTime = self.startTime { let packet = self.requestPacket(startTime).bigEndian let interval = TimeInterval(milliseconds: startTime.milliseconds) self.debugLog("Sending time: \(Date(timeIntervalSince1970: interval))") let err = CFSocketSendData(socket, self.address.networkData as CFData, packet.data as CFData, self.timeout) if err != .success { self.complete(.failure(NSError(errno: errno))) } else { self.startTimer() } } } } func handleResponse(_ data: Data) { let responseTicks = timeval.uptime() lockQueue.async { guard self.started else { return } // Socket closed. guard data.count == MemoryLayout.size else { return } // Invalid packet length. guard let startTime = self.startTime, let requestTicks = self.requestTicks else { assertionFailure("Uninitialized timestamps") return } let packet = data.withUnsafeBytes { $0.load(as: ntp_packet_t.self) }.nativeEndian let responseTime = startTime.milliseconds + (responseTicks.milliseconds - requestTicks.milliseconds) guard let response = NTPResponse(packet: packet, responseTime: responseTime) else { self.complete(.failure(NSError(trueTimeError: .badServerResponse))) return } self.debugLog("Buffer \(self.address) has read data!") self.debugLog("Start time: \(startTime.milliseconds) ms, " + "response: \(packet.timeDescription)") self.debugLog("Clock offset: \(response.offset) milliseconds") self.debugLog("Round-trip delay: \(response.delay) milliseconds") self.complete(.success(FrozenNetworkTime(time: response.networkDate, uptime: responseTicks, serverResponse: response, startTime: startTime))) } } func requestPacket(_ time: ntp_time_t) -> ntp_packet_t { var packet = ntp_packet_t() packet.client_mode = 3 packet.version_number = 3 packet.transmit_time = time return packet } }