| 1 |
/* |
| 2 |
* Copyright (c) 2023 Valtteri Koskivuori <vkoskiv@gmail.com> |
| 3 |
* |
| 4 |
* Permission to use, copy, modify, and distribute this software for any |
| 5 |
* purpose with or without fee is hereby granted, provided that the above |
| 6 |
* copyright notice and this permission notice appear in all copies. |
| 7 |
* |
| 8 |
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| 9 |
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| 10 |
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| 11 |
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| 12 |
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| 13 |
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| 14 |
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| 15 |
*/ |
| 16 |
|
| 17 |
#include <OSUtils.h> |
| 18 |
#include "tcp.h" |
| 19 |
#include "dnr.h" |
| 20 |
#include "MacNTP.h" |
| 21 |
|
| 22 |
/* |
| 23 |
TODO: |
| 24 |
rename tcp.c/h to udp.c/h and rename _TCPInit to UDPInit |
| 25 |
*/ |
| 26 |
|
| 27 |
void on_udp_receive(struct UDPiopb *iopb); |
| 28 |
|
| 29 |
// We already have this from dnr.h above, but Think C ignores it. |
| 30 |
OSErr ResolveName(char *name, unsigned long *ipAddress); |
| 31 |
|
| 32 |
/* |
| 33 |
The NTP protocol expresses time as seconds from 01-01-1900. |
| 34 |
The Macintosh expresses them as seconds from 01-01-1904. |
| 35 |
This first step converts the two. |
| 36 |
Seconds between 01-01-1900 and 01-01-1904 (none were leap years): |
| 37 |
4 * 365 * 24 * 60 * 60 = 126144000 |
| 38 |
*/ |
| 39 |
#define NTP_MAC_OFFSET 126144000 |
| 40 |
|
| 41 |
enum MacNTPError MacNTPSetSystemTime(struct ntp_packet *ntp, minutes_t utc_offset) { |
| 42 |
//FIXME: We seem to be behind about 10 seconds after update. |
| 43 |
OSErr err; |
| 44 |
long mac_seconds; |
| 45 |
|
| 46 |
// The Mac clock isn't very precise, and we don't currently even |
| 47 |
// measure the precise time we receive the response, so we forgo |
| 48 |
// the clock filter computations, and trust that the network latency |
| 49 |
// isn't too bad. |
| 50 |
//FIXME: Record receive time and compensate for it if delay exceeds 1s |
| 51 |
long ntp_seconds = ntp->transmit_timestamp.upper; |
| 52 |
ntp_seconds += utc_offset * 60; |
| 53 |
mac_seconds = ntp_seconds - NTP_MAC_OFFSET; |
| 54 |
err = SetDateTime(mac_seconds); |
| 55 |
if (err) { |
| 56 |
if (err == -86) { // clkWrErr |
| 57 |
return ClockWriteFailed; |
| 58 |
} else if (err == -85) { // clkRdErr |
| 59 |
return ClockReadFailed; |
| 60 |
} |
| 61 |
} |
| 62 |
return Success; |
| 63 |
} |
| 64 |
|
| 65 |
enum RequestState { |
| 66 |
AwaitingResponse = 0, |
| 67 |
GotResponse, |
| 68 |
ResponseFailed |
| 69 |
}; |
| 70 |
|
| 71 |
struct ntp_request { |
| 72 |
ip_addr host_ip; |
| 73 |
char *url; |
| 74 |
UDPiopb udp_iopb; |
| 75 |
StreamPtr udp_stream; |
| 76 |
unsigned char *udp_buf; |
| 77 |
unsigned short udp_buf_size; |
| 78 |
wdsEntry udp_wds[2]; |
| 79 |
enum RequestState state; |
| 80 |
|
| 81 |
struct ntp_packet payload; |
| 82 |
}; |
| 83 |
|
| 84 |
void on_udp_receive(struct UDPiopb *iopb) { |
| 85 |
struct ntp_request *req; |
| 86 |
req = (struct ntp_request *)iopb->csParam.receive.userDataPtr; |
| 87 |
if (iopb->csParam.receive.rcvBuffLen != sizeof(struct ntp_packet)) { |
| 88 |
req->state = ResponseFailed; |
| 89 |
} else { |
| 90 |
req->payload = *(struct ntp_packet *)iopb->csParam.receive.rcvBuff; |
| 91 |
} |
| 92 |
|
| 93 |
req->state = GotResponse; |
| 94 |
} |
| 95 |
|
| 96 |
enum MacNTPError MacNTPFetchTime(char *ntp_url, struct ntp_packet *packet, OSErr *supplemental, minutes_t utc_offset) { |
| 97 |
struct ntp_request req; |
| 98 |
OSErr err; |
| 99 |
UDPIOCompletionProc completion; |
| 100 |
long i; |
| 101 |
unsigned long local_transmit_time; |
| 102 |
long retries = 40000; // Arbitrary |
| 103 |
enum MacNTPError current_error = Success; |
| 104 |
// I tried other values, but they all seem to make UDPCreate fail with |
| 105 |
// the undocumented error code -23006 |
| 106 |
unsigned char udp_buf[3000]; |
| 107 |
|
| 108 |
completion = NewUDPIOCompletionProc(on_udp_receive); |
| 109 |
|
| 110 |
if (packet == NULL) return PacketParamNull; |
| 111 |
if (ntp_url == NULL) return InvalidURL; |
| 112 |
if (sizeof(struct ntp_packet) != 48lu) return BadNtpStructSize; |
| 113 |
err = _TCPInit(); |
| 114 |
if (err != 0) { |
| 115 |
if (supplemental) *supplemental = err; |
| 116 |
// error goto tears down UDP, which we don't want here, so return |
| 117 |
return MacTCPInitFailed; |
| 118 |
} |
| 119 |
|
| 120 |
mntp_memset(&req, 0, sizeof(req)); |
| 121 |
req.url = ntp_url; |
| 122 |
|
| 123 |
mntp_memset(&udp_buf, 0, sizeof(udp_buf)); |
| 124 |
req.udp_buf_size = sizeof(udp_buf); |
| 125 |
req.udp_buf = (unsigned char *)&udp_buf; |
| 126 |
err = _UDPCreate(&req.udp_iopb, &req.udp_stream, (Ptr)req.udp_buf, |
| 127 |
req.udp_buf_size, nil, nil, completion, false); |
| 128 |
if (err) { |
| 129 |
if (supplemental) *supplemental = err; |
| 130 |
current_error = UDPCreateFailed; |
| 131 |
goto error; |
| 132 |
} |
| 133 |
err = ResolveName(req.url, &req.host_ip); |
| 134 |
if (err) { |
| 135 |
if (supplemental) *supplemental = err; |
| 136 |
current_error = DNSResolveFailed; |
| 137 |
goto error; |
| 138 |
} |
| 139 |
|
| 140 |
// Set up NTP payload |
| 141 |
mntp_memset(&req.payload, 0, sizeof(req.payload)); |
| 142 |
// Set the mode to tell the server we're a client |
| 143 |
req.payload.li_vn_mode = (4 << 3) | 3; |
| 144 |
|
| 145 |
GetDateTime(&local_transmit_time); |
| 146 |
local_transmit_time += NTP_MAC_OFFSET; |
| 147 |
local_transmit_time -= utc_offset * 60; |
| 148 |
// Copy and verify this matches the response transmit ts |
| 149 |
req.payload.transmit_timestamp.upper = local_transmit_time; |
| 150 |
|
| 151 |
// And prepare for sending |
| 152 |
mntp_memset(&req.udp_wds, 0, sizeof(req.udp_wds)); |
| 153 |
req.udp_wds[0].ptr = (void *)&req.payload; |
| 154 |
req.udp_wds[0].length = sizeof(req.payload); |
| 155 |
|
| 156 |
err = _UDPSend(&req.udp_iopb, req.udp_stream, req.udp_wds, |
| 157 |
req.host_ip, 123, nil, nil, false); |
| 158 |
|
| 159 |
if (err) { |
| 160 |
if (supplemental) *supplemental = err; |
| 161 |
current_error = UDPSendFailed; |
| 162 |
goto error; |
| 163 |
} |
| 164 |
|
| 165 |
req.state = AwaitingResponse; |
| 166 |
|
| 167 |
//FIXME: Only works asynchronously |
| 168 |
err = _UDPRcv(&req.udp_iopb, |
| 169 |
req.udp_stream, |
| 170 |
nil, |
| 171 |
nil, |
| 172 |
req.host_ip, |
| 173 |
123, |
| 174 |
(Ptr)&req, |
| 175 |
completion, |
| 176 |
true); |
| 177 |
|
| 178 |
if (err) { |
| 179 |
if (supplemental) *supplemental = err; |
| 180 |
current_error = UDPRcvFailed; |
| 181 |
goto error; |
| 182 |
} |
| 183 |
|
| 184 |
// Wait for event handler to fire |
| 185 |
//FIXME: This needs a timeout, or we might be able to set it for udp |
| 186 |
while (req.state == AwaitingResponse) { |
| 187 |
if (retries <= 0) { |
| 188 |
current_error = UDPRcvTimedOut; |
| 189 |
goto error; |
| 190 |
} |
| 191 |
retries--; |
| 192 |
} |
| 193 |
if (req.state == ResponseFailed) { |
| 194 |
current_error = InvalidNTPResponse; |
| 195 |
goto error; |
| 196 |
} |
| 197 |
|
| 198 |
// The server copies transmit_timestamp from the packet we sent to |
| 199 |
// origin_timestamp in the response. We verify it here. |
| 200 |
if (req.payload.origin_timestamp.upper != local_transmit_time) { |
| 201 |
current_error = OriginTimestampMismatch; |
| 202 |
goto error; |
| 203 |
} |
| 204 |
|
| 205 |
*packet = req.payload; |
| 206 |
|
| 207 |
error: |
| 208 |
_UDPRelease(&req.udp_iopb, req.udp_stream, nil, nil, false); |
| 209 |
return current_error; |
| 210 |
} |