--- 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:
--
-- 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
--
-- And 4:00 UTC+1 is an earlier time:
--
-- date_to_timestamp({year=1970,month=1,day=1,hour=4,min=0,sec=0}, 1*60*60) --> 10800
--
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
-- 0
to mark UTC itself. Formatted strings with a time zone look
-- like this:
--
-- 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"
--
-- Without a time zone they look like this:
--
-- format_timestamp(os.time()) --> "2012-09-07T23:37:42"
--
--
-- 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 interval
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 t2
and t1
-- into a string
--
-- String is in one of the forms (signs may vary):
-- * 0s
-- * -4s
-- * +2m38s
-- * -9h12m34s
-- * +5d17h05m06s
-- * -2y177d10h13m20s
-- The string shows t2
relative to t1
; i.e., the
-- calculation is t2
minus t1
.
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