2023-04-03 11:04:31 +08:00

244 lines
7.5 KiB
Objective-C

//
// TDNTPServer.m
// NTPKit
//
// Created by Nico Cvitak on 2016-05-01.
// Copyright © 2016 Nicholas Cvitak. All rights reserved.
//
#import "TDNTPServer.h"
#import "TDNTPTypes.h"
#import "TDLogging.h"
#import <arpa/inet.h>
#import <assert.h>
#import <netdb.h>
#import <sys/time.h>
@implementation TDNTPServer {
NSTimeInterval _timeout;
int _socket;
NSTimeInterval _offset;
}
//! The number of seconds from 1900 to 1970
static const uint32_t kSecondsFrom1900To1970 = 2208988800UL;
//! Returns the local time as an NTP ufixed64 timestamp
static ufixed64_t ntp_localtime_get_ufixed64() {
struct timeval tv;
gettimeofday(&tv, NULL);
return ufixed64((uint32_t)tv.tv_sec + kSecondsFrom1900To1970, tv.tv_usec * (pow(2, 32) / USEC_PER_SEC));
}
+ (TDNTPServer *)defaultServer {
static TDNTPServer *server = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
server = [[TDNTPServer alloc] init];
});
return server;
}
- (instancetype)initWithHostname:(NSString *)hostname port:(NSUInteger)port {
self = [super init];
if (self) {
_hostname = [hostname copy];
_port = port;
_timeout = 3.0;
_socket = -1;
_offset = NAN;
}
return self;
}
- (instancetype)initWithHostname:(NSString *)hostname {
return [self initWithHostname:hostname port:123];
}
- (instancetype)init {
return [self initWithHostname:@"pool.ntp.org"];
}
- (void)dealloc {
[self disconnect];
}
- (void)setTimeout:(NSTimeInterval)timeout {
assert(timeout > 0 && isfinite(timeout));
@synchronized (self) {
_timeout = timeout;
if (_socket >= 0) {
struct timeval tv = { .tv_sec = _timeout, .tv_usec = (_timeout - trunc(_timeout)) * USEC_PER_SEC };
setsockopt(_socket, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
setsockopt(_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
}
}
}
- (NSTimeInterval)timeout {
@synchronized (self) {
return _timeout;
}
}
- (BOOL)isConnected {
@synchronized (self) {
return _socket >= 0;
}
}
- (BOOL)connectWithError:(NSError *__autoreleasing _Nullable *_Nullable)error {
@synchronized (self) {
if (_socket >= 0) {
return YES;
}
// setup hints.
struct addrinfo hints = {0}, *addrinfo = NULL;
hints.ai_family = AF_UNSPEC; // getaddrinfo will resolve (AF_INET or AF_INET6)
hints.ai_socktype = SOCK_DGRAM; // UDP
// get port string.
NSString *port = [[NSString alloc] initWithFormat:@"%lu", (unsigned long) _port];
// get address info.
int getaddrinfo_err = getaddrinfo(_hostname.UTF8String, port.UTF8String, &hints, &addrinfo);
if (getaddrinfo_err != 0) {
if (error) {
NSString *errorDescription = [[NSString alloc] initWithUTF8String:gai_strerror(getaddrinfo_err)];
NSDictionary *errorInfo = [[NSDictionary alloc] initWithObjectsAndKeys:errorDescription, NSLocalizedDescriptionKey, nil];
*error = [NSError errorWithDomain:@"netdb" code:getaddrinfo_err userInfo:errorInfo];
}
return NO;
}
// create the socket.
const int sock = socket(addrinfo->ai_family, addrinfo->ai_socktype, addrinfo->ai_protocol);
if (sock < 0) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
}
freeaddrinfo(addrinfo);
return NO;
}
// set nonblocking.
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK);
// connect async with timeout.
struct timeval timeout = { .tv_sec = _timeout, .tv_usec = (_timeout - trunc(_timeout)) * USEC_PER_SEC };
int connect_err = connect(sock, addrinfo->ai_addr, addrinfo->ai_addrlen) ? errno : 0;
if (connect_err == EINPROGRESS) {
fd_set fd;
FD_ZERO(&fd);
FD_SET(sock, &fd);
const int select_err = select(sock + 1, &fd, &fd, NULL, &timeout);
if (select_err <= 0) {
connect_err = select_err ? errno : ETIMEDOUT;
} else {
socklen_t optlen = sizeof(connect_err);
getsockopt(sock, SOL_SOCKET, SO_ERROR, &connect_err, &optlen);
}
}
freeaddrinfo(addrinfo);
if (connect_err) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:connect_err userInfo:nil];
}
close(sock);
return NO;
}
// set blocking.
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) & ~O_NONBLOCK);
// set timeout.
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
[self willChangeValueForKey:@"connected"];
_socket = sock;
[self didChangeValueForKey:@"connected"];
return YES;
}
}
- (void)disconnect {
@synchronized (self) {
if (_socket >= 0) {
close(_socket);
[self willChangeValueForKey:@"connected"];
_socket = -1;
[self didChangeValueForKey:@"connected"];
}
}
}
- (BOOL)syncWithError:(NSError *__autoreleasing _Nullable *_Nullable)error {
@synchronized (self) {
if (![self connectWithError:error]) {
return NO;
}
// setup NTP packet.
ntp_packet_t packet = {0};
packet.version_number = 4; // NTPv4.
packet.mode = 3; // client mode.
packet.transmit_timestamp = ntp_localtime_get_ufixed64(); // client transmit time.
// convert to network byte order.
packet = hton_ntp_packet(packet);
// send packet to server.
const ssize_t send_s = send(_socket, &packet, sizeof(packet), 0);
const int send_err = send_s == sizeof(packet) ? 0 : send_s >= 0 ? EIO : errno;
if (send_err) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:send_err userInfo:nil];
}
return NO;
}
// receive packet from server.
const ssize_t recv_s = recv(_socket, &packet, sizeof(packet), 0);
const int recv_err = recv_s == sizeof(packet) ? 0 : recv_s >= 0 ? EIO : errno;
if (recv_err) {
if (error) {
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:recv_err userInfo:nil];
}
return NO;
}
// convert to host byte order.
packet = ntoh_ntp_packet(packet);
// calculate and update host delay.
const double T[4] = {
ufixed64_as_double(packet.originate_timestamp), // client transmit time.
ufixed64_as_double(packet.receive_timestamp), // server receive time.
ufixed64_as_double(packet.transmit_timestamp), // server transmit time.
ufixed64_as_double(ntp_localtime_get_ufixed64()), // client receive time.
};
_offset = ((T[1] - T[0]) + (T[2] - T[3])) / 2.0;
TDLogDebug(@"%s %f", __func__, _offset);
return YES;
}
}
- (NSTimeInterval)dateWithError:(NSError *__autoreleasing _Nullable *_Nullable)error {
@synchronized (self) {
return isfinite(_offset) || [self syncWithError:error] ? _offset : 0;
}
}
@end