summaryrefslogtreecommitdiffstats
path: root/nselib/datetime.lua
blob: d6e1cbbde8f7e68afeafbbd37d07abd7ba86f1bd (plain)
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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
--- Functions for dealing with dates and timestamps
--
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
-- @class module
-- @name datetime
-- @author Daniel Miller

local stdnse = require "stdnse"
local os = require "os"
local math = require "math"
local string = require "string"
_ENV = stdnse.module("datetime", stdnse.seeall)

local difftime = os.difftime
local time = os.time
local date = os.date

local floor = math.floor
local fmod = math.fmod

local format = string.format
local match = string.match

--- Record a time difference between the scanner and the target
--
-- The skew will be recorded in the host's registry for later retrieval and
-- analysis. Adjusts for network distance by subtracting half the smoothed
-- round-trip time.
--
--@param host The host being scanned
--@param timestamp The target timestamp, in seconds.
--@param received The local time the stamp was received, in seconds.
function record_skew(host, timestamp, received)
  local skew_tab = host.registry.datetime_skew
  skew_tab = skew_tab or {}
  -- No srtt? I suppose we'll ignore it, but this could cause problems
  local srtt = host.times and host.times.srtt or 0
  local adjusted = difftime(floor(timestamp), floor(received)) - srtt / 2.0
  skew_tab[#skew_tab + 1] = adjusted
  stdnse.debug2("record_skew: %s", adjusted)
  host.registry.datetime_skew = skew_tab
end

-- Work around Windows error formatting time zones where 1970/1/1 UTC was 1969/12/31
local utc_offset_seconds
do
  -- What does the calendar say locally?
  local localtime = date("*t", 86400)
  -- What does the calendar say in UTC?
  local gmtime = date("!*t", 86400)
  -- Interpret both as local calendar dates and find the difference.
  utc_offset_seconds = difftime(time(localtime), time(gmtime))
end

-- The offset in seconds between local time and UTC.
--
-- That is, if we interpret a UTC date table as a local date table by passing
-- it to os.time, how much must be added to the resulting integer timestamp to
-- make it correct?
--
-- In other words, subtract this value from a timestamp if you intend to use it
-- in os.date.
function utc_offset() return utc_offset_seconds end

--- Convert a date table into an integer timestamp.
--
-- Unlike os.time, this does not assume that the date table represents a local
-- time. Rather, it takes an optional offset number of seconds representing the
-- time zone, and returns the timestamp that would result using that time zone
-- as local time. If the offset is omitted or 0, the date table is interpreted
-- as a UTC date. For example, 4:00 UTC is the same as 5:00 UTC+1:
-- <code>
-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0})          --> 14400
-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 0)       --> 14400
-- date_to_timestamp({year=1970,month=1,day=1,hour=5,min=0,sec=0}, 1*60*60) --> 14400
-- </code>
-- And 4:00 UTC+1 is an earlier time:
-- <code>
-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 1*60*60) --> 10800
-- </code>
function date_to_timestamp(date_t, offset)
  local status, tm = pcall(time, date_t)
  if not status then
    stdnse.debug1("Invalid date for this platform: %s", tm)
    return nil
  end
  offset = offset or 0
  return tm + utc_offset() - offset
end

local function format_tz(offset)
  local sign, hh, mm

  if not offset then
    return ""
  end
  if offset < 0 then
    sign = "-"
    offset = -offset
  else
    sign = "+"
  end
  -- Truncate to minutes.
  offset = floor(offset / 60)
  hh = floor(offset / 60)
  mm = floor(fmod(offset, 60))

  return format("%s%02d:%02d", sign, hh, mm)
