diff --git a/lib/common.coffee b/lib/common.coffee index 7edf83c..87e953f 100644 --- a/lib/common.coffee +++ b/lib/common.coffee @@ -18,7 +18,6 @@ ### buffer = require 'buffer' -querystring = require 'querystring' UNDEFINED_TYPE = '[object Undefined]' NULL_TYPE = '[object Null]' @@ -224,14 +223,6 @@ escapeRegExp = (str) -> return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&") -# djb2 hashing algorithm adapted from http://www.cse.yorku.ca/~oz/hash.html -djb2 = (str) -> - hash = 5381 - for i in [0...str.length] - hash = ((hash << 5) + hash) ^ str.charCodeAt(i) - return hash - - exports.UNDEFINED_TYPE = UNDEFINED_TYPE exports.NULL_TYPE = NULL_TYPE exports.NUMBER_TYPE = NUMBER_TYPE @@ -246,4 +237,3 @@ exports.parsePath = parsePath exports.addPathMeta = addPathMeta exports.hammingWeight = hammingWeight exports.escapeRegExp = escapeRegExp -exports.djb2 = djb2 diff --git a/lib/cwmp.coffee b/lib/cwmp.coffee index 09457b3..54411c4 100644 --- a/lib/cwmp.coffee +++ b/lib/cwmp.coffee @@ -31,6 +31,8 @@ cache = require './cache' localCache = require './local-cache' db = require './db' logger = require './logger' +scheduling = require './scheduling' + MAX_CYCLES = 4 @@ -198,28 +200,6 @@ transferComplete = (sessionContext, rpc) -> ) -testSchedule = (schedule, timestamp) -> - range = schedule.schedule.nextRange(10, new Date(timestamp)) - prev = schedule.schedule.prevRange(1, new Date(timestamp), new Date(timestamp - schedule.duration)) - - if prev - first = +(prev[0] ? 0) - last = +(prev[1] ? 0) + schedule.duration - else - first = +(range[0][0] ? 0) - last = +(range[0][1] ? 0) + schedule.duration - - for r in range - if last < r[0] - break - last = +(r[1] ? 0) + schedule.duration - - if timestamp >= first - return [true, Math.max(0, last - timestamp)] - else - return [false, first - timestamp] - - # Append providions and remove duplicates appendProvisions = (original, toAppend) -> modified = false @@ -293,8 +273,13 @@ applyPresets = (sessionContext) -> eventsMatch = false break - continue if (not eventsMatch) or (preset.schedule and not - (preset.schedule.schedule and testSchedule(preset.schedule, sessionContext.timestamp)[0])) + continue if not eventsMatch + + if preset.schedule + continue if not preset.schedule.schedule + r = scheduling.cron(sessionContext.timestamp, preset.schedule.schedule) + if not (r[0] + preset.schedule.duration > sessionContext.timestamp) + continue filteredPresets.push(preset) for k of preset.precondition diff --git a/lib/default-provisions.js b/lib/default-provisions.js index ff63625..1559380 100644 --- a/lib/default-provisions.js +++ b/lib/default-provisions.js @@ -21,6 +21,7 @@ const config = require("./config"); const common = require("./common"); const device = require("./device"); +const scheduling = require("./scheduling"); const MAX_DEPTH = config.get("MAX_DEPTH"); @@ -31,8 +32,8 @@ function refresh(sessionContext, provision, declarations, startRevision, endRevi path.length = MAX_DEPTH; path.fill("*", l); let every = 1000 * (provision[2] || 1); - let offset = Math.abs(common.djb2(sessionContext.deviceId)) % 86400000; - let t = Math.trunc((sessionContext.timestamp + offset) / every) * every - offset; + let offset = scheduling.variance(sessionContext.deviceId, every); + let t = scheduling.interval(sessionContext.timestamp, every, offset); for (let i = l; i < path.length; ++ i) declarations.push([path.slice(0, i), t, {object: 1, writable: 1, value: t}]); diff --git a/lib/local-cache.js b/lib/local-cache.js index b5d1b9b..1992a98 100644 --- a/lib/local-cache.js +++ b/lib/local-cache.js @@ -21,12 +21,11 @@ const vm = require("vm"); const crypto = require("crypto"); -const later = require("later"); - const db = require("./db"); const cache = require("./cache"); const query = require("./query"); const logger = require("./logger"); +const scheduling = require("./scheduling"); const REFRESH = 3000; @@ -125,11 +124,7 @@ function refresh(callback) { try { schedule.duration = +(parts.shift()) * 1000; - if (parts.length === 5) { - parts.unshift("0"); - // TODO later.js doesn't throw erorr if expression is invalid! - schedule.schedule = later.schedule(later.parse.cron(parts.join(" "), true)); - } + schedule.schedule = scheduling.parseCron(parts.join(" ")); } catch (err) { logger.warn({ diff --git a/lib/sandbox.js b/lib/sandbox.js index 571feee..d661c49 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -26,6 +26,7 @@ const common = require("./common"); const device = require("./device"); const extensions = require("./extensions"); const logger = require("./logger"); +const scheduling = require("./scheduling"); // Used for throwing to exit user script and commit @@ -40,15 +41,39 @@ let state; class SandboxDate { - constructor(arg) { + constructor() { if (arguments.length) - return new (Function.prototype.bind.apply(Date, arguments)); + return new Date(...arguments); return new Date(state.sessionContext.timestamp); } - static now() { - return state.sessionContext.timestamp; + static now(intervalOrCron, variance) { + let t = state.sessionContext.timestamp; + + if (typeof intervalOrCron === "number") { + + if (variance == null) + variance = intervalOrCron; + + let offset = 0; + if (variance) + offset = scheduling.variance(state.sessionContext.deviceId, variance); + + t = scheduling.interval(t, intervalOrCron, offset); + } + else if (typeof intervalOrCron === "string") { + let offset = 0; + if (variance) + offset = scheduling.variance(state.sessionContext.deviceId, variance); + let cron = scheduling.parseCron(intervalOrCron); + t = scheduling.cron(t, cron, offset)[0]; + } + else if (intervalOrCron) { + throw new Error("Invalid Date.now() argument"); + } + + return t; } }; diff --git a/lib/scheduling.js b/lib/scheduling.js new file mode 100644 index 0000000..2a4c8aa --- /dev/null +++ b/lib/scheduling.js @@ -0,0 +1,78 @@ +/** + * Copyright 2013-2017 Zaid Abdulla + * + * This file is part of GenieACS. + * + * GenieACS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * GenieACS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with GenieACS. If not, see . + */ +"use strict"; + +const crypto = require("crypto"); + +const later = require("later"); + + +function md532(str) { + const digest = crypto + .createHash("md5") + .update(str) + .digest(); + return ( + digest.readUInt32LE(0) ^ + digest.readUInt32LE(4) ^ + digest.readUInt32LE(8) ^ + digest.readUInt32LE(12) + ); +} + + +function variance(deviceId, variance) { + return (md532(deviceId) >>> 0) % variance; +} + + +function interval(timestamp, interval, offset = 0) { + return Math.trunc((timestamp + offset) / interval) * interval - offset; +} + + +function parseCron(cronExp) { + let parts = cronExp.trim().split(/\s+/); + if (parts.length === 5) + parts.unshift("*"); + + return later.schedule(later.parse.cron(parts.join(" "), true)); +} + + +function cron(timestamp, schedule, offset = 0) { + // TODO later.js doesn't throw erorr if expression is invalid! + const ret = [0, 0]; + + let prev = schedule.prev(1, new Date(timestamp + offset)); + if (prev) + ret[0] = prev.setMilliseconds(0) - offset; + + let next = schedule.next(1, new Date(timestamp + offset + 1000)); + if (next) + ret[1] = next.setMilliseconds(0) - offset; + + return ret; +} + + +exports.variance = variance; +exports.interval = interval; +exports.parseCron = parseCron; +exports.cron = cron;