-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathhttptime
More file actions
executable file
·232 lines (171 loc) · 5.4 KB
/
Copy pathhttptime
File metadata and controls
executable file
·232 lines (171 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#!/usr/bin/env python3
"""Small utility to detect and correct time skew of the system clock
This utility can update your system time based on the Date header of an HTTP
response. This is useful in cases where you need to update your time without an
NTP daemon.
"""
import datetime
import gc
import socket
import sys
import time
addr = ("1.1.1.1", 80)
"""The address of the HTTP server to connect to.
This can be any HTTP server as long as the server time is reasonably accurate.
"""
# The script will display the time skew without any arguments. If the --set
# argument is passed, it sets the system time instead.
_set = "--set" in sys.argv[1:]
def send_request(f):
"""Send a HEAD request to the server.
This function sends a HEAD request to the server. It uses TCP sockets
instead of an HTTP library in order to minimize the number of unnecessary
data.
This helps reduce the data transfer and makes the timing more accurate.
Parameters
----------
f : file
The file object that is bound to the socket.
Returns
-------
None
"""
f.write(b"HEAD /.well-known/time HTTP/1.1\r\n")
f.write(f"Host: {addr[0]}\r\n".encode("ascii"))
f.write(b"User-Agent: httptime\r\n\r\n")
f.flush()
def parse_http_date(date):
"""Parse an HTTP date into a Unix timestamp.
Parameters
----------
date : str
The date to parse (from the HTTP header)
Returns
-------
float
Unix timestamp of the parsed date
"""
fmt = "%a, %d %b %Y %H:%M:%S GMT"
dt = datetime.datetime.strptime(date, fmt)
return dt.replace(tzinfo=datetime.timezone.utc).timestamp()
def get_time(f):
"""Get the current datetime from the server
This function makes an HTTP request to the server, parses the date header
and returns it as a timestamp.
Parameters
----------
f : file
The file object that is bound to the socket
Returns
-------
transmit : float
The timestamp of when the request was sent.
receive : float
The timestamp of when the response was received.
datetime : float
The date header returned by the server parsed into a unix timestamp
"""
tr = time.time()
send_request(f)
dh = ""
for line in f:
line = line.strip()
if line.startswith(b"Date: "):
dh = line[6:].decode("ascii")
# An empty line in a HEAD request means the reponse is completed.
if not line:
rc = time.time()
return tr, rc, parse_http_date(dh)
def get_skew(f, N):
"""Determine the time skew by making N requests to the server.
Usually 4 iterations is enough for an accurate sync and anything above 8
doesn't provide extra accuracy.
Parameters
----------
f : file
The file object that is bound to the socket.
N : int
The number of requests to make.
Returns
-------
float
The time skew in seconds.
Notes
-----
The `Date` header has a one second granularity, but it is possible to get
more sub-second accuracy by making multiple requests and timing them
correctly.
The data has one-second resolution, but we can get more accuracy out of it
if we can determine at which point it is 10:15:12.000 instead of just
10:15:12.
The function maintains two values through the iterations, the lower bound of
the time skew and the upper bound of the time skew.
At the end of each iteration, we determine how much time we need to sleep in
order to align ourselves better with the passing of each second on the
server. The better aligned we are, the smaller the spread between the upper
and lower bounds.
Once the spread is small enough, or we do a certain number of iterations, we
can get the mean value of the upper and lower bounds, and use that as the
skew.
"""
lower = float("-inf")
upper = float("inf")
for i in range(N):
transmit, receive, ts = get_time(f)
_lower = (transmit - 1) - ts
_upper = receive - ts
rtt = receive - transmit
lower = max(_lower, lower)
upper = min(_upper, upper)
dt = 0.5 * (lower + upper) - 0.5 * rtt
if i == N - 1:
return (lower + upper) * 0.5
catch_up = frac(dt - frac(time.time()))
time.sleep(catch_up)
def frac(n):
"""
Return the fractional part of a time value.
Parameters
----------
n : int
The time value
Returns
-------
int
Fractional part of the time value
"""
n = n - int(n)
if n < 0:
n = 1 + n
return n
def set_time(skew):
"""Corrects the time skew by using the settime call.
Parameters
----------
skew : float
The time skew in seconds
Returns
-------
None
Raises
------
PermissionError
If the current user does not have permission to change the system clock.
See Also
--------
time.clock_settime : Sets the system time
"""
new_time = time.time() - skew
time.clock_settime(time.CLOCK_REALTIME, new_time)
if __name__ == "__main__":
gc.disable()
conn = socket.create_connection(addr)
f = conn.makefile("rwb")
skew = get_skew(f, 8)
conn.close()
if _set:
set_time(skew)
elif skew > 0:
print(f"Your time is {skew} seconds ahead")
else:
print(f"Your time is {abs(skew)} seconds behind")