end
--- Format a date and time (and optional time zone) for structured output.
--
-- Formatting is done according to RFC 3339 (a profile of ISO 8601), except
-- that a time zone may be omitted to signify an unspecified local time zone.
-- Time zones are given as an integer number of seconds from UTC. Use
-- <code>0</code> to mark UTC itself. Formatted strings with a time zone look
-- like this:
-- <code>
-- format_timestamp(os.time(), 0)       --> "2012-09-07T23:37:42+00:00"
-- format_timestamp(os.time(), 2*60*60) --> "2012-09-07T23:37:42+02:00"
-- </code>
-- Without a time zone they look like this:
-- <code>
-- format_timestamp(os.time())          --> "2012-09-07T23:37:42"
-- </code>
--
-- This function should be used for all dates emitted as part of NSE structured
-- output.
function format_timestamp(t, offset)
  if type(t) == "table" then
    return format(
      "%d-%02d-%02dT%02d:%02d:%02d",
      t.year, t.month, t.day, t.hour, t.min, t.sec
      )
  else
    local tz_string = format_tz(offset)
    offset = offset or 0
    local status, result = pcall(date, "!%Y-%m-%dT%H:%M:%S", floor(t + offset))
    if not status then
      local tmp = floor(t + offset)
      local extra_years
      local seconds_in_year = 31556926
      if tmp > 0xffffffff then
        -- Maybe too far in the future?
        extra_years = (tmp  - 0xffffffff) // seconds_in_year + 1
      elseif tmp < -utc_offset() then
        -- Windows can't display times before the epoch
        extra_years = tmp // seconds_in_year
      end
      if extra_years then
        tmp = tmp - extra_years * seconds_in_year
        status, result = pcall(date, "!*t", tmp)
        if status then
          -- seconds_in_year is imprecise, so we truncate to date only
          result = format("%d-%02d-%02d?", result.year + extra_years, result.month, result.day)
        end
      end
    end
    if not status then
      return ("Invalid timestamp: %s"):format(t)
    end
    return result .. tz_string
  end
end

--- Format a time interval into a string
--
-- String is in the same format as format_difftime
-- @param interval A time interval
-- @param unit The time unit division as a number. If <code>interval</code> is
--             in milliseconds, this is 1000 for instance. Default: 1 (seconds)
-- @return The time interval in string format
function format_time(interval, unit)
  local sign = ""
  if interval < 0 then
    sign = "-"
    interval = math.abs(interval)
  end
  unit = unit or 1
  local precision = floor(math.log(unit, 10))

  local sec = (interval % (60 * unit)) / unit
  interval = interval // (60 * unit)
  local min = interval % 60
  interval = interval // 60
  local hr = interval % 24
  interval = interval // 24

  local s = format("%.0fd%02.0fh%02.0fm%02.".. precision .."fs",
    interval, hr, min, sec)
  -- trim off leading 0 and "empty" units
  return sign .. (match(s, "([1-9].*)") or format("%0.".. precision .."fs", 0))
end

--- Format the difference between times <code>t2</code> and <code>t1</code>
-- into a string
--
-- String is in one of the forms (signs may vary):
-- * 0s
-- * -4s
-- * +2m38s
-- * -9h12m34s
-- * +5d17h05m06s
-- * -2y177d10h13m20s
-- The string shows <code>t2</code> relative to <code>t1</code>; i.e., the
-- calculation is <code>t2</code> minus <code>t1</code>.
function format_difftime(t2, t1)
  local d, s, sign, yeardiff

  d = difftime(time(t2), time(t1))
  if d > 0 then
    sign = "+"
  elseif d < 0 then
    sign = "-"
    t2, t1 = t1, t2
    d = -d
  else
    sign = ""
  end
  -- t2 is always later than or equal to t1 here.

  -- The year is a tricky case because it's not a fixed number of days
  -- the way a day is a fixed number of hours or an hour is a fixed
  -- number of minutes. For example, the difference between 2008-02-10
  -- and 2009-02-10 is 366 days because 2008 was a leap year, but it
  -- should be printed as 1y0d0h0m0s, not 1y1d0h0m0s. We advance t1 to be
  -- the latest year such that it is still before t2, which means that its
  -- year will be equal to or one less than t2's. The number of years
  -- skipped is stored in yeardiff.
  if t2.year > t1.year then
    local tmpyear = t1.year
    -- Put t1 in the same year as t2.
    t1.year = t2.year
    d = difftime(time(t2), time(t1))
    if d < 0 then
      -- Too far. Back off one year.
      t1.year = t2.year - 1
      d = difftime(time(t2), time(t1))
    end
    yeardiff = t1.year - tmpyear
    t1.year = tmpyear
  else
    yeardiff = 0
  end

  local s = format_time(d)
  if yeardiff == 0 then return sign .. s end
  -- Years.
  s = format("%dy", yeardiff) .. s
  return sign .. s
end

return _ENV