Merge branch 'v1.2'

This commit is contained in:
Zaid Abdulla committed 2019-04-04 13:02:01 -07:00
commit c6db1edc5f
122 files changed
+26076 -2742

No files matched your search

-380
View File
@@ -1,380 +0,0 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
"use strict";
const crypto = require("crypto");
const dgram = require("dgram");
const URL = require("url");
const http = require("http");
const config = require("./config");
const db = require("./db");
const common = require("./common");
const auth = require("./auth");
const cache = require("./cache");
function udpConReq(address, un, key, callback) {
if (!address) return false;
let [host, port] = address.split(":", 2);
if (!port) {
port = 80;
} else {
port = parseInt(port);
if (isNaN(port)) return false;
}
const ts = Math.trunc(Date.now() / 1000);
const id = Math.trunc(Math.random() * 4294967295);
const cn = crypto.randomBytes(8).toString("hex");
const sig = crypto
.createHmac("sha1", key)
.update(`${ts}${id}${un}${cn}`)
.digest("hex");
const uri = `http://${address}?ts=${ts}&id=${id}&un=${un}&cn=${cn}&sig=${sig}`;
const message = Buffer.from(
`GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n`
);
const client = dgram.createSocket({ type: "udp4", reuseAddr: true });
const UDP_CONNECTION_REQUEST_PORT = config.get("UDP_CONNECTION_REQUEST_PORT");
if (UDP_CONNECTION_REQUEST_PORT)
// When a device is NAT'ed, the UDP Connection Request must originate from
// the same address and port used by the STUN server, in order to traverse
// the firewall. This does require that the Genieacs NBI and STUN server
// are allowed to bind to the same address and port. The STUN server needs
// to open its UDP port with the SO_REUSEADDR option, allowing the NBI to
// also bind to the same port.
client.bind({ port: UDP_CONNECTION_REQUEST_PORT, exclusive: true });
let count = 3;
const func = err => {
if (err || --count <= 0) {
client.close();
return void callback(err);
}
client.send(message, 0, message.length, port, host, func);
};
client.send(message, 0, message.length, port, host, func);
return true;
}
function httpConReq(
url,
username,
password,
allowBasicAuth,
timeout,
callback
) {
const options = URL.parse(url);
if (options.protocol !== "http:") {
return void callback(
new Error("Invalid connection request URL or protocol")
);
}
options.agent = new http.Agent({ maxSockets: 1 });
function statusToError(statusCode) {
switch (statusCode) {
case 200:
case 204:
return null;
case 401:
return new Error("Incorrect connection request credentials");
case 0:
return new Error("Device is offline");
default:
return new Error(`Unexpected response code from device: ${statusCode}`);
}
}
const request = http
.get(options, response => {
if (response.statusCode === 401 && response.headers["www-authenticate"]) {
const authHeader = auth.parseAuthHeader(
response.headers["www-authenticate"]
);
if (authHeader.method === "Basic") {
if (!allowBasicAuth) {
request.abort();
return void callback(
new Error("Basic HTTP authentication not allowed")
);
}
options.headers = {
Authorization: auth.basic(username || "", password || "")
};
} else if (authHeader.method === "Digest") {
options.headers = {
Authorization: auth.digest(
username || "",
password || "",
options.path,
"GET",
null,
authHeader
)
};
}
const req = http
.get(options, res => {
if (res.statusCode === 0) {
// Workaround for some devices unexpectedly closing the connection
const req2 = http
.get(options, res2 => {
callback(statusToError(res2.statusCode));
res2.resume();
})
.on("error", () => {
req2.abort();
callback(statusToError(0));
})
.on("socket", socket => {
socket.setTimeout(timeout);
socket.on("timeout", () => {
req2.abort();
});
});
} else {
callback(statusToError(res.statusCode));
}
res.resume();
})
.on("error", () => {
req.abort();
callback(statusToError(0));
})
.on("socket", socket => {
socket.setTimeout(timeout);
socket.on("timeout", () => {
req.abort();
});
});
} else {
callback(statusToError(response.statusCode));
}
// No listener for data so emit resume
response.resume();
})
.on("error", () => {
request.abort();
callback(statusToError(0));
})
.on("socket", socket => {
socket.setTimeout(timeout);
socket.on("timeout", () => {
request.abort();
});
});
}
function connectionRequest(deviceId, callback) {
const CONNECTION_REQUEST_TIMEOUT = config.get(
"CONNECTION_REQUEST_TIMEOUT",
deviceId
);
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = config.get(
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
deviceId
);
const options = {
projection: {
"Device.ManagementServer.ConnectionRequestURL._value": 1,
"Device.ManagementServer.UDPConnectionRequestAddress._value": 1,
"Device.ManagementServer.ConnectionRequestUsername._value": 1,
"Device.ManagementServer.ConnectionRequestPassword._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value": 1,
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1
}
};
db.devicesCollection.findOne({ _id: deviceId }, options, (err, device) => {
if (err) return void callback(err);
if (!device) return void callback(new Error("No such device"));
let managementServer,
connectionRequestUrl,
udpConnectionRequestAddress,
username,
password;
if (device.Device)
// TR-181 data model
managementServer = device.Device.ManagementServer;
// TR-098 data model
else managementServer = device.InternetGatewayDevice.ManagementServer;
if (managementServer.ConnectionRequestURL)
connectionRequestUrl = managementServer.ConnectionRequestURL._value;
if (managementServer.UDPConnectionRequestAddress) {
udpConnectionRequestAddress =
managementServer.UDPConnectionRequestAddress._value;
}
if (managementServer.ConnectionRequestUsername)
username = managementServer.ConnectionRequestUsername._value;
if (managementServer.ConnectionRequestPassword)
password = managementServer.ConnectionRequestPassword._value;
const conReq = () => {
const udpSent = udpConReq(
udpConnectionRequestAddress,
username,
password,
err => {
if (err) throw err;
}
);
httpConReq(
connectionRequestUrl,
username,
password,
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
CONNECTION_REQUEST_TIMEOUT,
err => {
if (udpSent) return void callback();
callback(err);
}
);
};
if (config.auth && config.auth.connectionRequest) {
// Callback is optional for backward compatibility
if (config.auth.connectionRequest.length > 4) {
return void config.auth.connectionRequest(
deviceId,
connectionRequestUrl,
username,
password,
(u, p) => {
username = u;
password = p;
conReq();
}
);
}
[username, password] = config.auth.connectionRequest(
deviceId,
connectionRequestUrl,
username,
password
);
}
conReq();
});
}
function watchTask(deviceId, taskId, timeout, callback) {
setTimeout(() => {
db.tasksCollection.findOne(
{ _id: taskId },
{ projection: { _id: 1 } },
(err, task) => {
if (err) return void callback(err);
if (!task) return void callback(null, "completed");
const q = { _id: `${deviceId}:task_${taskId}` };
db.faultsCollection.findOne(
q,
{ projection: { _id: 1 } },
(err, fault) => {
if (err) return void callback(err);
if (fault) return void callback(null, "fault");
if ((timeout -= 500) <= 0) return void callback(null, "timeout");
watchTask(deviceId, taskId, timeout, callback);
}
);
}
);
}, 500);
}
function sanitizeTask(task, callback) {
task.timestamp = new Date(task.timestamp || Date.now());
if (task.expiry) {
if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry))
task.expiry = new Date(task.expiry);
else task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000);
}
callback(task);
}
function insertTasks(tasks, callback) {
if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE) tasks = [tasks];
else if (!tasks || tasks.length === 0)
return void callback(null, tasks || []);
let counter = tasks.length;
for (const task of tasks) {
sanitizeTask(task, t => {
if (t.uniqueKey) {
db.tasksCollection.deleteOne(
{ device: t.device, uniqueKey: t.uniqueKey },
() => {}
);
}
if (--counter === 0) db.tasksCollection.insertMany(tasks, callback);
});
}
}
function deleteDevice(deviceId, callback) {
db.tasksCollection.deleteMany({ device: deviceId }, err => {
if (err) return void callback(err);
db.devicesCollection.deleteOne({ _id: deviceId }, err => {
if (err) return void callback(err);
db.faultsCollection.deleteMany(
{
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`
}
},
err => {
if (err) return void callback(err);
db.operationsCollection.deleteMany(
{
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`
}
},
err => {
if (err) return void callback(err);
cache.del(`${deviceId}_tasks_faults_operations`, callback);
}
);
}
);
});
});
}
exports.connectionRequest = connectionRequest;
exports.watchTask = watchTask;
exports.insertTasks = insertTasks;
exports.deleteDevice = deleteDevice;
+234
View File
@@ -0,0 +1,234 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
import * as db from "./db";
import * as common from "./common";
import * as cache from "./cache";
import {
getCurrentSnapshot,
getConfig,
getConfigExpression
} from "./local-cache";
import {
httpConnectionRequest,
udpConnectionRequest
} from "./connection-request";
import { Expression } from "./types";
export function connectionRequest(deviceId, callback): void {
const options = {
projection: {
_deviceId: 1,
"Device.ManagementServer.ConnectionRequestURL._value": 1,
"Device.ManagementServer.UDPConnectionRequestAddress._value": 1,
"Device.ManagementServer.ConnectionRequestUsername._value": 1,
"Device.ManagementServer.ConnectionRequestPassword._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value": 1,
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1
}
};
db.devicesCollection.findOne({ _id: deviceId }, options, (err, device) => {
if (err) return void callback(err);
if (!device) return void callback(new Error("No such device"));
let managementServer,
connectionRequestUrl,
udpConnectionRequestAddress,
username,
password;
if (device.Device)
// TR-181 data model
managementServer = device.Device.ManagementServer;
// TR-098 data model
else managementServer = device.InternetGatewayDevice.ManagementServer;
if (managementServer.ConnectionRequestURL)
connectionRequestUrl = managementServer.ConnectionRequestURL._value;
if (managementServer.UDPConnectionRequestAddress) {
udpConnectionRequestAddress =
managementServer.UDPConnectionRequestAddress._value;
}
if (managementServer.ConnectionRequestUsername)
username = managementServer.ConnectionRequestUsername._value;
if (managementServer.ConnectionRequestPassword)
password = managementServer.ConnectionRequestPassword._value;
const context = {
id: device["_id"],
serialNumber: device["_deviceId"]["SerialNumber"],
productClass: device["_deviceId"]["ProductClass"],
oui: device["_deviceId"]["OUI"],
username: username || "",
password: password || ""
};
getCurrentSnapshot()
.then(snapshot => {
const now = Date.now();
const UDP_CONNECTION_REQUEST_PORT = +getConfig(
snapshot,
"cwmp.udpConnectionRequestAuth",
context,
now
);
const CONNECTION_REQUEST_TIMEOUT = +getConfig(
snapshot,
"cwmp.connectionRequestTimeout",
context,
now
);
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig(
snapshot,
"cwmp.connectionRequestAllowBasicAuth",
context,
now
);
let authExp: Expression = getConfigExpression(
snapshot,
"cwmp.connectionRequestAuth"
);
if (authExp === undefined) {
authExp = [
"FUNC",
"AUTH",
["PARAM", "username"],
["PARAM", "password"]
] as Expression;
}
let udpProm;
if (udpConnectionRequestAddress) {
udpProm = udpConnectionRequest(
udpConnectionRequestAddress,
authExp,
db.configCollection,
UDP_CONNECTION_REQUEST_PORT
);
}
httpConnectionRequest(
connectionRequestUrl,
authExp,
context,
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
CONNECTION_REQUEST_TIMEOUT
)
.then(callback)
.catch(err => {
if (udpProm) return void udpProm.then(callback).catch(callback);
callback(err);
});
})
.catch(callback);
});
}
export function watchTask(deviceId, taskId, timeout, callback): void {
setTimeout(() => {
db.tasksCollection.findOne(
{ _id: taskId },
{ projection: { _id: 1 } },
(err, task) => {
if (err) return void callback(err);
if (!task) return void callback(null, "completed");
const q = { _id: `${deviceId}:task_${taskId}` };
db.faultsCollection.findOne(
q,
{ projection: { _id: 1 } },
(err, fault) => {
if (err) return void callback(err);
if (fault) return void callback(null, "fault");
if ((timeout -= 500) <= 0) return void callback(null, "timeout");
watchTask(deviceId, taskId, timeout, callback);
}
);
}
);
}, 500);
}
function sanitizeTask(task, callback): void {
task.timestamp = new Date(task.timestamp || Date.now());
if (task.expiry) {
if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry))
task.expiry = new Date(task.expiry);
else task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000);
}
callback(task);
}
export function insertTasks(tasks, callback): void {
if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE) tasks = [tasks];
else if (!tasks || tasks.length === 0)
return void callback(null, tasks || []);
let counter = tasks.length;
for (const task of tasks) {
sanitizeTask(task, t => {
if (t.uniqueKey) {
db.tasksCollection.deleteOne(
{ device: t.device, uniqueKey: t.uniqueKey },
() => {}
);
}
if (--counter === 0) db.tasksCollection.insertMany(tasks, callback);
});
}
}
export function deleteDevice(deviceId, callback): void {
db.tasksCollection.deleteMany({ device: deviceId }, err => {
if (err) return void callback(err);
db.devicesCollection.deleteOne({ _id: deviceId }, err => {
if (err) return void callback(err);
db.faultsCollection.deleteMany(
{
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`
}
},
err => {
if (err) return void callback(err);
db.operationsCollection.deleteMany(
{
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`
}
},
err => {
if (err) return void callback(err);
cache.del(`${deviceId}_tasks_faults_operations`, callback);
}
);
}
);
});
});
}
+35 -15
View File
@@ -16,11 +16,10 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const crypto = require("crypto");
import { createHash, randomBytes, pbkdf2 } from "crypto";
function parseAuthHeader(authHeader) {
export function parseAuthHeader(authHeader): {} {
authHeader = authHeader.trim();
const method = authHeader.split(" ", 1)[0];
const res = { method: method };
@@ -53,11 +52,18 @@ function parseAuthHeader(authHeader) {
return res;
}
function basic(username, password) {
export function basic(username, password): string {
return "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
}
function digest(username, password, uri, httpMethod, body, authHeader) {
export function digest(
username,
password,
uri,
httpMethod,
body,
authHeader
): string {
const cnonce = "0a4f113b";
const nc = "00000001";
let qop;
@@ -68,7 +74,7 @@ function digest(username, password, uri, httpMethod, body, authHeader) {
else qop = authHeader.qop;
}
let ha1 = crypto.createHash("md5");
const ha1 = createHash("md5");
ha1
.update(username)
.update(":")
@@ -76,19 +82,19 @@ function digest(username, password, uri, httpMethod, body, authHeader) {
.update(":")
.update(password);
// TODO support "MD5-sess" algorithm directive
ha1 = ha1.digest("hex");
const ha1d = ha1.digest("hex");
let ha2 = crypto.createHash("md5");
const ha2 = createHash("md5");
ha2
.update(httpMethod)
.update(":")
.update(uri);
if (qop === "auth-int") ha2.update(":").update(body);
ha2 = ha2.digest("hex");
const ha2d = ha2.digest("hex");
const hash = crypto.createHash("md5");
const hash = createHash("md5");
hash
.update(ha1)
.update(ha1d)
.update(":")
.update(authHeader.nonce);
if (qop) {
@@ -100,7 +106,7 @@ function digest(username, password, uri, httpMethod, body, authHeader) {
.update(":")
.update(qop);
}
hash.update(":").update(ha2);
hash.update(":").update(ha2d);
let authString = `Digest username="${username}"`;
authString += `,realm="${authHeader.realm}"`;
@@ -114,6 +120,20 @@ function digest(username, password, uri, httpMethod, body, authHeader) {
return authString;
}
exports.parseAuthHeader = parseAuthHeader;
exports.basic = basic;
exports.digest = digest;
export function generateSalt(length): Promise<string> {
return new Promise((resolve, reject) => {
randomBytes(length, (err, rand) => {
if (err) return void reject(err);
resolve(rand.toString("hex"));
});
});
}
export function hashPassword(pass, salt): Promise<string> {
return new Promise((resolve, reject) => {
pbkdf2(pass, salt, 10000, 128, "sha512", (err, hash) => {
if (err) return void reject(err);
resolve(hash.toString("hex"));
});
});
}
-251
View File
@@ -1,251 +0,0 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
"use strict";
const config = require("./config");
const MAX_CACHE_TTL = config.get("MAX_CACHE_TTL");
let redisClient;
let mongoClient;
let mongoCollection;
let mongoTimeOffset = 0;
function connect(callback) {
const REDIS_HOST = config.get("REDIS_HOST");
if (REDIS_HOST) {
const redis = require("redis");
const REDIS_PORT = config.get("REDIS_PORT");
const REDIS_DB = config.get("REDIS_DB");
redisClient = redis.createClient(REDIS_PORT, REDIS_HOST);
redisClient.select(REDIS_DB, err => {
exports.get = redisGet;
exports.set = redisSet;
exports.del = redisDel;
exports.pop = redisPop;
exports.lock = redisLock;
callback(err);
});
} else {
const mongodb = require("mongodb");
const MONGODB_CONNECTION_URL = config.get("MONGODB_CONNECTION_URL");
mongodb.MongoClient.connect(
MONGODB_CONNECTION_URL,
{ useNewUrlParser: true },
(err, client) => {
if (err) return void callback(err);
const db = client.db();
mongoClient = client;
mongoCollection = db.collection("cache");
mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
const now = Date.now();
db.command({ hostInfo: 1 }, (err, res) => {
if (err) return void callback(err);
mongoTimeOffset = res.system.currentTime.getTime() - now;
exports.get = mongoGet;
exports.set = mongoSet;
exports.del = mongoDel;
exports.pop = mongoPop;
exports.lock = mongoLock;
callback();
});
}
);
}
}
function disconnect() {
if (redisClient) redisClient.quit();
if (mongoClient) mongoClient.close();
}
function redisGet(key, callback) {
if (Array.isArray(key)) redisClient.mget(key, callback);
else redisClient.get(key, callback);
}
function redisDel(key, callback) {
redisClient.del(key, callback);
}
function redisSet(key, value, ttl, callback) {
if (!callback && typeof v === "function") {
callback = ttl;
ttl = null;
}
if (!ttl) ttl = MAX_CACHE_TTL;
redisClient.setex(key, ttl, value, callback);
}
function redisPop(key, callback) {
const POP_SCRIPT =
'local v=redis.call("get",KEYS[1]);redis.call("del",KEYS[1]);return v;';
redisClient.eval(POP_SCRIPT, 1, key, callback);
}
function redisLock(lockName, ttl, callback) {
const UNLOCK_SCRIPT =
'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end';
const EXTEND_SCRIPT =
'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end';
const token = Math.random()
.toString(36)
.slice(2);
function unlockOrExtend(extendTtl) {
if (!extendTtl) {
redisClient.eval(UNLOCK_SCRIPT, 1, lockName, token, (err, res) => {
if (err || !res) throw err || new Error("Lock expired");
});
} else {
redisClient.eval(
EXTEND_SCRIPT,
1,
lockName,
token,
extendTtl,
(err, res) => {
if (err || !res) throw err || new Error("Lock expired");
}
);
}
}
redisClient.set(lockName, token, "NX", "EX", ttl, (err, res) => {
if (err || res) return void callback(err, unlockOrExtend);
setTimeout(() => {
redisLock(lockName, ttl, callback);
}, 200);
});
}
function mongoGet(key, callback) {
const expire = new Date(Date.now() - mongoTimeOffset);
if (Array.isArray(key)) {
mongoCollection.find({ _id: { $in: key } }).toArray((err, res) => {
if (err) return void callback(err);
const indices = {};
key.forEach((v, i) => {
indices[v] = i;
});
const values = [];
res.forEach(r => {
if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"];
});
callback(null, values);
});
} else {
mongoCollection.findOne({ _id: { $in: [key] } }, (err, res) => {
if (err || !res) return void callback(err);
if (res["expire"] > expire) return void callback(null, res["value"]);
callback();
});
}
}
function mongoDel(key, callback) {
if (Array.isArray(key))
mongoCollection.deleteMany({ _id: { $in: key } }, callback);
else mongoCollection.deleteOne({ _id: key }, callback);
}
function mongoSet(key, value, ttl, callback) {
if (!callback && typeof v === "function") {
callback = ttl;
ttl = null;
}
if (!ttl) ttl = MAX_CACHE_TTL;
const expire = new Date(Date.now() - mongoTimeOffset + ttl * 1000);
mongoCollection.replaceOne(
{ _id: key },
{ _id: key, value: value, expire: expire },
{ upsert: true },
callback
);
}
function mongoPop(key, callback) {
mongoCollection.findOneAndDelete({ _id: key }, (err, res) => {
if (err || !res["value"]) return void callback(err);
if (res["value"]["expire"] > new Date(Date.now() - mongoTimeOffset))
return void callback(null, res["value"]["value"]);
callback();
});
}
function mongoLock(lockName, ttl, callback) {
const token = Math.random()
.toString(36)
.slice(2);
function unlockOrExtend(extendTtl) {
if (!extendTtl) {
mongoCollection.deleteOne({ _id: lockName, value: token }, (err, res) => {
if (err || res["result"]["n"] !== 1)
throw err || new Error("Lock expired");
});
} else {
const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000);
mongoCollection.updateOne(
{ _id: lockName, value: token },
{ expire: expire },
(err, res) => {
if (err || res["result"]["n"] !== 1)
throw err | new Error("Lock expired");
}
);
}
}
const expireTest = new Date(Date.now() - mongoTimeOffset);
const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000);
mongoCollection.updateOne(
{ _id: lockName, expire: { $lte: expireTest } },
{ $set: { value: token, expire: expireSet } },
{ upsert: true },
err => {
if (err && err.code === 11000) {
return setTimeout(() => {
mongoLock(lockName, ttl, callback);
}, 200);
}
return callback(err, unlockOrExtend);
}
);
}
exports.connect = connect;
exports.disconnect = disconnect;
+178
View File
@@ -0,0 +1,178 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
import { MongoClient } from "mongodb";
import * as config from "./config";
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
let mongoClient;
let mongoCollection;
let mongoTimeOffset = 0;
export function connect(callback): void {
const MONGODB_CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
MongoClient.connect(
MONGODB_CONNECTION_URL,
{ useNewUrlParser: true },
(err, client) => {
if (err) return void callback(err);
const db = client.db();
mongoClient = client;
mongoCollection = db.collection("cache");
mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
const now = Date.now();
db.command({ hostInfo: 1 }, (err, res) => {
if (err) return void callback(err);
mongoTimeOffset = res.system.currentTime.getTime() - now;
callback();
});
}
);
}
export function disconnect(): void {
if (mongoClient) mongoClient.close();
}
export function get(key, callback): void {
const expire = new Date(Date.now() - mongoTimeOffset);
if (Array.isArray(key)) {
mongoCollection.find({ _id: { $in: key } }).toArray((err, res) => {
if (err) return void callback(err);
const indices = {};
key.forEach((v, i) => {
indices[v] = i;
});
const values = [];
res.forEach(r => {
if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"];
});
callback(null, values);
});
} else {
mongoCollection.findOne({ _id: { $in: [key] } }, (err, res) => {
if (err || !res) return void callback(err);
if (res["expire"] > expire) return void callback(null, res["value"]);
callback();
});
}
}
export function del(key, callback): void {
if (Array.isArray(key))
mongoCollection.deleteMany({ _id: { $in: key } }, callback);
else mongoCollection.deleteOne({ _id: key }, callback);
}
export function set(
key: string,
value: string | number,
callback: (err?: Error) => void
): void;
export function set(
key: string,
value: string | number,
ttl: number,
callback: (err?: Error) => void
): void;
export function set(
key: string,
value: string | number,
ttl: number | ((err?: Error) => void),
callback?: (err?: Error) => void
): void {
if (!callback && typeof ttl === "function") {
callback = ttl;
ttl = null;
}
if (!ttl) ttl = MAX_CACHE_TTL;
const expire = new Date(
Date.now() - mongoTimeOffset + (ttl as number) * 1000
);
mongoCollection.replaceOne(
{ _id: key },
{ _id: key, value: value, expire: expire },
{ upsert: true },
callback
);
}
export function pop(key, callback): void {
mongoCollection.findOneAndDelete({ _id: key }, (err, res) => {
if (err || !res["value"]) return void callback(err);
if (res["value"]["expire"] > new Date(Date.now() - mongoTimeOffset))
return void callback(null, res["value"]["value"]);
callback();
});
}
export function lock(lockName, ttl, callback): void {
const token = Math.random()
.toString(36)
.slice(2);
function unlockOrExtend(extendTtl): void {
if (!extendTtl) {
mongoCollection.deleteOne({ _id: lockName, value: token }, (err, res) => {
if (err || res["result"]["n"] !== 1)
throw err || new Error("Lock expired");
});
} else {
const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000);
mongoCollection.updateOne(
{ _id: lockName, value: token },
{ expire: expire },
(err, res) => {
if (err || res["result"]["n"] !== 1)
throw err || new Error("Lock expired");
}
);
}
}
const expireTest = new Date(Date.now() - mongoTimeOffset);
const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000);
mongoCollection.updateOne(
{ _id: lockName, expire: { $lte: expireTest } },
{ $set: { value: token, expire: expireSet } },
{ upsert: true },
err => {
if (err && err.code === 11000) {
return setTimeout(() => {
lock(lockName, ttl, callback);
}, 200);
}
return callback(err, unlockOrExtend);
}
);
}
-171
View File
@@ -1,171 +0,0 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
"use strict";
const cluster = require("cluster");
const logger = require("./logger");
const config = require("./config");
const VERSION = require("../package.json").version;
let respawnTimestamp = 0;
let tooManyCrashesTimestamp = 0;
function getDependencyVersions() {
const dependencies = [`node@${process.versions.node}`];
try {
const npmls = JSON.parse(
require("child_process").execSync("npm ls --prod --json --depth 0")
);
for (const k in npmls.dependencies)
dependencies.push(`${k}@${npmls.dependencies[k].version}`);
} catch (err) {
// Continue regardless of error
}
return dependencies.join(",");
}
function getConfig() {
const conf = [];
const keys = Object.keys(process.env);
for (const k of keys) {
if (k.startsWith("GENIEACS_")) {
const name = k.slice(9);
const value = process.env[k];
const def = config.getDefault(name);
if (def == null || value !== `${def}`) conf.push(`${name}=${value}`);
}
}
return conf.join(",");
}
function restartWorker(worker, code, signal) {
const msg = {
message: "Worker died",
pid: worker.process.pid
};
if (code != null) msg.exitCode = code;
if (signal != null) msg.signal = signal;
logger.error(msg);
const now = Date.now();
respawnTimestamp = Math.max(now, respawnTimestamp + 2000);
if (respawnTimestamp === now) {
tooManyCrashesTimestamp = now;
cluster.fork();
return;
}
if (now - tooManyCrashesTimestamp > 60000) {
process.exitCode = 1;
cluster.removeListener("exit", restartWorker);
for (const pid in cluster.workers) cluster.workers[pid].kill();
logger.error({
message: "Too many crashes, exiting",
pid: process.pid
});
return;
}
setTimeout(() => {
if (process.exitCode) return;
cluster.fork();
}, respawnTimestamp - now);
}
function start(service) {
logger.init(service, VERSION);
const SERVICE_ADDRESS = config.get(`${service.toUpperCase()}_INTERFACE`);
const SERVICE_PORT = config.get(`${service.toUpperCase()}_PORT`);
logger.info({
message: `GenieACS (genieacs-${service}) starting`,
pid: process.pid,
version: VERSION,
dependencies: getDependencyVersions(),
config: getConfig()
});
cluster.on("listening", (worker, address) => {
if (
(address.addressType === 4 || address.addressType === 6) &&
address.address === SERVICE_ADDRESS &&
address.port === SERVICE_PORT
) {
logger.info({
message: "Worker listening",
pid: worker.process.pid,
address: address.address,
port: address.port
});
}
});
cluster.on("exit", restartWorker);
process.on("SIGINT", () => {
logger.info({
message: "Received signal SIGINT, exiting",
pid: process.pid
});
cluster.removeListener("exit", restartWorker);
});
process.on("SIGTERM", () => {
logger.info({
message: "Received signal SIGTERM, exiting",
pid: process.pid
});
cluster.removeListener("exit", restartWorker);
for (const pid in cluster.workers) cluster.workers[pid].kill();
});
cluster.setupMaster({
exec: "lib/server",
args: [service]
});
let workerCount = config.get(`${service.toUpperCase()}_WORKER_PROCESSES`);
if (!workerCount) workerCount = Math.max(2, require("os").cpus().length);
for (let i = 0; i < workerCount; ++i) cluster.fork();
}
process.on("uncaughtException", err => {
logger.error({
message: "Uncaught exception in master process, exiting",
exception: err,
pid: process.pid
});
process.exitCode = 1;
cluster.removeListener("exit", restartWorker);
for (const pid in cluster.workers) cluster.workers[pid].kill();
});
exports.start = start;
+95
View File
@@ -0,0 +1,95 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
import cluster from "cluster";
import { cpus } from "os";
import * as logger from "./logger";
let respawnTimestamp = 0;
let tooManyCrashesTimestamp = 0;
function restartWorker(worker, code, signal): void {
const msg = {
message: "Worker died",
pid: worker.process.pid,
exitCode: null,
signal: null
};
if (code != null) msg.exitCode = code;
if (signal != null) msg.signal = signal;
logger.error(msg);
const now = Date.now();
respawnTimestamp = Math.max(now, respawnTimestamp + 2000);
if (respawnTimestamp === now) {
tooManyCrashesTimestamp = now;
cluster.fork();
return;
}
if (now - tooManyCrashesTimestamp > 60000) {
process.exitCode = 1;
cluster.removeListener("exit", restartWorker);
for (const pid in cluster.workers) cluster.workers[pid].kill();
logger.error({
message: "Too many crashes, exiting",
pid: process.pid
});
return;
}
setTimeout(() => {
if (process.exitCode) return;
cluster.fork();
}, respawnTimestamp - now);
}
export function start(workerCount, servicePort, serviceAddress): void {
cluster.on("listening", (worker, address) => {
if (
(address.addressType === 4 || address.addressType === 6) &&
address.address === serviceAddress &&
address.port === servicePort
) {
logger.info({
message: "Worker listening",
pid: worker.process.pid,
address: address.address,
port: address.port
});
}
});
cluster.on("exit", restartWorker);
if (!workerCount) workerCount = Math.max(2, cpus().length);
for (let i = 0; i < workerCount; ++i) cluster.fork();
}
export function stop(): void {
cluster.removeListener("exit", restartWorker);
for (const pid in cluster.workers) cluster.workers[pid].kill();
}
export const worker = cluster.worker;
-204
View File
@@ -1,204 +0,0 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
"use strict";
const UNDEFINED_TYPE = "[object Undefined]";
const NULL_TYPE = "[object Null]";
const BOOLEAN_TYPE = "[object Boolean]";
const NUMBER_TYPE = "[object Number]";
const STRING_TYPE = "[object String]";
const ARRAY_TYPE = "[object Array]";
const OBJECT_TYPE = "[object Object]";
const REGEXP_TYPE = "[object RegExp]";
const DATE_TYPE = "[object Date]";
const typeOf = obj => Object.prototype.toString.call(obj);
function generateDeviceId(deviceIdStruct) {
// Escapes everything except alphanumerics and underscore
function esc(str) {
return str.replace(/[^A-Za-z0-9_]/g, chr => {
const buf = Buffer.from(chr, "utf8");
let rep = "";
for (const b of buf) rep += "%" + b.toString(16).toUpperCase();
return rep;
});
}
// Guaranteeing globally unique id as defined in TR-069
if (deviceIdStruct["ProductClass"]) {
return (
esc(deviceIdStruct["OUI"]) +
"-" +
esc(deviceIdStruct["ProductClass"]) +
"-" +
esc(deviceIdStruct["SerialNumber"])
);
}
return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]);
}
function parseAlias(pattern, start, res) {
const aliases = [];
let i = start;
while (i < pattern.length && pattern[i] !== "]") {
const alias = [];
let j = (i = parsePath(pattern, i, alias) + 1);
while (pattern[j] !== "]" && pattern[j] !== ",") {
if (pattern[j] === '"' && i === j) {
++j;
while (pattern[j] !== '"' || pattern[j - 1] === "\\") {
if (++j >= pattern.length)
throw new Error("Invalid alias expression");
}
}
if (++j >= pattern.length) throw new Error("Invalid alias expression");
}
let value = pattern.slice(i, j).trim();
i = j;
if (value[0] === '"') {
try {
value = JSON.parse(value);
} catch (error) {
throw new Error("Invalid alias expression");
}
}
alias.push(value);
aliases.push(alias);
if (pattern[i] === ",") ++i;
}
// Sort to ensure identical expressions have idential string representation
function srt(a, b) {
const jMax = Math.min(a.length, b.length);
for (let j = 0; j < jMax; j += 2) {
const kMax = Math.min(a[j].length, b[j].length);
for (let k = 0; k < kMax; ++k) {
if (Array.isArray(a[j][k])) {
if (Array.isArray(b[j][k])) return srt(a[j][k], b[j][k]);
else if (b[j][k] == null) return -1;
else return 1;
} else if (a[j][k] == null) {
if (b[j][k] == null) return 0;
else return 1;
} else if (b[j][k] == null || Array.isArray(b[j][k])) {
return -1;
} else if (a[j][k] > b[j][k]) {
return 1;
} else if (a[j][k] < b[j][k]) {
return -1;
}
}
if (a[j].length > b[j].length) return -1;
else if (a[j].length < b[j].length) return 1;
if (a[j + 1] > b[j + 1]) return -1;
else if (a[j + 1] < b[j + 1]) return 1;
}
if (a.length > b.length) return -1;
else if (a.length < b.length) return 1;
return 0;
}
aliases.sort(srt);
res.push([].concat.apply([], aliases));
return i;
}
function parsePath(pattern, start, res) {
const path = [];
path.wildcard = 0;
path.alias = 0;
let i = start || 0;
// Colon separator is needed for parseAlias
if (i < pattern.length && pattern[i] !== ":") {
for (;;) {
if (pattern[i] === "[") {
path.alias |= 1 << path.length;
i = parseAlias(pattern, i + 1, path) + 1;
} else {
const j = i;
while (i < pattern.length && pattern[i] !== ":" && pattern[i] !== ".")
++i;
const n = pattern.slice(j, i);
if (n === "*") path.wildcard |= 1 << path.length;
path.push(n);
}
if (i >= pattern.length || pattern[i] === ":") break;
else if (pattern[i] !== ".") throw new Error("Invalid alias expression");
++i;
}
}
Object.freeze(path);
if (res == null) return path;
res.push(path);
return i;
}
function addPathMeta(path) {
if (path.alias != null && path.wildcard != null) return path;
path.alias = 0;
path.wildcard = 0;
for (const [i, p] of path.entries()) {
if (Array.isArray(p)) {
path.alias |= 1 << i;
for (let j = 0; j < p.length; j += 2) addPathMeta(p[j]);
} else if (p === "*") {
path.wildcard |= 1 << i;
}
}
Object.freeze(path);
return path;
}
// Source: http://stackoverflow.com/a/6969486
function escapeRegExp(str) {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
exports.UNDEFINED_TYPE = UNDEFINED_TYPE;
exports.NULL_TYPE = NULL_TYPE;
exports.BOOLEAN_TYPE = BOOLEAN_TYPE;
exports.NUMBER_TYPE = NUMBER_TYPE;
exports.STRING_TYPE = STRING_TYPE;
exports.ARRAY_TYPE = ARRAY_TYPE;
exports.OBJECT_TYPE = OBJECT_TYPE;
exports.DATE_TYPE = DATE_TYPE;
exports.REGEXP_TYPE = REGEXP_TYPE;
exports.typeOf = typeOf;
exports.generateDeviceId = generateDeviceId;
exports.parsePath = parsePath;
exports.addPathMeta = addPathMeta;
exports.escapeRegExp = escapeRegExp;
+59
View File
@@ -0,0 +1,59 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
export const UNDEFINED_TYPE = "[object Undefined]";
export const NULL_TYPE = "[object Null]";
export const BOOLEAN_TYPE = "[object Boolean]";
export const NUMBER_TYPE = "[object Number]";
export const STRING_TYPE = "[object String]";
export const ARRAY_TYPE = "[object Array]";
export const OBJECT_TYPE = "[object Object]";
export const REGEXP_TYPE = "[object RegExp]";
export const DATE_TYPE = "[object Date]";
export const typeOf = (obj): string => Object.prototype.toString.call(obj);
export function generateDeviceId(deviceIdStruct): string {
// Escapes everything except alphanumerics and underscore
function esc(str): string {
return str.replace(/[^A-Za-z0-9_]/g, chr => {
const buf = Buffer.from(chr, "utf8");
let rep = "";
for (const b of buf) rep += "%" + b.toString(16).toUpperCase();
return rep;
});
}
// Guaranteeing globally unique id as defined in TR-069
if (deviceIdStruct["ProductClass"]) {
return (
esc(deviceIdStruct["OUI"]) +
"-" +
esc(deviceIdStruct["ProductClass"]) +
"-" +
esc(deviceIdStruct["SerialNumber"])
);
}
return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]);
}
// Source: http://stackoverflow.com/a/6969486
export function escapeRegExp(str): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
+93
View File
@@ -0,0 +1,93 @@
import pass from "./validators/pass";
import test from "./validators/test";
import { PermissionSet } from "../types";
export const validators = { pass, test };
export default class Authorizer {
private permissionSets: PermissionSet[];
private validatorCache: WeakMap<
object,
(mutationType, mutation, any) => boolean
>;
private hasAccessCache: Map<string, boolean>;
public constructor(permissionSets) {
this.permissionSets = permissionSets;
this.validatorCache = new WeakMap();
this.hasAccessCache = new Map();
}
public hasAccess(resourceType, access): boolean {
const cacheKey = `${resourceType}-${access}`;
if (this.hasAccessCache.has(cacheKey))
return this.hasAccessCache.get(cacheKey);
let has = false;
for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) {
if (perm[resourceType]) {
if (perm[resourceType].access >= access) {
has = true;
break;
}
}
}
}
this.hasAccessCache.set(cacheKey, has);
return has;
}
public getValidator(
resourceType,
resource
): (mutationType: string, mutation: any, args: any) => boolean {
if (this.validatorCache.has(resource))
return this.validatorCache.get(resource);
let funcs = {};
for (const permissionSet of this.permissionSets) {
for (const perm of permissionSet) {
if (perm[resourceType]) {
if (perm[resourceType].access >= 3) {
if (perm[resourceType].validate) {
for (const [k, v] of Object.entries(perm[resourceType].validate))
funcs[k] = v;
}
} else {
funcs = {};
}
}
}
}
const validator = (
mutationType: string,
mutation: any,
any: any
): boolean => {
let valid = false;
for (const [k, v] of Object.entries(funcs)) {
if (v) {
const res = validators[k](
resourceType,
resource,
mutationType,
mutation,
any
);
if (res > 0) valid = true;
else if (res < 0) return false;
}
}
return valid;
};
this.validatorCache.set(resource, validator);
return validator;
}
}
+266
View File
@@ -0,0 +1,266 @@
import { map, parseLikePattern } from "./expression-parser";
import { Expression } from "../types";
type CNF = number[][];
function or(cnf1: CNF, cnf2: CNF): CNF {
if (cnf1.length === 0) return cnf2;
else if (cnf2.length === 0) return cnf1;
if (cnf1.length > cnf2.length) {
const c = cnf1;
cnf1 = cnf2;
cnf2 = c;
}
const res = [];
for (let i = 0; i < cnf1.length; ++i)
for (let j = i; j < cnf2.length; ++j) res.push(cnf1[i].concat(cnf2[j]));
return res;
}
function likePatternIncludes(pat1, pat2, idx1 = 0, idx2 = 0): boolean {
while (idx1 < pat1.length && idx2 < pat2.length) {
if (pat1[idx1] === "\\%") {
for (let i = idx2; i <= pat2.length; ++i)
if (likePatternIncludes(pat1, pat2, idx1 + 1, i)) return true;
return false;
} else if (pat2[idx2] === "\\%") {
if (pat1[idx1] !== "\\%") return false;
} else if (pat1[idx1] === "\\_") {
// Ignore
} else if (pat2[idx2] === "\\_") {
return false;
} else if (pat2[idx2] !== pat1[idx1]) {
return false;
}
++idx1;
++idx2;
}
if (idx1 === pat1.length && idx2 === pat2.length) return true;
return false;
}
export function booleanCnf(exp: Expression): { vars: number; clauses: CNF } {
const ranges = new Map();
const likePatterns = new Map();
map(exp, e => {
if (!Array.isArray(e)) return e;
if (e[0] === "LIKE" || e[0] === "NOT LIKE") {
if (
typeof e[2] === "string" &&
(e.length >= 4 || (e[3] != null && !Array.isArray(e[3])))
) {
const key = JSON.stringify(e[1]);
let s = likePatterns.get(key);
if (!s) likePatterns.set(key, (s = new Map()));
s.set(`${e[3] || ""}:${e[2]}`, parseLikePattern(e[2], e[3]));
}
} else if ([">", ">=", "<", "<=", "=", "<>"].includes(e[0])) {
if (!Array.isArray(e[2])) {
const key = `${JSON.stringify(e[1])}:${typeof e[2]}`;
let s = ranges.get(key);
if (!s) ranges.set(key, (s = new Set()));
s.add(e[2]);
} else if (!Array.isArray(e[1])) {
const key = `${JSON.stringify(e[2])}:${typeof e[1]}`;
let s = ranges.get(key);
if (!s) ranges.set(key, (s = new Set()));
s.add(e[2]);
}
}
return e;
});
for (const [k, v] of ranges) {
if (k.endsWith(":number")) {
ranges.set(
k,
Array.from(v as number[]).sort((a: number, b: number) => a - b)
);
} else {
ranges.set(k, Array.from(v).sort());
}
}
const expressions: Map<string, number> = new Map();
const mutuallyExclusive: Map<string, Set<number>> = new Map();
function getVariable(lhs, op, rhs = ""): number {
const key = `${lhs}:${op}:${rhs}`;
let f = expressions.get(key);
if (!f) {
expressions.set(key, (f = expressions.size + 1));
let m = mutuallyExclusive.get(lhs);
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
m.add(f);
if (rhs || op !== "null") m.add(getVariable(lhs, "null"));
}
return f;
}
function getComparisonVariables(
lhs,
from,
fromInclusive,
to,
toInclusive
): number[] {
const type = from != null ? typeof from : typeof to;
const range = ranges.get(`${lhs}:${type}`);
const vars = [];
if (from == null) vars.push(`${lhs}:--${range[0]}`);
for (let i = 0; i < range.length; ++i) {
if (
i > 0 &&
(from == null || range[i] > from) &&
(to == null || range[i] <= to)
)
vars.push(`${lhs}:${range[i - 1]}--${range[i]}`);
if (
(range[i] > from && range[i] < to) ||
(range[i] === from && fromInclusive) ||
(range[i] === to && toInclusive)
)
vars.push(`${lhs}:${range[i]}`);
}
if (to == null) vars.push(`${lhs}:${range[range.length - 1]}--`);
return vars.map(key => {
let f = expressions.get(key);
if (!f) {
expressions.set(key, (f = expressions.size + 1));
let m = mutuallyExclusive.get(lhs);
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
m.add(f);
}
return f;
});
}
function getLikeVariables(lhs, pat, esc): number[] {
const vars = [];
const pats = likePatterns.get(lhs);
pat = pats.get(`${esc || ""}:${pat}`);
for (const p of pats.values()) {
if (pat === p || likePatternIncludes(pat, p))
vars.push(`${lhs}:like:${JSON.stringify(p)}`);
}
return vars.map(key => {
let f = expressions.get(key);
if (!f) expressions.set(key, (f = expressions.size + 1));
return f;
});
}
interface ComparisonOperators {
[name: string]: (v: string) => [string, boolean, string, boolean];
}
const comparisonOperators: ComparisonOperators = {
"=": v => [v, true, v, true],
">": v => [v, false, null, null],
">=": v => [v, true, null, null],
"<": v => [null, null, v, false],
"<=": v => [null, null, v, true]
};
function recursive(clause, negate): CNF {
let op = clause[0];
if ((op === "AND" && !negate) || (op === "OR" && negate)) {
let res = [];
for (let i = 1; i < clause.length; ++i)
res = res.concat(recursive(clause[i], negate));
return res;
} else if ((op === "OR" && !negate) || (op === "AND" && negate)) {
let res = [];
for (let i = 1; i < clause.length; ++i)
res = or(res, recursive(clause[i], negate));
return res;
} else if (op === "NOT") {
return recursive(clause[1], !negate);
} else if (op === "<>") {
const vars = getComparisonVariables(
JSON.stringify(clause[1]),
...comparisonOperators["="](clause[2])
);
if (!negate) return vars.map(x => [0 - x]);
else return [vars];
} else if (op === "IS NULL") {
const v = getVariable(JSON.stringify(clause[1]), "null");
if (negate) return [[0 - v]];
else return [[v]];
} else if (op === "IS NOT NULL") {
const v = getVariable(JSON.stringify(clause[1]), "null");
if (!negate) return [[0 - v]];
else return [[v]];
} else if (op in comparisonOperators) {
if (!Array.isArray(clause[2])) {
const vars = getComparisonVariables(
JSON.stringify(clause[1]),
...comparisonOperators[op](clause[2])
);
if (negate) return vars.map(x => [0 - x]);
else return [vars];
} else if (!Array.isArray(clause[1])) {
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
const vars = getComparisonVariables(
JSON.stringify(clause[2]),
...comparisonOperators[op](clause[1])
);
if (negate) return vars.map(x => [0 - x]);
else return [vars];
} else {
let lhs = JSON.stringify(clause[1]);
let rhs = JSON.stringify(clause[2]);
// For consistency
if (rhs > lhs) {
const t = rhs;
rhs = lhs;
lhs = t;
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
}
const v = getVariable(lhs, op, rhs);
if (negate) return [[0 - v]];
else return [[v]];
}
} else if (
(op === "LIKE" || op === "NOT LIKE") &&
!Array.isArray(clause[2]) &&
clause[2] != null &&
(clause.length >= 4 || clause[3] != null)
) {
if (op === "NOT LIKE") negate = !negate;
const vars = getLikeVariables(
JSON.stringify(clause[1]),
clause[2],
clause[3]
);
if (negate) return vars.map(x => [0 - x]);
else return [vars];
} else {
const v = getVariable(JSON.stringify(clause), "");
if (negate) return [[0 - v]];
else return [[v]];
}
}
const cnf = recursive(exp, false);
for (const m of mutuallyExclusive.values()) {
const ar: number[] = Array.from(m);
for (let i = 0; i < ar.length; ++i)
for (let j = i + 1; j < ar.length; ++j) cnf.push([0 - ar[i], 0 - ar[j]]);
}
return { vars: expressions.size, clauses: cnf };
}
+378
View File
@@ -0,0 +1,378 @@
import * as parsimmon from "parsimmon";
import { Expression } from "../types";
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
function interpretEscapes(str): string {
const escapes = {
b: "\b",
f: "\f",
n: "\n",
r: "\r",
t: "\t"
};
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => {
const type = escape.charAt(0);
const hex = escape.slice(1);
if (type === "u") return String.fromCharCode(parseInt(hex, 16));
if (escapes.hasOwnProperty(type)) return escapes[type];
return type;
});
}
export function map(exp, callback): Expression {
if (!Array.isArray(exp)) return callback(exp);
let clone;
for (let i = 1; i < exp.length; ++i) {
const sub = map(exp[i], callback);
if (sub !== exp[i]) {
clone = clone || exp.slice();
clone[i] = sub;
}
}
return callback(clone || exp);
}
function binaryLeft(operatorsParser, nextParser): parsimmon.Parser<{}> {
return parsimmon.seqMap(
nextParser,
parsimmon.seq(operatorsParser, nextParser).many(),
(first, rest) =>
rest.reduce((acc, ch) => {
const [op, another] = ch;
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]);
if (Array.isArray(another) && op === another[0])
return [op, acc].concat(another.slice(1));
return [op, acc, another];
}, first)
);
}
const lang = parsimmon.createLanguage({
ComparisonOperator: function() {
return parsimmon
.alt(
parsimmon.string(">="),
parsimmon.string("<>"),
parsimmon.string("<="),
parsimmon.string("="),
parsimmon.string(">"),
parsimmon.string("<")
)
.skip(parsimmon.optWhitespace);
},
LikeOperator: function() {
return parsimmon
.alt(
parsimmon
.regexp(/like/i)
.result("LIKE")
.desc("LIKE"),
parsimmon
.regexp(/not\s+like/i)
.result("NOT LIKE")
.desc("NOT LIKE")
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
IsNullOperator: function() {
return parsimmon
.alt(
parsimmon
.regexp(/is\s+null/i)
.result("IS NULL")
.desc("IS NULL"),
parsimmon
.regexp(/is\s+not\s+null/i)
.result("IS NOT NULL")
.desc("IS NOT NULL")
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
NotOperator: function() {
return parsimmon
.regexp(/not/i)
.result("NOT")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("NOT");
},
AndOperator: function() {
return parsimmon
.regexp(/and/i)
.result("AND")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("AND");
},
OrOperator: function() {
return parsimmon
.regexp(/or/i)
.result("OR")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("OR");
},
Parameter: function(r) {
return parsimmon
.alt(
parsimmon.regexp(/[a-zA-Z0-9_.*-]+/),
r.Expression.wrap(
parsimmon.string("{").skip(parsimmon.optWhitespace),
parsimmon.string("}")
)
)
.atLeast(1)
.map(x => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]])
.skip(parsimmon.optWhitespace)
.desc("parameter");
},
StringValueSql: function() {
return parsimmon
.regexp(/'([^']*)'/, 1)
.atLeast(1)
.skip(parsimmon.optWhitespace)
.map(s => s.join("'"))
.desc("string");
},
StringValueJs: function() {
return parsimmon
.regexp(/"((?:\\.|.)*?)"/, 1)
.skip(parsimmon.optWhitespace)
.map(interpretEscapes)
.desc("string");
},
NumberValue: function() {
return parsimmon
.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)
.skip(parsimmon.optWhitespace)
.map(Number)
.desc("number");
},
BooleanValue: function() {
return parsimmon
.alt(
parsimmon
.regexp(/true/i)
.result(true)
.desc("TRUE"),
parsimmon
.regexp(/false/i)
.result(false)
.desc("FALSE")
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
NullValue: function() {
return parsimmon
.regexp(/null/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.result(null)
.desc("NULL");
},
FuncValue: function(r) {
return parsimmon.seqMap(
parsimmon
.regexp(/([a-zA-Z0-9_]+)/, 1)
.skip(parsimmon.optWhitespace)
.desc("function"),
r.ValueExpression.sepBy(
parsimmon.string(",").skip(parsimmon.optWhitespace)
).wrap(
parsimmon.string("(").skip(parsimmon.optWhitespace),
parsimmon.string(")").skip(parsimmon.optWhitespace)
),
(f, args) => ["FUNC", f.toUpperCase()].concat(args)
);
},
Value: function(r) {
return parsimmon.alt(
r.NullValue,
r.BooleanValue,
r.NumberValue,
r.StringValueSql,
r.StringValueJs,
r.FuncValue
);
},
ValueExpression: function(r) {
return binaryLeft(
parsimmon.string("||").skip(parsimmon.optWhitespace),
binaryLeft(
parsimmon
.alt(parsimmon.string("+"), parsimmon.string("-"))
.skip(parsimmon.optWhitespace),
binaryLeft(
parsimmon
.alt(parsimmon.string("*"), parsimmon.string("/"))
.skip(parsimmon.optWhitespace),
parsimmon.alt(
r.Value,
r.Parameter,
r.Expression.wrap(
parsimmon.string("(").skip(parsimmon.optWhitespace),
parsimmon.string(")").skip(parsimmon.optWhitespace)
)
)
)
)
);
},
Comparison: function(r) {
return parsimmon.alt(
parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]),
parsimmon.seqMap(
r.ValueExpression,
r.ComparisonOperator,
r.ValueExpression,
(p, o, v) => [o, p, v]
),
parsimmon.seqMap(
r.ValueExpression,
r.LikeOperator,
r.ValueExpression.skip(
parsimmon
.regexp(/escape/i)
.result("ESCAPE")
.skip(parsimmon.whitespace)
.desc("ESCAPE")
),
r.ValueExpression,
(a, b, c, d) => [b, a, c, d]
),
parsimmon.seqMap(
r.ValueExpression,
r.LikeOperator,
r.ValueExpression,
(a, b, c) => [b, a, c]
)
);
},
Expression: function(r) {
function unary(operatorsParser, nextParser): parsimmon.Parser<{}> {
return parsimmon.seq(operatorsParser, nextParser).or(nextParser);
}
return binaryLeft(
r.OrOperator,
binaryLeft(
r.AndOperator,
unary(r.NotOperator, r.Comparison.or(r.ValueExpression))
)
).trim(parsimmon.optWhitespace);
}
});
export function parse(str): Expression {
if (!str) return null;
return lang.Expression.tryParse(str);
}
export function stringify(exp, level = 0): string {
if (!Array.isArray(exp)) return JSON.stringify(exp);
const opLevels = {
OR: 10,
AND: 11,
NOT: 12,
"=": 20,
"<>": 20,
">": 20,
">=": 20,
"<": 20,
"<=": 20,
LIKE: 20,
"NOT LIKE": 20,
"IS NULL": 20,
"IS NOT NULL": 20,
"||": 30,
"+": 31,
"-": 31,
"*": 32,
"/": 32
};
const op = exp[0].toUpperCase();
function wrap(e): string {
if (opLevels[op] <= level) return `(${e})`;
else return e;
}
if (op === "FUNC") {
return wrap(
`${exp[1]}(${exp
.slice(2)
.map(e => stringify(e))
.join(", ")})`
);
} else if (op === "PARAM") {
if (typeof exp[1] === "string") {
return wrap(exp[1]);
} else if (Array.isArray(exp[1]) && exp[1][0] === "||") {
return wrap(
exp[1]
.slice(1)
.map(p => {
if (typeof p === "string") return p;
else return `{${stringify(p)}}`;
})
.join("")
);
} else {
return wrap(`{${stringify(exp[1])}}`);
}
} else if (op === "IS NULL" || op === "IS NOT NULL") {
return wrap(`${stringify(exp[1], opLevels[op])} ${op}`);
} else if (op === "LIKE" || op === "NOT LIKE") {
if (exp[3]) {
return wrap(
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
exp[2],
opLevels[op]
)} ESCAPE ${stringify(exp[3], opLevels[op])}`
);
} else {
return wrap(
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
exp[2],
opLevels[op]
)}`
);
}
} else if (op in opLevels) {
const parts = exp.slice(1).map((e, i) => {
return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0));
});
if (op === "NOT") return wrap(`${op} ${parts[0]}`);
else return wrap(parts.join(` ${op} `));
} else {
throw new Error(`Unrecognized operator ${exp[0]}`);
}
}
export function parseLikePattern(pat, esc): string[] {
const chars = pat.split("");
for (let i = 0; i < chars.length; ++i) {
const c = chars[i];
if (c === esc) {
chars[i] = chars[i + 1] || "";
chars[i + 1] = "";
} else if (c === "_") {
chars[i] = "\\_";
} else if (c === "%") {
chars[i] = "\\%";
while (chars[i + 1] === "%") chars[++i] = "";
}
}
return chars.filter(c => c);
}
+250
View File
@@ -0,0 +1,250 @@
import { map, parseLikePattern } from "./expression-parser";
import { booleanCnf } from "./expression-cnf";
import { naiveDpll } from "./sat-solver";
import { Expression } from "../types";
const isArray = Array.isArray;
const regExpCache = new WeakMap();
const REDUCE_SKIP = {};
function reduce(exp, callback): Expression {
let loop = true;
while (loop) {
loop = false;
for (let i = 2; i < exp.length; ++i) {
const res = callback(exp[i - 1], exp[i], i - 2);
if (res !== REDUCE_SKIP) {
loop = true;
exp = exp.slice();
exp.splice(i - 1, 2, res);
}
}
}
if (exp.length === 2) return exp[1];
return exp;
}
export function likePatternToRegExp(pat, esc = "", flags = ""): RegExp {
const convChars = {
"-": "\\-",
"/": "\\/",
"\\": "\\/",
"^": "\\^",
$: "\\$",
"*": "\\*",
"+": "\\+",
"?": "\\?",
".": "\\.",
"(": "\\(",
")": "\\)",
"|": "\\|",
"[": "\\[",
"]": "\\]",
"{": "\\{",
"}": "\\}",
"\\%": ".*",
"\\_": "."
};
let chars = parseLikePattern(pat, esc);
if (!chars.length) return new RegExp("^$", flags);
chars = chars.map(c => convChars[c] || c);
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
const l = chars.length - 1;
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
return new RegExp(chars.join(""), flags);
}
export function evaluate(
exp,
obj,
now: number,
cb?: Function
): string | number | boolean | null;
export function evaluate(exp, obj?, now?: number, cb?: Function): Expression;
export function evaluate(exp, obj?, now?: number, cb?: Function): Expression {
function getRegExp(pat, esc): RegExp {
const k = `${esc || ""}:${pat}`;
let c = regExpCache.get(exp);
if (!c) regExpCache.set(exp, (c = {}));
if (!c[k]) c[k] = likePatternToRegExp(pat, esc);
return c[k];
}
return map(exp, e => {
if (cb) e = cb(e);
if (!isArray(e)) return e;
if (e[0] === "FUNC") {
if (e[1] === "NOW") {
if (now) return now;
} else if (e[1] === "UPPER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
} else if (e[1] === "LOWER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
}
} else if (e[0] === "PARAM") {
if (e[1] == null) return null;
if (obj && !isArray(e[1])) {
let v = obj[e[1]];
if (v == null) return null;
if (typeof v === "object") v = v.value ? v.value[0] : null;
return v;
}
} else if (e[0] === "AND") {
return reduce(e, (a, b) => {
if (!isArray(a)) return a ? b : a;
if (!isArray(b)) return b ? a : b;
return REDUCE_SKIP;
});
} else if (e[0] === "OR") {
return reduce(e, (a, b) => {
if (!isArray(a)) return a ? a : b;
if (!isArray(b)) return b ? b : a;
return REDUCE_SKIP;
});
} else if (e[0] === "NOT") {
if (!isArray(e[1])) return !e[1];
else if (e[1][0] === "NOT") return e[1][1];
} else if (e[0] === "IS NULL") {
if (isArray(e[1])) return e;
else if (e[1] == null) return true;
else return null;
} else if (e[0] === "IS NOT NULL") {
if (isArray(e[1])) return e;
else if (e[1] != null) return true;
else return null;
} else if (e[0] === "LIKE") {
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
else if (e[1] == null || e[2] == null || (e.length >= 4 && e[3] == null))
return null;
const r = getRegExp(e[2], e[3]);
return r.test(e[1]);
} else if (e[0] === "NOT LIKE") {
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
else if (e[1] == null || e[2] == null || (e.length >= 4 && e[3] == null))
return null;
const r = getRegExp(e[2], e[3]);
return !r.test(e[1]);
} else if (e[0] === "=") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] === e[2];
} else if (e[0] === "<>") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] !== e[2];
} else if (e[0] === ">") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] > e[2];
} else if (e[0] === ">=") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] >= e[2];
} else if (e[0] === "<") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] < e[2];
} else if (e[0] === "<=") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] <= e[2];
} else if (e[0] === "*") {
return reduce(e, (a, b) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return a * b;
}
return REDUCE_SKIP;
});
} else if (e[0] === "/") {
return reduce(e, (a, b, i) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return i === 0 ? a / b : a * b;
}
return REDUCE_SKIP;
});
} else if (e[0] === "+") {
return reduce(e, (a, b) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return a + b;
}
return REDUCE_SKIP;
});
} else if (e[0] === "-") {
return reduce(e, (a, b, i) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return i === 0 ? a - b : a + b;
}
return REDUCE_SKIP;
});
} else if (e[0] === "||") {
return reduce(e, (a, b) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return `${a}${b}`;
}
return REDUCE_SKIP;
});
}
return e;
});
}
export function and(exp1, exp2): Expression {
if (!isArray(exp1)) return exp1 ? exp2 : exp1;
if (!isArray(exp2)) return exp2 ? exp1 : exp2;
let res: Expression = ["AND"];
if (exp1[0] === "AND") res = res.concat(exp1.slice(1));
else res.push(exp1);
if (exp2[0] === "AND") res = res.concat(exp2.slice(1));
else res.push(exp2);
return res;
}
export function or(exp1, exp2): Expression {
if (!isArray(exp1)) return exp1 ? exp1 : exp2;
if (!isArray(exp2)) return exp2 ? exp2 : exp1;
let res: Expression = ["OR"];
if (exp1[0] === "OR") res = res.concat(exp1.slice(1));
else res.push(exp1);
if (exp2[0] === "OR") res = res.concat(exp2.slice(1));
else res.push(exp2);
return res;
}
export function not(exp): Expression {
if (isArray(exp) && exp[0] === "NOT") return exp[1];
return ["NOT", exp];
}
export function subset(exp1, exp2): boolean {
const e = evaluate(["NOT", ["OR", ["NOT", exp1], exp2]]);
if (!isArray(e)) return !e;
const { vars, clauses } = booleanCnf(e);
return !naiveDpll(clauses, vars);
}
export function extractParams(exp): string[] {
const params = new Set();
map(exp, e => {
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
return e;
});
return Array.from(params);
}
+43
View File
@@ -0,0 +1,43 @@
let cache1 = new Map();
let cache2 = new Map();
const keys = new WeakMap();
function getKey(obj): string {
if (obj === null) return "null";
else if (obj === undefined) return "undefined";
const t = typeof obj;
if (t === "number" || t === "boolean" || t === "string") return `${t}:${obj}`;
if (t !== "function" && t !== "object")
throw new Error(`Cannot memoize ${t} arguments`);
let k = keys.get(obj);
if (!k) {
const rnd = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER);
k = `${t}:${rnd.toString(36)}`;
keys.set(obj, k);
}
return k;
}
export default function memoize(func): Function {
return (...args) => {
const key = JSON.stringify(args.map(getKey)) + getKey(func);
if (cache1.has(key)) return cache1.get(key);
let r;
if (cache2.has(key)) r = cache2.get(key);
else r = func(...args);
cache1.set(key, r);
return r;
};
}
const interval = setInterval(() => {
cache2 = cache1;
cache1 = new Map();
}, 120000);
// Don't hold Node.js process
if (interval.unref) interval.unref();
+36 -34
View File
@@ -16,19 +16,24 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
class PathSet {
constructor() {
import Path from "./path";
export default class PathSet {
private lengthIndex: Set<Path>[];
private fragmentIndex: Map<string, Set<Path>>[];
public constructor() {
this.lengthIndex = [];
this.fragmentIndex = [];
}
get depth() {
public get depth(): number {
return this.lengthIndex.length;
}
add(path) {
public add(path: Path): Path {
if (path.alias) throw new Error("PathSet does not support aliased paths");
const p = this.get(path);
if (p) return p;
@@ -41,43 +46,36 @@ class PathSet {
const lengthIndex = this.lengthIndex[path.length];
lengthIndex.add(path);
let wildcard = 0;
for (let i = 0; i < path.length; ++i) {
const fragment = path[i];
if (fragment === "*") wildcard = wildcard | (1 << i);
const fragment = path.segments[i] as string;
const fragmentIndex = this.fragmentIndex[i];
let fragmentIndexSet = fragmentIndex.get(fragment);
if (!fragmentIndexSet) {
fragmentIndexSet = new Set();
fragmentIndexSet = new Set<Path>();
fragmentIndex.set(fragment, fragmentIndexSet);
}
fragmentIndexSet.add(path);
}
if (path.wildcard == null) {
path.alias = 0;
path.wildcard = wildcard;
Object.freeze(path);
}
return path;
}
get(pattern) {
const lengthIndex = this.lengthIndex[pattern.length];
public get(path: Path): Path {
if (path.alias) throw new Error("PathSet does not support aliased paths");
const lengthIndex = this.lengthIndex[path.length];
if (!lengthIndex || !lengthIndex.size) return null;
if (lengthIndex.has(pattern)) return pattern;
if (lengthIndex.has(path)) return path;
const sets = [lengthIndex];
for (let i = 0; i < pattern.length; ++i) {
for (let i = 0; i < path.length; ++i) {
const fragmentIndex = this.fragmentIndex[i];
if (!fragmentIndex || !fragmentIndex.size) return null;
const fragment = pattern[i];
const fragment = path.segments[i] as string;
const fragmentIndexSet = fragmentIndex.get(fragment);
if (!fragmentIndexSet) return null;
@@ -88,34 +86,40 @@ class PathSet {
sets.sort((a, b) => a.size - b.size);
const smallestSet = sets.shift();
for (let path of smallestSet) {
for (let p of smallestSet) {
for (const s of sets) {
if (!s.has(path)) {
path = null;
if (!s.has(p)) {
p = null;
break;
}
}
if (path) return path;
if (p) return p;
}
return null;
}
find(pattern, superset = false, subset = false, depth = pattern.length) {
public find(
path: Path,
superset: boolean = false,
subset: boolean = false,
depth: number = path.length
): Path[] {
if (path.alias) throw new Error("PathSet does not support aliased paths");
const res = [];
const groups = [[]];
for (let i = pattern.length; i <= depth; ++i)
for (let i = path.length; i <= depth; ++i)
if (this.lengthIndex[i]) groups[0].push(this.lengthIndex[i]);
if (!groups[0].length) return res;
for (let i = 0; i < pattern.length; ++i) {
for (let i = 0; i < path.length; ++i) {
const fragmentIndex = this.fragmentIndex[i];
if (!fragmentIndex) return res;
const fragment = pattern[i];
const fragment = path.segments[i] as string;
if (fragment === "*" && subset) continue;
@@ -140,26 +144,24 @@ class PathSet {
const smallestGroup = groups.shift();
for (const set of smallestGroup) {
for (let path of set) {
for (let p of set) {
for (const group of groups) {
let found = false;
for (const s of group) {
if (s.has(path)) {
if (s.has(p)) {
found = true;
break;
}
}
if (!found) {
path = null;
p = null;
break;
}
}
if (path) res.push(path);
if (p) res.push(p);
}
}
return res;
}
}
module.exports = PathSet;
+189
View File
@@ -0,0 +1,189 @@
/**
* Copyright 2013-2019 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 <http://www.gnu.org/licenses/>.
*/
type Segments = (string | Alias)[];
type Alias = [Path, string][];
let cache1 = new Map<string, Path>();
let cache2 = new Map<string, Path>();
export default class Path {
public readonly segments: Segments;
public readonly wildcard: number;
public readonly alias: number;
protected _string: string;
protected static parseAlias(
pattern: string,
index: number
): { index: number; alias: Alias } {
const alias: Alias = [];
while (index < pattern.length && pattern[index] !== "]") {
const res = Path.parsePath(pattern, index);
let j = (index = res.index + 1);
while (pattern[j] !== "]" && pattern[j] !== ",") {
if (pattern[j] === '"' && index === j) {
++j;
while (pattern[j] !== '"' || pattern[j - 1] === "\\") {
if (++j >= pattern.length)
throw new Error("Invalid alias expression");
}
}
if (++j >= pattern.length) throw new Error("Invalid alias expression");
}
let value = pattern.slice(index, j).trim();
index = j;
if (value[0] === '"') {
try {
value = JSON.parse(value);
} catch (error) {
throw new Error("Invalid alias expression");
}
}
alias.push([new Path(res.segments, res.wildcard, res.alias), value]);
if (pattern[index] === ",") ++index;
}
// Ensure identical expressions have idential string representation
alias.sort((a, b) => {
if (a[0].toString() > b[0].toString()) return 1;
else if (a[0].toString() < b[0].toString()) return -1;
else if (a[1] > b[1]) return 1;
else if (a[1] < b[1]) return -1;
else return 0;
});
Object.freeze(alias);
return { index, alias };
}
protected static parsePath(
pattern: string,
index: number
): { index: number; segments: Segments; wildcard: number; alias: number } {
const segments = [];
let wildcard = 0;
let alias = 0;
// Colon separator is needed for parseAlias
if (index < pattern.length && pattern[index] !== ":") {
for (;;) {
if (pattern[index] === "[") {
const res = Path.parseAlias(pattern, index + 1);
index = res.index + 1;
alias |= 1 << segments.length;
segments.push(res.alias);
} else {
const j = index;
while (
index < pattern.length &&
pattern[index] !== ":" &&
pattern[index] !== "."
)
++index;
const s = pattern.slice(j, index).trim();
if (s === "*") wildcard |= 1 << segments.length;
segments.push(s);
}
if (index >= pattern.length || pattern[index] === ":") break;
else if (pattern[index] !== ".")
throw new Error("Invalid alias expression");
++index;
}
}
Object.freeze(segments);
return { index, segments, wildcard, alias };
}
protected constructor(segments: Segments, wildcard: number, alias: number) {
this.segments = segments;
this.wildcard = wildcard;
this.alias = alias;
this._string = null;
}
public static parse(str: string): Path {
let path = cache1.get(str);
if (!path) {
path = cache2.get(str);
if (!path) {
const res = Path.parsePath(str, 0);
path = new Path(res.segments, res.wildcard, res.alias);
}
cache1.set(str, path);
}
return path;
}
public get length(): number {
return this.segments.length;
}
public toString(): string {
if (this._string == null) {
this._string = this.segments
.map(s => {
if (Array.isArray(s)) {
const parts = s.map(
al => `${al[0].toString()}:${JSON.stringify(al[1])}`
);
return `[${parts.join(",")}]`;
}
return s;
})
.join(".");
}
return this._string;
}
public slice(start: number = 0, end: number = this.segments.length): Path {
if (start < 0) start = Math.max(0, this.segments.length + start);
if (end < 0) end = Math.max(0, this.segments.length + end);
if (start >= end) return new Path([], 0, 0);
const segments = this.segments.slice(start, end);
const mask = (1 << (end - start)) - 1;
const wildcard = (this.wildcard >> start) & mask;
const alias = (this.alias >> start) & mask;
return new Path(segments, wildcard, alias);
}
public concat(path: Path): Path {
const segments = this.segments.concat(path.segments);
const wildcard = this.wildcard | (path.wildcard << this.segments.length);
const alias = this.alias | (path.alias << this.segments.length);
return new Path(segments, wildcard, alias);
}
public stripAlias(): Path {
if (!this.alias) return this;
const segments = this.segments.map(s => (Array.isArray(s) ? "*" : s));
return new Path(segments, this.wildcard | this.alias, 0);
}
}
const interval = setInterval(() => {
cache2 = cache1;
cache1 = new Map();
}, 120000);
// Don't hold Node.js process
if (interval.unref) interval.unref();
+175
View File
@@ -0,0 +1,175 @@
/**
* Naive DPLL solver for propositional logic adapted from
* https://github.com/tammet/logictools
*
* Origianl code by Tanel Tammet released under the MIT license.
*
* Check whether a clause set is satisfiable or contradictory, using a naive
* implementation of the dpll (Davis-Putnam-Logemann-Loveland) procedure
* combining truth table search with the resolution-like unit propagation.
*
* The standard pure-literal rule is not implemented in this naive version,
* since it is not really crucial for the algorithm.
*
* The code is very similar to the pure table solver, except we use a
* unit-propagating, derived-varlist-returing satisfiableAt.
*/
/* eslint-disable @typescript-eslint/no-use-before-define */
function satisfiableAt(clauses, varVals, varnr, val): boolean {
if (varnr !== 0) varVals[varnr] = val;
const propRes = unitPropagate(clauses, varVals);
if (propRes === 1) {
varVals[varnr] = 0;
return true;
}
// if result false (-1) unitPropagate restores the varVals state itself
if (propRes === -1) {
varVals[varnr] = 0;
return false;
}
// find next unassigned var to split
let nextVar = 0;
for (let i = 1; i < varVals.length; i++) {
if (varVals[i] === 0) {
nextVar = i;
break;
}
}
if (nextVar === 0) throw new Error("Error in satisfiableAt");
if (
satisfiableAt(clauses, varVals, nextVar, 1) ||
satisfiableAt(clauses, varVals, nextVar, -1)
)
// not necessary to restore split var: search is halted anyway
return true;
// restore split var
varVals[nextVar] = 0;
// restore derived vars
for (let i = 0; i < propRes.length; i++) varVals[propRes[i]] = 0;
return false;
}
/**
* Check whether clauses are satisfiable under the varVals assignment, without
* search.
*
* Iterate through all clauses and check whether
*
* - all literals in the clause are assigned false: undo derived units (set
* values in varVals to 0) and return false immediately
*
* - a clause contains a literal assigned true: skip further actions on this
* clause
*
* - a single literal in the clause is unassigned, all the rest are assigned
* false: derive this literal as a unit clause.
*
* In case at least one literal is derived during the process above, iterate
* the whole process until no more new literals are derived.
*
* Returns:
*
* 1: all clauses were assigned true, hence the clause set is satisfiable.
* The whole search will terminate after that.
*
* -1: clause set is contradictory. Derived units have been restored.
*
* a list of derived variables: the status of the clause set is
* undetermined. The values of variables on the list will be restored (set to
* 0) during backtracking.
*/
function unitPropagate(clauses, varVals): number[] | 1 | -1 {
// all vars with a value derived in the forthcoming main loop
const derivedVars = [];
// the main loop is run, deriving new unit clauses, until either:
// - the clause set is detected to be unsatisfiable
// - the clause set is detected to be satisfiable
// - there are no more derived unit clauses
for (;;) {
let allClausesFound = true; // set to false if we find an undetermined clause
const derivedQueue = []; // derived units (literals) during one iteration
for (let i = 0; i < clauses.length; i++) {
const clause = clauses[i];
let clauseVal = 0;
let unassignedCount = 0; // count unassigned vars in clause
let unassignedLit = 0; // 0 means none found for this clause
for (let j = 0; j < clause.length; j++) {
const lit = clause[j];
let nr, polarity;
if (lit < 0) {
nr = 0 - lit;
polarity = -1;
} else {
nr = lit;
polarity = 1;
}
if (varVals[nr] === polarity) {
clauseVal = 1;
break;
} else if (varVals[nr] === 0) {
unassignedCount++;
unassignedLit = lit;
}
}
if (clauseVal === 1) continue; // clause is subsumed
// clause is not subsumed by varVals
if (unassignedCount === 0) {
for (let j = 0; j < derivedVars.length; j++)
varVals[derivedVars[j]] = 0; // restore derived
return -1;
} else if (unassignedCount === 1) {
// unassignedLit is a derived literal
derivedQueue.push(unassignedLit);
}
if (unassignedCount !== 0) allClausesFound = false;
}
// if all clauses were subsumed or all the variables in a clause had a value
// no need to restore varVals: whole search finished
if (allClausesFound) return 1;
// in case there are any derived units present, store to varVals and
// iterate the main loop
if (derivedQueue.length === 0) break;
// some units were derived: iterate the main loop
for (let j = 0; j < derivedQueue.length; j++) {
const lit = derivedQueue[j];
let nr, polarity;
if (lit < 0) {
nr = 0 - lit;
polarity = -1;
} else {
nr = lit;
polarity = 1;
}
varVals[nr] = polarity;
derivedVars.push(nr);
}
}
return derivedVars;
}
export function naiveDpll(clauses, maxvarnr): boolean {
// variable values are 0 if not set, 1 if positive, -1 if negative
const varVals = new Int32Array(maxvarnr + 1);
return satisfiableAt(clauses, varVals, 0, 0);
}
+3
View File
@@ -0,0 +1,3 @@
export default function pass(): number {
return 1;
}
+12
View File
@@ -0,0 +1,12 @@
export default function test(resourceType, resource): number {
if (resourceType === "devices") {
if (resource["Tags.test"] && resource["Tags.test"].value[0]) return 1;
} else if (resourceType === "presets") {
if (resource["_id"].startsWith("test-"))
// TODO check "test" tag too
return 1;
} else if (resourceType === "provisions") {
if (resource["_id"].startsWith("test-")) return 1;
}
return -1;
}
+25 -23
View File
@@ -16,9 +16,9 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const path = require("path");
import { resolve } from "path";
import { readFileSync } from "fs";
const options = {
CONFIG_DIR: { type: "path", default: "config" },
@@ -26,9 +26,6 @@ const options = {
type: "string",
default: "mongodb://127.0.0.1/genieacs"
},
REDIS_PORT: { type: "int", default: 6379 },
REDIS_HOST: { type: "string", default: "" },
REDIS_DB: { type: "int", default: 0 },
CWMP_WORKER_PROCESSES: { type: "int", default: 0 },
CWMP_PORT: { type: "int", default: 7547 },
@@ -52,6 +49,14 @@ const options = {
FS_LOG_FILE: { type: "path", default: "" },
FS_ACCESS_LOG_FILE: { type: "path", default: "" },
UI_WORKER_PROCESSES: { type: "int", default: 0 },
UI_PORT: { type: "int", default: 3000 },
UI_INTERFACE: { type: "string", default: "0.0.0.0" },
UI_SSL: { type: "bool", default: false },
UI_LOG_FILE: { type: "path", default: "" },
UI_ACCESS_LOG_FILE: { type: "path", default: "" },
UI_JWT_SECRET: { type: "string", default: "" },
UDP_CONNECTION_REQUEST_PORT: { type: "int", default: 0 },
DOWNLOAD_TIMEOUT: { type: "int", default: 3600 },
@@ -84,9 +89,9 @@ const options = {
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }
};
const allConfig = {};
const allConfig: { [name: string]: string | number } = {};
function setConfig(name, value, commandLineArgument) {
function setConfig(name, value, commandLineArgument = false): boolean {
if (allConfig[name] != null) return true;
// For compatibility with v1.0
@@ -113,7 +118,7 @@ function setConfig(name, value, commandLineArgument) {
if (name === "FS_IP" || name === "fs-ip") setConfig("FS_HOSTNAME", value);
function cast(val, type) {
function cast(val, type): string | number | boolean {
switch (type) {
case "int":
return Number(val);
@@ -127,7 +132,7 @@ function setConfig(name, value, commandLineArgument) {
return String(val);
case "path":
if (!val) return "";
return path.resolve(val);
return resolve(val);
default:
return null;
}
@@ -175,7 +180,11 @@ for (const [k, v] of Object.entries(process.env))
setConfig("CONFIG_DIR", options["CONFIG_DIR"]["default"]);
// Configuration file
const configFile = require(path.resolve(allConfig.CONFIG_DIR, "config"));
const configFile = JSON.parse(
readFileSync(
resolve(allConfig.CONFIG_DIR as string, "config.json")
).toString()
);
for (const [k, v] of Object.entries(configFile)) {
if (!setConfig(k, v))
// Pass as environment variable to be accessable by extensions
@@ -186,7 +195,10 @@ for (const [k, v] of Object.entries(configFile)) {
for (const [k, v] of Object.entries(options))
if (v["default"] != null) setConfig(k, v["default"]);
function get(optionName, deviceId) {
export function get(
optionName: string,
deviceId?: string
): string | number | boolean {
if (!deviceId) return allConfig[optionName];
optionName = `${optionName}-${deviceId}`;
@@ -214,22 +226,12 @@ function get(optionName, deviceId) {
return null;
}
function getDefault(optionName) {
export function getDefault(optionName): string | number | boolean {
const option = options[optionName];
if (!option) return null;
let val = option["default"];
if (val && option.type === "path") val = path.resolve(val);
if (val && option.type === "path") val = resolve(val);
return val;
}
// Load authentication scripts
try {
exports.auth = require(path.resolve(allConfig.CONFIG_DIR, "auth"));
} catch (error) {
// No auth.js exists
}
exports.get = get;
exports.getDefault = getDefault;
+198
View File
@@ -0,0 +1,198 @@
/**
* Copyright 2013-2019 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 <http://www.gnu.org/licenses/>.
*/
import * as crypto from "crypto";
import * as dgram from "dgram";
import { parse } from "url";
import * as http from "http";
import { evaluate } from "./common/expression";
import { Expression } from "./types";
import * as auth from "./auth";
function extractAuth(
exp: Expression,
context: {},
now: number,
dflt: any
): [string, string, Expression] {
let username, password;
exp = evaluate(exp, context, now, e => {
if (!username && Array.isArray(e) && e[0] === "FUNC" && e[1] === "AUTH") {
if (!Array.isArray(e[2]) && !Array.isArray(e[3])) {
username = e[2];
password = e[3];
}
return dflt;
}
return e;
});
return [username, password, exp];
}
function httpGet(
options: {},
timeout
): Promise<{ statusCode: number; headers: {} }> {
return new Promise((resolve, reject) => {
const req = http
.get(options, res => {
res.resume();
resolve({ statusCode: res.statusCode, headers: res.headers });
})
.on("error", () => {
req.abort();
reject(new Error("Device is offline"));
})
.on("socket", socket => {
socket.setTimeout(timeout);
socket.on("timeout", () => {
req.abort();
});
});
});
}
export async function httpConnectionRequest(
address: string,
authExp: Expression,
context: {},
allowBasicAuth: boolean,
timeout: number
): Promise<void> {
const now = Date.now();
const options: http.RequestOptions = parse(address);
if (options.protocol !== "http:")
throw new Error("Invalid connection request URL or protocol");
options.agent = new http.Agent({
maxSockets: 1,
keepAlive: true
});
let authHeader: {};
let username: string;
let password: string;
while (!authHeader || (username != null && password != null)) {
let opts = options;
if (authHeader) {
if (authHeader["method"] === "Basic") {
if (!allowBasicAuth)
throw new Error("Basic HTTP authentication not allowed");
opts = Object.assign(
{
headers: {
Authorization: auth.basic(username || "", password || "")
}
},
options
);
} else if (authHeader["method"] === "Digest") {
opts = Object.assign(
{
headers: {
Authorization: auth.digest(
username,
password,
options.path,
"GET",
null,
authHeader
)
}
},
options
);
} else {
throw new Error("Unrecognized auth method");
}
}
let res = await httpGet(opts, timeout);
// Workaround for some devices unexpectedly closing the connection
if (res.statusCode === 0 && authHeader) res = await httpGet(opts, timeout);
if (res.statusCode === 0) throw new Error("Device is offline");
if (res.statusCode === 200 || res.statusCode === 204) return;
if (res.statusCode === 401 && res.headers["www-authenticate"]) {
authHeader = auth.parseAuthHeader(res.headers["www-authenticate"]);
[username, password, authExp] = extractAuth(authExp, context, now, false);
} else {
throw new Error(
`Unexpected response code from device: ${res.statusCode}`
);
}
}
throw new Error("Incorrect connection request credentials");
}
export async function udpConnectionRequest(
address: string,
authExp: Expression,
context: {},
sourcePort: number = 0
): Promise<void> {
const [host, portStr] = address.split(":", 2);
const port = portStr ? parseInt(portStr) : 80;
const now = Date.now();
const client = dgram.createSocket({ type: "udp4", reuseAddr: true });
// When a device is NAT'ed, the UDP Connection Request must originate from
// the same address and port used by the STUN server, in order to traverse
// the firewall. This does require that the Genieacs NBI and STUN server
// are allowed to bind to the same address and port. The STUN server needs
// to open its UDP port with the SO_REUSEADDR option, allowing the NBI to
// also bind to the same port.
if (sourcePort) client.bind({ port: sourcePort, exclusive: true });
let username: string;
let password: string;
[username, password, authExp] = extractAuth(authExp, context, now, null);
if (username == null) username = "";
if (password == null) password = "";
while (username != null && password != null) {
const ts = Math.trunc(now / 1000);
const id = Math.trunc(Math.random() * 4294967295);
const cn = crypto.randomBytes(8).toString("hex");
const sig = crypto
.createHmac("sha1", password)
.update(`${ts}${id}${username}${cn}`)
.digest("hex");
const uri = `http://${address}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
const message = Buffer.from(
`GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n`
);
for (let i = 0; i < 3; ++i) {
await new Promise((resolve, reject) => {
client.send(message, 0, message.length, port, host, (err: Error) => {
if (err) reject(err);
else resolve();
});
});
}
[username, password, authExp] = extractAuth(authExp, context, now, null);
}
client.close();
}
+197 -102
View File
@@ -16,25 +16,36 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const zlib = require("zlib");
const crypto = require("crypto");
const fs = require("fs");
const config = require("./config");
const common = require("./common");
const soap = require("./soap");
const session = require("./session");
const query = require("./query");
const device = require("./device");
const cache = require("./cache");
const localCache = require("./local-cache");
const db = require("./db");
const logger = require("./logger");
const scheduling = require("./scheduling");
import * as zlib from "zlib";
import * as crypto from "crypto";
import * as fs from "fs";
import { Socket } from "net";
import * as config from "./config";
import * as common from "./common";
import * as soap from "./soap";
import * as session from "./session";
import { evaluate, extractParams } from "./common/expression";
import * as device from "./device";
import * as cache from "./cache";
import * as localCache from "./local-cache";
import * as db from "./db";
import * as logger from "./logger";
import * as scheduling from "./scheduling";
import Path from "./common/path";
import {
SessionContext,
AcsRequest,
SetAcsRequest,
SessionFault,
Operation,
Fault
} from "./types";
const MAX_CYCLES = 4;
const MAX_CONCURRENT_REQUESTS = config.get("MAX_CONCURRENT_REQUESTS");
const MAX_CONCURRENT_REQUESTS = +config.get("MAX_CONCURRENT_REQUESTS");
const currentSessions = new WeakMap<Socket, SessionContext>();
const stats = {
concurrentRequests: 0,
@@ -43,7 +54,7 @@ const stats = {
initiatedSessions: 0
};
function throwError(err, httpResponse) {
function throwError(err, httpResponse?): never {
if (httpResponse) {
currentSessions.delete(httpResponse.connection);
httpResponse.writeHead(500, { Connection: "close" });
@@ -53,11 +64,15 @@ function throwError(err, httpResponse) {
throw err;
}
function writeResponse(sessionContext, res, close) {
function writeResponse(
sessionContext: SessionContext,
res,
close = false
): void {
// Close connection after last request in session
if (close) res.headers["Connection"] = "close";
if (config.get("DEBUG", sessionContext.deviceId)) {
if (sessionContext.debug) {
const dump =
`# RESPONSE ${new Date(Date.now())}\n` +
JSON.stringify(res.headers) +
@@ -69,7 +84,7 @@ function writeResponse(sessionContext, res, close) {
});
}
function finish() {
function finish(): void {
stats.concurrentRequests -= 1;
if (sessionContext.httpRequest.connection.destroyed) {
logger.accessError({
@@ -130,7 +145,19 @@ function writeResponse(sessionContext, res, close) {
}
}
function recordFault(sessionContext, fault, provisions, channels) {
function recordFault(
sessionContext: SessionContext,
fault: Fault,
provisions,
channels
): void;
function recordFault(sessionContext: SessionContext, fault: Fault): void;
function recordFault(
sessionContext: SessionContext,
fault: Fault,
provisions?,
channels?
): void {
if (!provisions) {
provisions = sessionContext.provisions;
channels = sessionContext.channels;
@@ -141,7 +168,10 @@ function recordFault(sessionContext, fault, provisions, channels) {
const provs = sessionContext.faults[channel]
? sessionContext.faults[channel].provisions
: [];
faults[channel] = Object.assign({ provisions: provs }, fault);
faults[channel] = Object.assign(
{ provisions: provs, timestamp: sessionContext.timestamp },
fault
) as SessionFault;
if (channel.startsWith("task_")) {
const taskId = channel.slice(5);
for (const t of sessionContext.tasks)
@@ -185,7 +215,7 @@ function recordFault(sessionContext, fault, provisions, channels) {
session.clearProvisions(sessionContext);
}
function inform(sessionContext, rpc) {
function inform(sessionContext, rpc): void {
session.inform(sessionContext, rpc.cpeRequest, (err, acsResponse) => {
if (err) return void throwError(err, sessionContext.httpResponse);
@@ -195,7 +225,12 @@ function inform(sessionContext, rpc) {
cwmpVersion: sessionContext.cwmpVersion
});
const cookiesPath = config.get("COOKIES_PATH", sessionContext.deviceId);
const cookiesPath = localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.cookiesPath",
sessionContext.configContext
);
if (cookiesPath) {
res.headers["Set-Cookie"] = `session=${
sessionContext.sessionId
@@ -208,7 +243,7 @@ function inform(sessionContext, rpc) {
});
}
function transferComplete(sessionContext, rpc) {
function transferComplete(sessionContext, rpc): void {
session.transferComplete(
sessionContext,
rpc.cpeRequest,
@@ -244,8 +279,8 @@ function transferComplete(sessionContext, rpc) {
);
}
// Append providions and remove duplicates
function appendProvisions(original, toAppend) {
// Append provisions and remove duplicates
function appendProvisions(original, toAppend): boolean {
let modified = false;
const stringified = new WeakMap();
@@ -276,7 +311,7 @@ function appendProvisions(original, toAppend) {
return modified;
}
function applyPresets(sessionContext) {
function applyPresets(sessionContext: SessionContext): void {
const deviceData = sessionContext.deviceData;
const presets = localCache.getPresets(sessionContext.cacheSnapshot);
@@ -284,7 +319,11 @@ function applyPresets(sessionContext) {
const blackList = {};
let whiteList = null;
let whiteListProvisions = null;
const RETRY_DELAY = config.get("RETRY_DELAY", sessionContext.deviceId);
const RETRY_DELAY = +localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.retryDelay",
sessionContext.configContext
);
if (sessionContext.faults) {
for (const [channel, fault] of Object.entries(sessionContext.faults)) {
@@ -309,13 +348,13 @@ function applyPresets(sessionContext) {
deviceData.attributes.revision = 1;
const deviceEvents = {};
for (const p of deviceData.paths.find(["Events", "*"], false, true)) {
for (const p of deviceData.paths.find(Path.parse("Events.*"), false, true)) {
const attrs = deviceData.attributes.get(p);
if (attrs && attrs.value && attrs.value[1][0] >= sessionContext.timestamp)
deviceEvents[p[1]] = true;
deviceEvents[p.segments[1] as string] = true;
}
const parameters = {};
const parameters: { [name: string]: Path } = {};
const filteredPresets = [];
for (const preset of presets) {
@@ -345,11 +384,8 @@ function applyPresets(sessionContext) {
}
filteredPresets.push(preset);
for (const k of Object.keys(preset.precondition)) {
sessionContext.channels[preset.channel] = 0;
const p = k.split(/([^a-zA-Z0-9\-_.].*)/, 1)[0];
parameters[p] = common.parsePath(p);
}
for (const k of extractParams(preset.precondition))
parameters[k] = Path.parse(k);
}
const declarations = Object.values(parameters).map(v => ({
@@ -380,7 +416,7 @@ function applyPresets(sessionContext) {
const parameterValues = {};
for (const [k, v] of Object.entries(parameters)) {
const unpacked = device.unpack(deviceData, v);
if (!unpacked[0]) continue;
if (!unpacked.length) continue;
const attrs = deviceData.attributes.get(unpacked[0]);
if (attrs && attrs.value && attrs.value[1])
parameterValues[k] = attrs.value[1][0];
@@ -390,8 +426,9 @@ function applyPresets(sessionContext) {
session.addProvisions(sessionContext, whiteList, whiteListProvisions);
const appendProvisionsToFaults = {};
const now = Date.now();
for (const p of filteredPresets) {
if (query.testFilter(parameterValues, p.precondition)) {
if (evaluate(p.precondition, parameterValues, now)) {
if (blackList[p.channel] === 2) {
appendProvisionsToFaults[p.channel] = (
appendProvisionsToFaults[p.channel] || []
@@ -469,7 +506,7 @@ function applyPresets(sessionContext) {
);
}
function nextRpc(sessionContext) {
function nextRpc(sessionContext: SessionContext): void {
session.rpcRequest(sessionContext, null, (err, fault, id, acsRequest) => {
if (err) return void throwError(err, sessionContext.httpResponse);
@@ -595,7 +632,7 @@ function nextRpc(sessionContext) {
});
}
function endSession(sessionContext, callback) {
function endSession(sessionContext: SessionContext, callback): void {
let saveCache = sessionContext.cacheUntil != null;
let counter = 3;
@@ -705,24 +742,29 @@ function endSession(sessionContext, callback) {
if ((counter -= 2) === 1) callback(null, sessionContext.new);
}
function sendAcsRequest(sessionContext, id, acsRequest) {
function sendAcsRequest(
sessionContext: SessionContext,
id?: string,
acsRequest?: AcsRequest
): void {
if (!acsRequest)
return void writeResponse(sessionContext, soap.response(null), true);
if (acsRequest.name === "Download") {
acsRequest.fileSize = 0;
if (!acsRequest.url) {
const downloadRequest = acsRequest as SetAcsRequest;
downloadRequest.fileSize = 0;
if (!downloadRequest.url) {
const FS_PORT = config.get("FS_PORT");
const FS_HOSTNAME = config.get("FS_HOSTNAME");
const FS_SSL = config.get("FS_SSL");
acsRequest.url = FS_SSL ? "https://" : "http://";
acsRequest.url += FS_HOSTNAME;
if (FS_PORT !== 80) acsRequest.url += ":" + FS_PORT;
acsRequest.url += "/" + encodeURI(acsRequest.fileName);
downloadRequest.url = FS_SSL ? "https://" : "http://";
downloadRequest.url += FS_HOSTNAME;
if (FS_PORT !== 80) downloadRequest.url += ":" + FS_PORT;
downloadRequest.url += "/" + encodeURI(downloadRequest.fileName);
const files = localCache.getFiles(sessionContext.cacheSnapshot);
if (files[acsRequest.fileName])
acsRequest.fileSize = files[acsRequest.fileName].length;
if (files[downloadRequest.fileName])
downloadRequest.fileSize = files[downloadRequest.fileName].length;
}
}
@@ -742,7 +784,11 @@ function sendAcsRequest(sessionContext, id, acsRequest) {
writeResponse(sessionContext, res);
}
function getSession(connection, sessionId, callback) {
function getSession(
connection,
sessionId,
callback: (err?: Error, sessionContext?: SessionContext) => void
): void {
const sessionContext = currentSessions.get(connection);
if (sessionContext) {
currentSessions.delete(connection);
@@ -759,10 +805,8 @@ function getSession(connection, sessionId, callback) {
}, 100);
}
const currentSessions = new WeakMap();
// When socket closes, store active sessions in cache
function onConnection(socket) {
export function onConnection(socket): void {
// The property remoteAddress may be undefined after the connection is
// closed, unless we read it at least once (caching?)
socket.remoteAddress;
@@ -831,7 +875,29 @@ setInterval(() => {
stats.initiatedSessions = 0;
}, 10000).unref();
function getDueTasksAndFaultsAndOperations(deviceId, timestamp, callback) {
interface GetDueTasksAndFaultsAndOperationsCallback {
(
err: null,
tasks: any[],
faults: { [channel: string]: SessionFault },
operations: { [commandKey: string]: Operation },
ttl: number
): void;
(
err: Error,
tasks?: null,
faults?: null,
operations?: null,
ttl?: null
): void;
}
function getDueTasksAndFaultsAndOperations(
deviceId,
timestamp,
callback: GetDueTasksAndFaultsAndOperationsCallback
): void {
cache.get(`${deviceId}_tasks_faults_operations`, (err, res) => {
if (err) return void callback(err);
@@ -841,7 +907,8 @@ function getDueTasksAndFaultsAndOperations(deviceId, timestamp, callback) {
null,
res.tasks || [],
res.faults || {},
res.operations || {}
res.operations || {},
0
);
}
@@ -890,8 +957,12 @@ function cacheDueTasksAndFaultsAndOperations(
operations,
cacheUntil,
callback
) {
const v = {};
): void {
const v = {
tasks: null,
faults: null,
operations: null
};
if (tasks.length) v.tasks = tasks;
if (Object.keys(faults).length) v.faults = faults;
if (Object.keys(operations).length) v.operations = operations;
@@ -908,7 +979,7 @@ function cacheDueTasksAndFaultsAndOperations(
);
}
function processRequest(sessionContext, rpc) {
function processRequest(sessionContext, rpc): void {
if (rpc.cpeRequest) {
if (rpc.cpeRequest.name === "Inform") {
logger.accessInfo({
@@ -987,7 +1058,7 @@ function processRequest(sessionContext, rpc) {
}
}
function listener(httpRequest, httpResponse) {
export function listener(httpRequest, httpResponse): void {
stats.totalRequests += 1;
if (httpRequest.method !== "POST") {
@@ -1055,14 +1126,14 @@ function listener(httpRequest, httpResponse) {
offset += chunk.length;
}
function parsedRpc(sessionContext, rpc, parseWarnings) {
function parsedRpc(sessionContext, rpc, parseWarnings): void {
for (const w of parseWarnings) {
w.sessionContext = sessionContext;
w.rpc = rpc;
logger.accessWarn(w);
}
if (config.get("DEBUG", sessionContext.deviceId)) {
if (sessionContext.debug) {
const dump =
`# REQUEST ${new Date(Date.now())}\n` +
JSON.stringify(httpRequest.headers) +
@@ -1134,7 +1205,7 @@ function listener(httpRequest, httpResponse) {
if (sessionContext) {
if (
(rpc.cpeRequest && rpc.cpeRequest.name === "Inform") ||
!sessionContext.rpcRequest ^ !(rpc.cpeResponse || rpc.cpeFault)
!sessionContext.rpcRequest !== !(rpc.cpeResponse || rpc.cpeFault)
) {
logger.accessError({
message: "Bad session state",
@@ -1165,48 +1236,72 @@ function listener(httpRequest, httpResponse) {
stats.initiatedSessions += 1;
const deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId);
session.init(
deviceId,
rpc.cwmpVersion,
rpc.sessionTimeout || config.get("SESSION_TIMEOUT", deviceId),
(err, _sessionContext) => {
if (err) return void throwError(err, httpResponse);
localCache.getCurrentSnapshot((err, cacheSnapshot) => {
if (err) return void throwError(err, httpResponse);
_sessionContext.httpRequest = httpRequest;
_sessionContext.httpResponse = httpResponse;
_sessionContext.sessionId = crypto.randomBytes(8).toString("hex");
httpRequest.connection.setTimeout(_sessionContext.timeout * 1000);
const configContext = {
id: deviceId,
serialNumber: rpc.cpeRequest.deviceId["SerialNumber"],
productClass: rpc.cpeRequest.deviceId["ProductClass"],
oui: rpc.cpeRequest.deviceId["OUI"]
};
getDueTasksAndFaultsAndOperations(
deviceId,
_sessionContext.timestamp,
(err, dueTasks, faults, operations, cacheUntil) => {
if (err) return void throwError(err, httpResponse);
_sessionContext.tasks = dueTasks;
_sessionContext.operations = operations;
_sessionContext.cacheUntil = cacheUntil;
_sessionContext.faults = faults;
_sessionContext.retries = {};
for (const [k, v] of Object.entries(_sessionContext.faults)) {
if (v.expiry >= _sessionContext.timestamp) {
// Delete expired faults
delete _sessionContext.faults[k];
if (!_sessionContext.faultsTouched)
_sessionContext.faultsTouched = {};
_sessionContext.faultsTouched[k] = true;
} else {
_sessionContext.retries[k] = v.retries;
}
}
parsedRpc(_sessionContext, rpc, parseWarnings);
}
const sessionTimeout =
rpc.sessionTimeout ||
localCache.getConfig(
cacheSnapshot,
"cwmp.sessionTimeout",
configContext
);
}
);
session.init(
deviceId,
rpc.cwmpVersion,
sessionTimeout,
(err, _sessionContext) => {
if (err) return void throwError(err, httpResponse);
_sessionContext.cacheSnapshot = cacheSnapshot;
_sessionContext.configContext = configContext;
_sessionContext.debug = !!localCache.getConfig(
cacheSnapshot,
"cwmp.debug",
configContext
);
_sessionContext.httpRequest = httpRequest;
_sessionContext.httpResponse = httpResponse;
_sessionContext.sessionId = crypto.randomBytes(8).toString("hex");
httpRequest.connection.setTimeout(_sessionContext.timeout * 1000);
getDueTasksAndFaultsAndOperations(
deviceId,
_sessionContext.timestamp,
(err, dueTasks, faults, operations, cacheUntil) => {
if (err) return void throwError(err, httpResponse);
_sessionContext.tasks = dueTasks;
_sessionContext.operations = operations;
_sessionContext.cacheUntil = cacheUntil;
_sessionContext.faults = faults;
_sessionContext.retries = {};
for (const [k, v] of Object.entries(_sessionContext.faults)) {
if (v.expiry >= _sessionContext.timestamp) {
// Delete expired faults
delete _sessionContext.faults[k];
if (!_sessionContext.faultsTouched)
_sessionContext.faultsTouched = {};
_sessionContext.faultsTouched[k] = true;
} else {
_sessionContext.retries[k] = v.retries;
}
}
parsedRpc(_sessionContext, rpc, parseWarnings);
}
);
}
);
});
});
});
}
exports.listener = listener;
exports.onConnection = onConnection;
+252 -183
View File
@@ -16,13 +16,15 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const mongodb = require("mongodb");
const config = require("./config");
const common = require("./common");
import { MongoClient, ObjectID } from "mongodb";
import { get } from "./config";
import { escapeRegExp } from "./common";
import { parse } from "./common/expression-parser";
import { DeviceData, Attributes } from "./types";
import Path from "./common/path";
let tasksCollection,
export let tasksCollection,
devicesCollection,
presetsCollection,
objectsCollection,
@@ -30,47 +32,49 @@ let tasksCollection,
virtualParametersCollection,
faultsCollection,
filesCollection,
operationsCollection;
operationsCollection,
permissionsCollection,
usersCollection,
configCollection;
function connect(callback) {
mongodb.MongoClient.connect(
config.get("MONGODB_CONNECTION_URL"),
export let client;
export function connect(callback): void {
MongoClient.connect(
"" + get("MONGODB_CONNECTION_URL"),
{ useNewUrlParser: true },
(err, client) => {
(err, _client) => {
if (err) return void callback(err);
exports.client = client;
client = _client;
const db = client.db();
exports.tasksCollection = tasksCollection = db.collection("tasks");
tasksCollection = db.collection("tasks");
tasksCollection.createIndex({ device: 1, timestamp: 1 });
exports.devicesCollection = devicesCollection = db.collection("devices");
exports.presetsCollection = presetsCollection = db.collection("presets");
exports.objectsCollection = objectsCollection = db.collection("objects");
exports.filesCollection = filesCollection = db.collection("fs.files");
exports.provisionsCollection = provisionsCollection = db.collection(
"provisions"
);
exports.virtualParametersCollection = virtualParametersCollection = db.collection(
"virtualParameters"
);
exports.faultsCollection = faultsCollection = db.collection("faults");
exports.operationsCollection = operationsCollection = db.collection(
"operations"
);
devicesCollection = db.collection("devices");
presetsCollection = db.collection("presets");
objectsCollection = db.collection("objects");
filesCollection = db.collection("fs.files");
provisionsCollection = db.collection("provisions");
virtualParametersCollection = db.collection("virtualParameters");
faultsCollection = db.collection("faults");
operationsCollection = db.collection("operations");
permissionsCollection = db.collection("permissions");
usersCollection = db.collection("users");
configCollection = db.collection("config");
callback();
}
);
}
function disconnect() {
if (exports.client) exports.client.close();
export function disconnect(): void {
if (client) client.close();
}
// Optimize projection by removing overlaps
// This can modify the object
function optimizeProjection(obj) {
function optimizeProjection(obj: {}): {} {
if (obj[""]) return { "": obj[""] };
const keys = Object.keys(obj).sort();
@@ -89,55 +93,66 @@ function optimizeProjection(obj) {
return obj;
}
function fetchDevice(id, timestamp, patterns, callback) {
const MAX_DEPTH = config.get("MAX_DEPTH", id);
export function fetchDevice(
id: string,
timestamp: number,
patterns: [Path, number][],
callback
): void {
const MAX_DEPTH = +get("MAX_DEPTH", id);
if (!patterns || !patterns.length) patterns = [[[], (1 << MAX_DEPTH) - 1]];
if (!patterns || !patterns.length)
patterns = [[Path.parse(""), (1 << MAX_DEPTH) - 1]];
let projection = {};
const projectionTree = {};
function func(path, pats, projTree) {
const children = {};
function func(
path: string,
pathLength: number,
pats: [Path, number][],
projTree
): void {
const children: { [fragment: string]: [Path, number][] } = {};
for (const pat of pats) {
const fragment = pat[0][path.length] || "*";
const fragment = (pat[0].segments[pathLength] as string) || "*";
if (fragment === "*") {
if (pat[1] << path.length) projection[path.join(".")] = 1;
if (pat[1] << pathLength) projection[path.slice(0, -1)] = 1;
return;
}
if (!projTree[fragment]) projTree[fragment] = {};
if (pat[1] & (1 << path.length)) {
projection[path.concat("_timestamp").join(".")] = 1;
projection[path.concat("_object").join(".")] = 1;
projection[path.concat("_instance").join(".")] = 1;
projection[path.concat([fragment, "_timestamp"]).join(".")] = 1;
projection[path.concat([fragment, "_value"]).join(".")] = 1;
projection[path.concat([fragment, "_type"]).join(".")] = 1;
projection[path.concat([fragment, "_object"]).join(".")] = 1;
projection[path.concat([fragment, "_instance"]).join(".")] = 1;
projection[path.concat([fragment, "_writable"]).join(".")] = 1;
projection[path.concat([fragment, "_orig"]).join(".")] = 1;
if (pat[1] & (1 << pathLength)) {
projection[path + "_timestamp"] = 1;
projection[path + "_object"] = 1;
projection[path + "_instance"] = 1;
projection[path + fragment + "._timestamp"] = 1;
projection[path + fragment + "._value"] = 1;
projection[path + fragment + "._type"] = 1;
projection[path + fragment + "._object"] = 1;
projection[path + fragment + "._instance"] = 1;
projection[path + fragment + "._writable"] = 1;
projection[path + fragment + "._orig"] = 1;
}
if (pat[1] >> (path.length + 1)) {
if (pat[1] >> (pathLength + 1)) {
if (!children[fragment]) children[fragment] = [];
children[fragment].push(pat);
}
}
for (const [k, v] of Object.entries(children))
func(path.concat(k), v, projTree[k]);
func(path + k + ".", pathLength + 1, v, projTree[k]);
}
func([], patterns, projectionTree);
func("", 0, patterns, projectionTree);
delete projectionTree["DeviceID"];
delete projectionTree["Events"];
delete projectionTree["Tags"];
const res = [];
const loaded = [];
const res: [Path, number, Attributes?][] = [];
const loaded: [Path, number][] = [];
projection = optimizeProjection(projection);
@@ -145,38 +160,47 @@ function fetchDevice(id, timestamp, patterns, callback) {
if (k === "" || k === "Events" || k.startsWith("Events.")) {
if (k === "" || k === "Events" || k === "Events._writable") {
res.push([
["Events"],
Path.parse("Events"),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }
]);
if (k === "Events._writable") loaded.push([["Events"], 1]);
if (k === "Events._writable") loaded.push([Path.parse("Events"), 1]);
}
if (k === "Events") {
projection["_lastInform"] = 1;
projection["_lastBoot"] = 1;
projection["_lastBootstrap"] = 1;
projection["_registered"] = 1;
loaded.push([["Events"], (1 << MAX_DEPTH) - 1]);
loaded.push([Path.parse("Events"), (1 << MAX_DEPTH) - 1]);
} else if (k === "Events.Inform._writable" || k === "Events.Inform") {
projection["_lastInform"] = 1;
loaded.push([["Events", "Inform"], 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([Path.parse("Events.Inform"), 1 ^ ((1 << MAX_DEPTH) - 1)]);
} else if (k === "Events.1_BOOT._writable" || k === "Events.1_BOOT") {
projection["_lastBoot"] = 1;
loaded.push([["Events", "1_BOOT"], 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([Path.parse("Events.1_BOOT"), 1 ^ ((1 << MAX_DEPTH) - 1)]);
} else if (
k === "Events.0_BOOTSTRAP._writable" ||
k === "Events.0_BOOTSTRAP"
) {
projection["_lastBootstrap"] = 1;
loaded.push([["Events", "0_BOOTSTRAP"], 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([
Path.parse("Events.0_BOOTSTRAP"),
1 ^ ((1 << MAX_DEPTH) - 1)
]);
} else if (
k === "Events.Registered._writable" ||
k === "Events.Registered"
) {
projection["_registered"] = 1;
loaded.push([["Events", "Registered"], 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([
Path.parse("Events.Registered"),
1 ^ ((1 << MAX_DEPTH) - 1)
]);
} else if (k.endsWith("._writable") && k !== "Events._writable") {
loaded.push([k.split(".").slice(0, 2), 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([
Path.parse(k.split(".", 2).join(".")),
1 ^ ((1 << MAX_DEPTH) - 1)
]);
}
if (k !== "") delete projection[k];
}
@@ -184,42 +208,58 @@ function fetchDevice(id, timestamp, patterns, callback) {
if (k === "" || k === "DeviceID" || k.startsWith("DeviceID.")) {
if (k === "" || k === "DeviceID" || k === "DeviceID._writable") {
res.push([
["DeviceID"],
Path.parse("DeviceID"),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }
]);
if (k === "DeviceID._writable") loaded.push([["DeviceID"], 1]);
if (k === "DeviceID._writable")
loaded.push([Path.parse("DeviceID"), 1]);
}
if (k === "DeviceID") {
projection["_id"] = 1;
projection["_deviceId"] = 1;
loaded.push([["DeviceID"], (1 << MAX_DEPTH) - 1]);
loaded.push([Path.parse("DeviceID"), (1 << MAX_DEPTH) - 1]);
} else if (k === "DeviceID.ID._writable" || k === "DeviceID.ID") {
projection["_id"] = 1;
loaded.push([["DeviceID", "ID"], 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([Path.parse("DeviceID.ID"), 1 ^ ((1 << MAX_DEPTH) - 1)]);
} else if (
k === "DeviceID.Manufacturer._writable" ||
k === "DeviceID.DeManufacturer"
) {
projection["_deviceId._Manufacturer"] = 1;
loaded.push([["DeviceID", "Manufacturer"], 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([
Path.parse("DeviceID.Manufacturer"),
1 ^ ((1 << MAX_DEPTH) - 1)
]);
} else if (
k === "DeviceID.ProductClass._writable" ||
k === "DeviceID.ProductClass"
) {
projection["_deviceId._ProductClass"] = 1;
loaded.push([["DeviceID", "ProductClass"], 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([
Path.parse("DeviceID.ProductClass"),
1 ^ ((1 << MAX_DEPTH) - 1)
]);
} else if (k === "DeviceID.OUI._writable" || k === "DeviceID.OUI") {
projection["_deviceId._OUI"] = 1;
loaded.push([["DeviceID", "ProductClass"], 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([
Path.parse("DeviceID.ProductClass"),
1 ^ ((1 << MAX_DEPTH) - 1)
]);
} else if (
k === "DeviceID.SerialNumber._writable" ||
k === "DeviceID.SerialNumber"
) {
projection["_deviceId._SerialNumber"] = 1;
loaded.push([["DeviceID", "SerialNumber"], 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([
Path.parse("DeviceID.SerialNumber"),
1 ^ ((1 << MAX_DEPTH) - 1)
]);
} else if (k.endsWith("._writable") && k !== "DeviceID._writable") {
loaded.push([k.split(".").slice(0, 2), 1 ^ ((1 << MAX_DEPTH) - 1)]);
loaded.push([
Path.parse(k.split(".", 2).join(".")),
1 ^ ((1 << MAX_DEPTH) - 1)
]);
}
if (k !== "") delete projection[k];
}
@@ -227,49 +267,54 @@ function fetchDevice(id, timestamp, patterns, callback) {
if (k === "Tags" || k.startsWith("Tags.")) {
if (!projection["_tags"]) {
projection["_tags"] = 1;
loaded.push([["Tags"], (1 << MAX_DEPTH) - 1]);
loaded.push([Path.parse("Tags"), (1 << MAX_DEPTH) - 1]);
}
delete projection[k];
}
}
const options = {};
let options;
if (!projection[""]) {
if (!Object.keys(projection).length) options.projection = { _id: 1 };
else options.projection = projection;
if (!Object.keys(projection).length) options = { projection: { _id: 1 } };
else options = { projection: projection };
}
devicesCollection.findOne({ _id: id }, options, (err, device) => {
if (err || !device) return void callback(err);
function storeParams(obj, path, ts, descendantsFetched, projTree) {
function storeParams(
obj,
path: string,
pathLength: number,
ts,
descendantsFetched,
projTree
): void {
let thisFetched = false;
if (descendantsFetched) {
thisFetched = true;
} else if (projection[path.join(".")]) {
} else if (projection[path.slice(0, -1)]) {
descendantsFetched = true;
if (
path.length &&
pathLength &&
projection[
path
.slice(0, -1)
.concat("_timestamp")
.join(".")
path.slice(0, path.lastIndexOf(".", path.length - 2) + 1) +
"_timestamp"
]
) {
thisFetched = true;
loaded.push([
path,
((1 << (path.length - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1)
Path.parse(path.slice(0, -1)),
((1 << (pathLength - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1)
]);
} else {
loaded.push([
path,
((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)
Path.parse(path.slice(0, -1)),
((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1)
]);
}
} else if (projection[path.concat("_writable").join(".")]) {
loaded.push([path, 1 << (path.length - 1)]);
} else if (projection[path + "_writable"]) {
loaded.push([Path.parse(path.slice(0, -1)), 1 << (pathLength - 1)]);
thisFetched = true;
}
@@ -279,7 +324,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
if (obj["_instance"] && obj["_object"] == null) obj["_object"] = true;
if (thisFetched) {
const attrs = {};
const attrs: Attributes = {};
let t = obj["_timestamp"] || 1;
if (ts > t) t = ts;
@@ -295,7 +340,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0];
res.push([path, t, attrs]);
res.push([Path.parse(path.slice(0, -1)), t, attrs]);
}
for (const [k, v] of Object.entries(obj)) {
@@ -303,7 +348,8 @@ function fetchDevice(id, timestamp, patterns, callback) {
obj["_object"] = true;
storeParams(
v,
path.concat(k),
path + k + ".",
pathLength + 1,
obj["_timestamp"],
descendantsFetched,
projTree ? projTree[k] : null
@@ -315,14 +361,14 @@ function fetchDevice(id, timestamp, patterns, callback) {
if (!descendantsFetched) {
if (projTree) {
for (const k of Object.keys(projTree)) {
const p = path.concat(k);
loaded.push([p, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)]);
if ((obj["_object"] || !path.length) && obj["_timestamp"])
const p = Path.parse(path + k);
loaded.push([p, ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1)]);
if ((obj["_object"] || !pathLength) && obj["_timestamp"])
res.push([p, obj["_timestamp"]]);
}
}
} else if ((obj["_object"] || !path.length) && obj["_timestamp"]) {
res.push([path.concat("*"), obj["_timestamp"]]);
} else if ((obj["_object"] || !pathLength) && obj["_timestamp"]) {
res.push([Path.parse(path + "*"), obj["_timestamp"]]);
}
}
@@ -330,7 +376,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
switch (k) {
case "_lastInform":
res.push([
["Events", "Inform"],
Path.parse("Events.Inform"),
+v,
{
object: [+v, 0],
@@ -342,7 +388,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
break;
case "_lastBoot":
res.push([
["Events", "1_BOOT"],
Path.parse("Events.1_BOOT"),
+v,
{
object: [+v, 0],
@@ -354,7 +400,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
break;
case "_lastBootstrap":
res.push([
["Events", "0_BOOTSTRAP"],
Path.parse("Events.0_BOOTSTRAP"),
+v,
{
object: [+v, 0],
@@ -367,7 +413,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
case "_registered":
// Use current timestamp for registered event attribute timestamps
res.push([
["Events", "Registered"],
Path.parse("Events.Registered"),
timestamp,
{
object: [timestamp, 0],
@@ -380,12 +426,12 @@ function fetchDevice(id, timestamp, patterns, callback) {
case "_id":
if (projection[""] || projection["_id"]) {
res.push([
["DeviceID", "ID"],
Path.parse("DeviceID.ID"),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, 0],
value: [timestamp, [v, "xsd:string"]]
value: [timestamp, [v as string, "xsd:string"]]
}
]);
}
@@ -393,18 +439,18 @@ function fetchDevice(id, timestamp, patterns, callback) {
delete device[k];
break;
case "_tags":
if (v.length) {
if ((v as string[]).length) {
res.push([
["Tags"],
Path.parse("Tags"),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }
]);
}
for (let t of v) {
for (let t of v as string[]) {
t = t.replace(/[^a-zA-Z0-9-]+/g, "_");
res.push([
["Tags", t],
Path.parse("Tags." + t),
timestamp,
{
object: [timestamp, 0],
@@ -418,7 +464,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
case "_deviceId":
if (v["_Manufacturer"] != null) {
res.push([
["DeviceID", "Manufacturer"],
Path.parse("DeviceID.Manufacturer"),
timestamp,
{
object: [timestamp, 0],
@@ -430,7 +476,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
if (v["_OUI"] != null) {
res.push([
["DeviceID", "OUI"],
Path.parse("DeviceID.OUI"),
timestamp,
{
object: [timestamp, 0],
@@ -442,7 +488,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
if (v["_ProductClass"] != null) {
res.push([
["DeviceID", "ProductClass"],
Path.parse("DeviceID.ProductClass"),
timestamp,
{
object: [timestamp, 0],
@@ -454,7 +500,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
if (v["_SerialNumber"] != null) {
res.push([
["DeviceID", "SerialNumber"],
Path.parse("DeviceID.SerialNumber"),
timestamp,
{
object: [timestamp, 0],
@@ -468,50 +514,49 @@ function fetchDevice(id, timestamp, patterns, callback) {
}
}
storeParams(device, [], 0, false, projectionTree);
storeParams(device, "", 0, 0, false, projectionTree);
callback(null, res, loaded);
});
}
function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
export function saveDevice(
deviceId: string,
deviceData: DeviceData,
isNew: boolean,
sessionTimestamp: number,
callback
): void {
const update = { $set: {}, $unset: {}, $addToSet: {}, $pull: {} };
for (const diff of deviceData.timestamps.diff()) {
if (diff[0].wildcard !== 1 << (diff[0].length - 1)) continue;
if (
diff[0][0] === "Events" ||
diff[0][0] === "DeviceID" ||
diff[0][0] === "Tags"
diff[0].segments[0] === "Events" ||
diff[0].segments[0] === "DeviceID" ||
diff[0].segments[0] === "Tags"
)
continue;
const parent = deviceData.paths.get(diff[0].slice(0, -1));
// Param timestamps may be greater than session timestamp to track revisions
if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp;
if (diff[2] == null && diff[1] != null) {
update["$unset"][
diff[0]
.slice(0, -1)
.concat("_timestamp")
.join(".")
parent.length ? parent.toString() + "._timestamp" : "_timestamp"
] = 1;
} else if (diff[2] !== diff[1]) {
const parent = deviceData.paths.get(diff[0].slice(0, -1));
} else {
if (parent && (!parent.length || deviceData.attributes.has(parent))) {
update["$set"][
diff[0]
.slice(0, -1)
.concat("_timestamp")
.join(".")
parent.length ? parent.toString() + "._timestamp" : "_timestamp"
] = new Date(diff[2]);
}
}
}
for (const diff of deviceData.attributes.diff()) {
if (diff[1] === diff[2]) continue;
const path = diff[0];
const value1 = (((diff[1] || {}).value || [])[1] || [])[0];
const value2 = (((diff[2] || {}).value || [])[1] || [])[0];
@@ -524,11 +569,11 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
const writable2 = ((diff[2] || {}).writable || [])[1];
const writable1 = ((diff[1] || {}).writable || [])[1];
switch (path[0]) {
switch (path.segments[0]) {
case "Events":
if (diff[0].length === 2 && value2 !== value1) {
if (path.length === 2 && value2 !== value1) {
if (!diff[2]) {
switch (path[1]) {
switch (path.segments[1]) {
case "Inform":
update["$unset"]["_lastInform"] = 1;
break;
@@ -542,8 +587,8 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
update["$unset"]["_registered"] = 1;
}
} else {
const t = new Date(diff[2].value[1][0]);
switch (path[1]) {
const t = new Date(diff[2].value[1][0] as number);
switch (path.segments[1]) {
case "Inform":
update["$set"]["_lastInform"] = t;
break;
@@ -563,7 +608,7 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
case "DeviceID":
if (value2 !== value1) {
const v = diff[2].value[1][0];
switch (path[1]) {
switch (path.segments[1]) {
case "ID":
update["$set"]["_id"] = v;
break;
@@ -586,21 +631,21 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
if (value2 != null) {
if (!update["$addToSet"]["_tags"])
update["$addToSet"]["_tags"] = { $each: [] };
update["$addToSet"]["_tags"]["$each"].push(diff[0][1]);
update["$addToSet"]["_tags"]["$each"].push(path.segments[1]);
} else {
if (!update["$pull"]["_tags"]) {
update["$pull"]["_tags"] = {
$in: []
};
}
update["$pull"]["_tags"]["$in"].push(diff[0][1]);
update["$pull"]["_tags"]["$in"].push(path.segments[1]);
}
}
break;
default:
if (!diff[2]) {
update["$unset"][diff[0].join(".")] = 1;
update["$unset"][path.toString()] = 1;
continue;
}
@@ -615,35 +660,38 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
if (value2 !== value1) {
if (
valueType2 === "xsd:dateTime" &&
Number.isInteger(value2)
Number.isInteger(value2 as number)
) {
update["$set"][path.concat("_value").join(".")] = new Date(
value2
update["$set"][path.toString() + "._value"] = new Date(
value2 as number
);
} else {
update["$set"][path.concat("_value").join(".")] = value2;
update["$set"][path.toString() + "._value"] = value2;
}
}
if (valueType2 !== valueType1)
update["$set"][path.concat("_type").join(".")] = valueType2;
update["$set"][path.toString() + "._type"] = valueType2;
if (valueTimestamp2 !== valueTimestamp1) {
update["$set"][
path.concat("_timestamp").join(".")
] = new Date(valueTimestamp2);
update["$set"][path.toString() + "._timestamp"] = new Date(
valueTimestamp2
);
}
break;
case "object":
if (!diff[1] || !diff[1].object || object2 !== object1)
update["$set"][path.concat("_object").join(".")] = !!object2;
if (!diff[1] || !diff[1].object || object2 !== object1) {
update["$set"][
path.length ? path.toString() + "._object" : "_object"
] = !!object2;
}
break;
case "writable":
if (!diff[1] || !diff[1].writable || writable2 !== writable1) {
update["$set"][
path.concat("_writable").join(".")
path.length ? path.toString() + "._writable" : "_writable"
] = !!writable2;
}
}
@@ -656,10 +704,11 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
diff[1][attrName][1] != null &&
(!diff[2] || !diff[2][attrName] || diff[2][attrName][1] == null)
) {
update["$unset"][path.concat(`_${attrName}`).join(".")] = 1;
const p = path.length ? path.toString() + "." : "";
update["$unset"][`${p}_${attrName}`] = 1;
if (attrName === "value") {
update["$unset"][path.concat("_type").join(".")] = 1;
update["$unset"][path.concat("_timestamp").join(".")] = 1;
update["$unset"][p + "_type"] = 1;
update["$unset"][p + "_timestamp"] = 1;
}
}
}
@@ -721,9 +770,9 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
);
}
function getFaults(deviceId, callback) {
export function getFaults(deviceId, callback): void {
faultsCollection
.find({ _id: { $regex: `^${common.escapeRegExp(deviceId)}\\:` } })
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
.toArray((err, res) => {
if (err) return callback(err);
@@ -742,7 +791,7 @@ function getFaults(deviceId, callback) {
});
}
function saveFault(deviceId, channel, fault, callback) {
export function saveFault(deviceId, channel, fault, callback): void {
const id = `${deviceId}:${channel}`;
fault = Object.assign({}, fault);
fault._id = id;
@@ -753,11 +802,11 @@ function saveFault(deviceId, channel, fault, callback) {
faultsCollection.replaceOne({ _id: id }, fault, { upsert: true }, callback);
}
function deleteFault(deviceId, channel, callback) {
export function deleteFault(deviceId, channel, callback): void {
faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` }, callback);
}
function getDueTasks(deviceId, timestamp, callback) {
export function getDueTasks(deviceId, timestamp, callback): void {
const cur = tasksCollection.find({ device: deviceId }).sort(["timestamp"]);
const tasks = [];
@@ -779,8 +828,8 @@ function getDueTasks(deviceId, timestamp, callback) {
// For API compatibility
if (task.name === "download" && task.file) {
let q;
if (mongodb.ObjectID.isValid(task.file))
q = { _id: { $in: [task.file, new mongodb.ObjectID(task.file)] } };
if (ObjectID.isValid(task.file))
q = { _id: { $in: [task.file, new ObjectID(task.file)] } };
else q = { _id: task.file };
filesCollection.find(q).toArray((err, res) => {
@@ -802,16 +851,16 @@ function getDueTasks(deviceId, timestamp, callback) {
);
}
function clearTasks(deviceId, taskIds, callback) {
export function clearTasks(deviceId, taskIds, callback): void {
tasksCollection.deleteMany(
{ _id: { $in: taskIds.map(id => new mongodb.ObjectID(id)) } },
{ _id: { $in: taskIds.map(id => new ObjectID(id)) } },
callback
);
}
function getOperations(deviceId, callback) {
export function getOperations(deviceId, callback): void {
operationsCollection
.find({ _id: { $regex: `^${common.escapeRegExp(deviceId)}\\:` } })
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
.toArray((err, res) => {
if (err) return void callback(err);
@@ -830,7 +879,7 @@ function getOperations(deviceId, callback) {
});
}
function saveOperation(deviceId, commandKey, operation, callback) {
export function saveOperation(deviceId, commandKey, operation, callback): void {
const id = `${deviceId}:${commandKey}`;
operation = Object.assign({}, operation);
operation._id = id;
@@ -846,47 +895,67 @@ function saveOperation(deviceId, commandKey, operation, callback) {
);
}
function deleteOperation(deviceId, commandKey, callback) {
export function deleteOperation(deviceId, commandKey, callback): void {
operationsCollection.deleteOne(
{ _id: `${deviceId}:${commandKey}` },
callback
);
}
function getPresets(callback) {
export function getPresets(callback): void {
presetsCollection.find().toArray(callback);
}
function getObjects(callback) {
export function getObjects(callback): void {
objectsCollection.find().toArray(callback);
}
function getProvisions(callback) {
export function getProvisions(callback): void {
provisionsCollection.find().toArray(callback);
}
function getVirtualParameters(callback) {
export function getVirtualParameters(callback): void {
virtualParametersCollection.find().toArray(callback);
}
function getFiles(callback) {
export function getFiles(callback): void {
filesCollection.find().toArray(callback);
}
exports.connect = connect;
exports.disconnect = disconnect;
exports.fetchDevice = fetchDevice;
exports.saveDevice = saveDevice;
exports.getFaults = getFaults;
exports.saveFault = saveFault;
exports.deleteFault = deleteFault;
exports.clearTasks = clearTasks;
exports.saveOperation = saveOperation;
exports.deleteOperation = deleteOperation;
exports.getPresets = getPresets;
exports.getObjects = getObjects;
exports.getProvisions = getProvisions;
exports.getVirtualParameters = getVirtualParameters;
exports.getFiles = getFiles;
exports.getDueTasks = getDueTasks;
exports.getOperations = getOperations;
export function getConfig(callback): void {
configCollection.find().toArray((err, res) => {
if (err) return void callback(err);
callback(
null,
res.map(c => ({
id: c["_id"],
value: parse(c["value"])
}))
);
});
}
export function getPermissions(
callback: (
err?: Error,
permissions?: {
role: string;
resource: string;
access: number;
filter: string;
validate: string;
}[]
) => void
): void {
permissionsCollection.find().toArray(callback);
}
export function getUsers(
callback: (
err?: Error,
users?: { _id: string; password: string; salt: string; roles: string }[]
) => void
): void {
usersCollection.find().toArray(callback);
}
@@ -16,20 +16,20 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const config = require("./config");
const common = require("./common");
const device = require("./device");
const scheduling = require("./scheduling");
import Path from "./common/path";
import * as config from "./config";
import * as device from "./device";
import * as scheduling from "./scheduling";
const MAX_DEPTH = config.get("MAX_DEPTH");
const MAX_DEPTH = +config.get("MAX_DEPTH");
function refresh(sessionContext, provision, declarations) {
const path = common.parsePath(provision[1]).slice();
const l = path.length;
path.length = MAX_DEPTH;
path.fill("*", l);
export function refresh(sessionContext, provision, declarations): boolean {
const segments = Path.parse(provision[1]).segments.slice();
const l = segments.length;
segments.length = MAX_DEPTH;
segments.fill("*", l);
const path = Path.parse(segments.join("."));
const every = 1000 * (provision[2] || 1);
const offset = scheduling.variance(sessionContext.deviceId, every);
const t = scheduling.interval(sessionContext.timestamp, every, offset);
@@ -48,10 +48,9 @@ function refresh(sessionContext, provision, declarations) {
return true;
}
function value(sessionContext, provision, declarations) {
const path = common.parsePath(provision[1]);
export function value(sessionContext, provision, declarations): boolean {
declarations.push({
path: path,
path: Path.parse(provision[1]),
pathGet: 1,
pathSet: null,
attrGet: { value: 1 },
@@ -62,11 +61,9 @@ function value(sessionContext, provision, declarations) {
return true;
}
function tag(sessionContext, provision, declarations) {
const path = ["Tags", provision[1]];
export function tag(sessionContext, provision, declarations): boolean {
declarations.push({
path: path,
path: Path.parse(`Tags.${provision[1]}`),
pathGet: 1,
pathSet: null,
attrGet: { value: 1 },
@@ -77,9 +74,9 @@ function tag(sessionContext, provision, declarations) {
return true;
}
function reboot(sessionContext, provision, declarations) {
export function reboot(sessionContext, provision, declarations): boolean {
declarations.push({
path: ["Reboot"],
path: Path.parse("Reboot"),
pathGet: 1,
pathSet: null,
attrGet: { value: 1 },
@@ -90,9 +87,9 @@ function reboot(sessionContext, provision, declarations) {
return true;
}
function reset(sessionContext, provision, declarations) {
export function reset(sessionContext, provision, declarations): boolean {
declarations.push({
path: ["FactoryReset"],
path: Path.parse("FactoryReset"),
pathGet: 1,
pathSet: null,
attrGet: { value: 1 },
@@ -103,18 +100,15 @@ function reset(sessionContext, provision, declarations) {
return true;
}
function download(sessionContext, provision, declarations) {
export function download(sessionContext, provision, declarations): boolean {
const alias = [
["FileType"],
provision[1] || "",
["FileName"],
provision[2] || "",
["TargetFileName"],
provision[3] || ""
];
`FileType:${JSON.stringify(provision[1] || "")}`,
`FileName:${JSON.stringify(provision[2] || "")}`,
`TargetFileName:${JSON.stringify(provision[3] || "")}`
].join(",");
declarations.push({
path: ["Downloads", alias],
path: Path.parse(`Downloads.[${alias}]`),
pathGet: 1,
pathSet: 1,
attrGet: null,
@@ -123,7 +117,7 @@ function download(sessionContext, provision, declarations) {
});
declarations.push({
path: ["Downloads", alias, "Download"],
path: Path.parse(`Downloads.[${alias}].Download`),
pathGet: 1,
pathSet: null,
attrGet: { value: 1 },
@@ -134,18 +128,18 @@ function download(sessionContext, provision, declarations) {
return true;
}
function instances(
export function instances(
sessionContext,
provision,
declarations,
startRevision,
endRevision
) {
): boolean {
let count = Number(provision[2]);
if (Number.isNaN(count)) return true;
const path = common.parsePath(provision[1]);
const path = Path.parse(provision[1]);
if (provision[2][0] === "+" || provision[2][0] === "-") {
declarations.push({
@@ -178,11 +172,3 @@ function instances(
return true;
}
exports.refresh = refresh;
exports.value = value;
exports.tag = tag;
exports.reboot = reboot;
exports.reset = reset;
exports.download = download;
exports.instances = instances;
+96 -89
View File
@@ -16,7 +16,9 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
import Path from "./common/path";
import { DeviceData, Declaration, Clear } from "./types";
const CHANGE_FLAGS = {
object: 2,
@@ -24,7 +26,7 @@ const CHANGE_FLAGS = {
value: 8
};
function parseBool(v) {
function parseBool(v): boolean {
v = "" + v;
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
@@ -32,14 +34,16 @@ function parseBool(v) {
else return null;
}
function sanitizeParameterValue(parameterValue) {
export function sanitizeParameterValue(
parameterValue: [string | number | boolean, string]
): [string | number | boolean, string?] {
if (parameterValue[0] != null) {
switch (parameterValue[1]) {
case "xsd:boolean":
if (typeof parameterValue[0] !== "boolean") {
let v = parseBool(parameterValue[0]);
if (v == null) v = "" + parameterValue[0];
parameterValue[0] = v;
const b = parseBool(parameterValue[0]);
if (b == null) parameterValue[0] = "" + parameterValue[0];
else parameterValue[0] = b;
}
break;
case "xsd:string":
@@ -52,20 +56,22 @@ function sanitizeParameterValue(parameterValue) {
case "xsd:int":
case "xsd:unsignedInt":
if (typeof parameterValue[0] !== "number") {
let v = parseInt(parameterValue[0]);
if (isNaN(v)) v = "" + parameterValue[0];
parameterValue[0] = v;
const i = parseInt(parameterValue[0] as string);
if (isNaN(i)) parameterValue[0] = "" + parameterValue[0];
else parameterValue[0] = i;
}
break;
case "xsd:dateTime":
if (typeof parameterValue[0] !== "number") {
// Don't use parseInt because it reads date string as a number
let v = +parameterValue[0];
if (isNaN(v)) {
v = Date.parse(parameterValue[0]);
if (isNaN(v)) v = "" + parameterValue[0];
let i = +parameterValue[0];
if (isNaN(i)) {
i = Date.parse(parameterValue[0] as string);
if (isNaN(i)) parameterValue[0] = "" + parameterValue[0];
else parameterValue[0] = i;
} else {
parameterValue[0] = i;
}
parameterValue[0] = v;
}
break;
default:
@@ -83,85 +89,79 @@ function sanitizeParameterValue(parameterValue) {
return parameterValue;
}
function getAliasDeclarations(path, timestamp) {
var decs = [];
function recursive(p, prefix) {
const pattern = prefix.concat(p);
for (let i = prefix.length; i < pattern.length; ++i) {
if (Array.isArray(pattern[i])) {
const pat = pattern[i];
pattern[i] = "*";
for (let j = 0; j < pat.length; j += 2)
recursive(pat[j], pattern.slice(0, i + 1));
}
}
decs.push({
path: pattern,
export function getAliasDeclarations(
path: Path,
timestamp: number,
attrGet = null
): Declaration[] {
const stripped = path.stripAlias();
let decs: Declaration[] = [
{
path: stripped,
pathGet: timestamp,
pathSet: null,
attrGet: prefix.length ? { value: timestamp } : null,
attrGet: attrGet,
attrSet: null,
defer: true
});
}
}
];
recursive(path, []);
if (path.alias) {
for (const [i, alias] of path.segments.entries()) {
if (Array.isArray(alias)) {
const parent = stripped.slice(0, i + 1);
for (const [p] of alias as [Path, string][]) {
decs = decs.concat(
getAliasDeclarations(parent.concat(p), timestamp, {
value: timestamp
})
);
}
}
}
}
return decs;
}
function unpack(deviceData, path, revision) {
let allMatches = [];
export function unpack(
deviceData: DeviceData,
path: Path,
revision?: number
): Path[] {
let allMatches = [] as Path[];
if (!path.alias) {
for (const p of deviceData.paths.find(path, false, true))
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
} else {
const wildcardPath = path.slice();
wildcardPath.wildcard = path.wildcard;
for (let i = 0; i < wildcardPath.length; ++i) {
if (Array.isArray(wildcardPath[i])) {
wildcardPath[i] = "*";
wildcardPath.wildcard |= 1 << i;
}
}
const wildcardPath = path.stripAlias();
for (const p of deviceData.paths.find(wildcardPath, false, true))
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
for (let i = path.length - 1; i >= 0; --i) {
if (Array.isArray(path[i])) {
for (let j = 0; j < path[i].length; j += 2) {
const p = wildcardPath.slice(0, i + 1).concat(path[i][j]);
p.alias =
((wildcardPath.alias & ((1 << (i + 1)) - 1)) + path[i][j].alias) <<
(i + 1);
p.wildcard =
((wildcardPath.wildcard & ((1 << (i + 1)) - 1)) +
path[i][j].wildcard) <<
(i + 1);
if (path.alias & (i << i)) {
for (const [param, val] of path.segments[i] as [Path, string][]) {
const p = wildcardPath.slice(0, i + 1).concat(param);
const unpacked = unpack(deviceData, p, revision);
const filtered = [];
const filtered: Path[] = [];
for (const up of unpacked) {
const attributes = deviceData.attributes.get(up, revision);
if (
attributes &&
attributes.value &&
attributes.value[1] &&
sanitizeParameterValue([
path[i][j + 1],
attributes.value[1][1]
])[0] === attributes.value[1][0]
sanitizeParameterValue([val, attributes.value[1][1]])[0] ===
attributes.value[1][0]
) {
for (let m = 0; m < allMatches.length; ++m) {
let k;
for (k = i; k >= 0; --k) if (allMatches[m][k] !== up[k]) break;
for (k = i; k >= 0; --k)
if (allMatches[m].segments[k] !== up.segments[k]) break;
if (k < 0) {
filtered.push(allMatches[m]);
allMatches[m] = [];
allMatches[m] = null;
}
}
}
@@ -172,21 +172,19 @@ function unpack(deviceData, path, revision) {
}
}
allMatches.sort((a, b) => {
for (let i = 0; i < a.length; ++i) {
if (a === b) continue;
allMatches.sort((p1, p2) => {
for (let i = 0; i < p1.length; ++i) {
const a = p1.segments[i] as string;
const b = p2.segments[i] as string;
if (a !== b) {
// Use numeric sorting for numbers
const ia = parseInt(a);
const ib = parseInt(b);
// Use numeric sorting for numbers
const ia = parseInt(a);
const ib = parseInt(b);
if (ia.toString() === a && ib.toString() === b) {
a = ia;
b = ib;
if (ia === +a && ib === +b) return ia - ib;
else if (a < b) return -1;
else return 1;
}
if (a < b) return -1;
else return 1;
}
return 0;
});
@@ -194,7 +192,13 @@ function unpack(deviceData, path, revision) {
return allMatches;
}
function clear(deviceData, path, timestamp, attributes, changeFlags = 0) {
export function clear(
deviceData,
path,
timestamp,
attributes,
changeFlags = 0
): void {
const changeTrackers = {};
timestamp = timestamp || 0;
@@ -253,8 +257,8 @@ function clear(deviceData, path, timestamp, attributes, changeFlags = 0) {
if (changeTrackers[k] & changeFlags) deviceData.changes.add(k);
}
function compareEquality(a, b) {
var t = typeof a;
function compareEquality(a, b): boolean {
const t = typeof a;
if (
a === null ||
a === undefined ||
@@ -268,7 +272,13 @@ function compareEquality(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
function set(deviceData, path, timestamp, attributes, toClear) {
export function set(
deviceData: DeviceData,
path: Path,
timestamp,
attributes,
toClear?: Clear[]
): Clear[] {
path = deviceData.paths.add(path);
const currentTimestamp = deviceData.timestamps.get(path);
@@ -366,7 +376,12 @@ function set(deviceData, path, timestamp, attributes, toClear) {
return toClear;
}
function track(deviceData, path, marker, attributes) {
export function track(
deviceData: DeviceData,
path: Path,
marker: string,
attributes?
): void {
path = deviceData.paths.add(path);
let f = 1;
@@ -382,7 +397,7 @@ function track(deviceData, path, marker, attributes) {
cur[marker] |= f;
}
function clearTrackers(deviceData, tracker) {
export function clearTrackers(deviceData: DeviceData, tracker): void {
if (Array.isArray(tracker)) {
for (const v of deviceData.trackers.values())
for (const t of tracker) delete v[t];
@@ -392,11 +407,3 @@ function clearTrackers(deviceData, tracker) {
deviceData.changes.delete(tracker);
}
}
exports.sanitizeParameterValue = sanitizeParameterValue;
exports.getAliasDeclarations = getAliasDeclarations;
exports.unpack = unpack;
exports.set = set;
exports.clear = clear;
exports.track = track;
exports.clearTrackers = clearTrackers;
-89
View File
@@ -1,89 +0,0 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
"use strict";
const jobs = new Set();
const fileName = process.argv[2];
let script;
function errorToFault(err) {
if (!err) return null;
if (!err.name) return { code: "ext", message: `${err}` };
const fault = {
code: `ext.${err.name}`,
message: err.message,
detail: {
name: err.name,
message: err.message
}
};
if (err.stack) {
fault.detail.stack = err.stack;
// Trim the stack trace
const stackTrimIndex = fault.detail.stack.match(
/\s+at\s[^\s]+\s\(.*extension-wrapper\.js:.+\)/
);
if (stackTrimIndex)
fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index);
}
return fault;
}
process.on("uncaughtException", err => {
const fault = errorToFault(err);
jobs.forEach(jobId => {
process.send([jobId, fault]);
});
jobs.clear();
process.disconnect();
});
process.on("message", message => {
jobs.add(message[0]);
if (!script) {
const cwd = process.env["GENIEACS_CONFIG_DIR"] + "/ext";
process.chdir(cwd);
script = require(`${cwd}/${fileName}`);
}
const funcName = message[1][0];
if (!script[funcName]) {
const fault = {
code: "ext",
message: `No such function '${funcName}' in extension '${fileName}'`
};
process.send([message[0], fault]);
return;
}
script[funcName](message[1].slice(1), (err, res) => {
if (!jobs.delete(message[0])) return;
process.send([message[0], errorToFault(err), res]);
});
});
// Ignore SIGINT
process.on("SIGINT", () => {});
+24 -23
View File
@@ -16,35 +16,41 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const childProcess = require("child_process");
const crypto = require("crypto");
import { spawn } from "child_process";
import * as crypto from "crypto";
import * as config from "./config";
const config = require("./config");
const TIMEOUT = config.get("EXT_TIMEOUT");
const TIMEOUT = +config.get("EXT_TIMEOUT");
const processes = {};
const jobs = {};
const jobs = new Map();
function messageHandler(message) {
const func = jobs[message[0]];
function messageHandler(message): void {
const func = jobs.get(message[0]);
if (func) {
delete jobs[message[0]];
jobs.delete(message[0]);
func(null, message[1], message[2]);
}
}
function run(args, callback) {
export function run(args, callback): boolean {
const scriptName = args[0];
const id = crypto.randomBytes(8).toString("hex");
jobs.set(id, callback);
if (!processes[scriptName]) {
const p = childProcess.fork("lib/extension-wrapper", [scriptName]);
const p = spawn("bin/genieacs-ext", [scriptName], {
stdio: ["inherit", "inherit", "inherit", "ipc"]
});
processes[scriptName] = p;
p.on("error", () => {
p.on("error", err => {
if (processes[scriptName] === p) {
if (jobs.delete(id))
callback(null, { code: err.name, message: err.message });
kill(processes[scriptName]);
delete processes[scriptName];
}
@@ -60,19 +66,17 @@ function run(args, callback) {
p.on("message", messageHandler);
}
const id = crypto.randomBytes(8).toString("hex");
jobs[id] = callback;
setTimeout(() => {
if (id in jobs) {
delete jobs[id];
if (jobs.delete(id))
callback(null, { code: "timeout", message: "Extension timed out" });
}
}, TIMEOUT);
if (!processes[scriptName].connected) return false;
return processes[scriptName].send([id, args.slice(1)]);
}
function kill(process, callback) {
function kill(process, callback?: () => void): void {
if (process.signalCode !== null || process.exitCode !== null) return;
const timeToKill = Date.now() + 5000;
@@ -91,7 +95,7 @@ function kill(process, callback) {
}, 100);
}
function killAll(callback) {
export function killAll(callback?): void {
let counter = 1;
for (const n in processes) {
++counter;
@@ -102,6 +106,3 @@ function killAll(callback) {
}
if (--counter === 0 && callback) callback();
}
exports.run = run;
exports.killAll = killAll;
+7 -10
View File
@@ -16,14 +16,13 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const url = require("url");
const querystring = require("querystring");
const mongodb = require("mongodb");
const db = require("./db");
import * as url from "url";
import * as querystring from "querystring";
import { GridFSBucket } from "mongodb";
import * as db from "./db";
function listener(request, response) {
export function listener(request, response): void {
const urlParts = url.parse(request.url, true);
if (request.method === "GET") {
const filename = querystring.unescape(urlParts.pathname.substring(1));
@@ -45,8 +44,8 @@ function listener(request, response) {
"Content-Length": file.length
});
const bucket = new mongodb.GridFSBucket(db.client.db());
const downloadStream = bucket.openDownloadStream(filename);
const bucket = new GridFSBucket(db.client.db());
const downloadStream = bucket.openDownloadStreamByName(filename);
downloadStream.pipe(response);
});
} else {
@@ -54,5 +53,3 @@ function listener(request, response) {
response.end("405 Method Not Allowed");
}
}
exports.listener = listener;
@@ -16,7 +16,6 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const WILDCARD_MULTIPLIER = 2;
const UNDISCOVERED_DEPTH = 7;
@@ -28,7 +27,7 @@ const UNDISCOVERED_DEPTH = 7;
// flags is an int where its bits mark the segments in the pattern that
// need refreshing. Leading 0s indicate that the pattern up to that
// point has been discovered.
function estimateGpnCount(gpnPatterns, depth = 0) {
export function estimateGpnCount(gpnPatterns, depth = 0): number {
const children = {};
const wildcardChildren = [];
let wildcardDiscovered = false;
@@ -77,5 +76,3 @@ function estimateGpnCount(gpnPatterns, depth = 0) {
return gpnCount;
}
exports.estimateGpnCount = estimateGpnCount;
+18 -15
View File
@@ -16,22 +16,27 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
class InstanceSet {
constructor() {
interface Instance {
[name: string]: string;
}
export default class InstanceSet {
private set: Set<Instance>;
public constructor() {
this.set = new Set();
}
add(instance) {
public add(instance: Instance): void {
this.set.add(instance);
}
delete(instance) {
public delete(instance: Instance): void {
this.set.delete(instance);
}
superset(instance) {
public superset(instance: Instance): Instance[] {
const res = [];
for (const inst of this.set) {
let match = true;
@@ -67,7 +72,7 @@ class InstanceSet {
return res;
}
subset(instance) {
public subset(instance: Instance): Instance[] {
const res = [];
for (const inst of this.set) {
@@ -104,25 +109,23 @@ class InstanceSet {
return res;
}
[Symbol.iterator]() {
public [Symbol.iterator](): IterableIterator<Instance> {
return this.set.values();
}
forEach(callback) {
return this.set.forEach(callback);
public forEach(callback: (instance: Instance) => void): void {
this.set.forEach(callback);
}
values() {
public values(): IterableIterator<Instance> {
return this.set.values();
}
clear() {
public clear(): void {
this.set.clear();
}
get size() {
public get size(): number {
return this.set.size;
}
}
module.exports = InstanceSet;
+254 -31
View File
@@ -16,16 +16,30 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const vm = require("vm");
const crypto = require("crypto");
import * as vm from "vm";
import * as crypto from "crypto";
import * as config from "./config";
import * as db from "./db";
import * as cache from "./cache";
import { mongoQueryToFilter } from "./mongodb-functions";
import * as logger from "./logger";
import * as scheduling from "./scheduling";
import * as expression from "./common/expression";
import { parse } from "./common/expression-parser";
import { Preset, Expression } from "./types";
const db = require("./db");
const cache = require("./cache");
const query = require("./query");
const logger = require("./logger");
const scheduling = require("./scheduling");
interface Permissions {
[role: string]: {
[access: number]: {
[resource: string]: {
access: number;
filter: Expression;
validate: { [validator: string]: boolean };
};
};
};
}
const REFRESH = 3000;
const EVICT_TIMEOUT = 60000;
@@ -34,7 +48,7 @@ const snapshots = new Map();
let currentSnapshot = null;
let nextRefresh = 1;
function computeHash(snapshot) {
function computeHash(snapshot): string {
// MD5 hash for presets, provisions, virtual parameters for detecting changes
const h = crypto.createHash("md5");
for (const p of snapshot.presets) {
@@ -56,10 +70,35 @@ function computeHash(snapshot) {
h.update(JSON.stringify(keys));
for (const k of keys) h.update(snapshot.virtualParameters[k].md5);
keys = Object.keys(snapshot.config).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(JSON.stringify(snapshot.config[k]));
keys = Object.keys(snapshot.files).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(JSON.stringify(snapshot.files[k]));
keys = Object.keys(snapshot.users).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(JSON.stringify(snapshot.users[k]));
const roles = Object.keys(snapshot.permissions).sort();
h.update(JSON.stringify(roles));
for (const r of roles) {
const levels = Object.keys(snapshot.permissions[r]).sort();
h.update(JSON.stringify(levels));
for (const l of levels) {
keys = Object.keys(snapshot.permissions[r][l]).sort();
h.update(JSON.stringify(keys));
for (const k of keys)
h.update(JSON.stringify(snapshot.permissions[r][l][k]));
}
}
return h.digest("hex");
}
function flattenObject(src, prefix = "", dst = {}) {
function flattenObject(src, prefix = "", dst = {}): {} {
for (const k of Object.keys(src)) {
const v = src[k];
if (typeof v === "object" && !Array.isArray(v))
@@ -69,7 +108,7 @@ function flattenObject(src, prefix = "", dst = {}) {
return dst;
}
function refresh(callback) {
function refresh(callback): void {
if (!nextRefresh) {
return void setTimeout(() => {
refresh(callback);
@@ -120,7 +159,7 @@ function refresh(callback) {
else return a.weight - b.weight;
});
const presets = [];
const presets = [] as Preset[];
for (const preset of res) {
let schedule = null;
if (preset.schedule) {
@@ -146,9 +185,15 @@ function refresh(callback) {
}
const events = preset.events || {};
const precondition = query.convertMongoQueryToFilters(
JSON.parse(preset.precondition)
);
let precondition;
try {
precondition = parse(preset.precondition);
} catch (error) {
precondition = mongoQueryToFilter(
JSON.parse(preset.precondition)
);
}
const _provisions = preset.provisions || [];
// Generate provisions from the old configuration format
@@ -296,13 +341,107 @@ function refresh(callback) {
})
);
promises.push(
new Promise((resolve, reject) => {
db.getPermissions((err, perms) => {
if (err) return void reject(err);
const permissions: Permissions = {};
for (const p of perms) {
if (!permissions[p.role]) permissions[p.role] = {};
if (!permissions[p.role][p.access])
permissions[p.role][p.access] = {};
const validate = {};
permissions[p.role][p.access][p.resource] = {
access: p.access,
filter: parse(p.filter || "true"),
validate: validate
};
for (const v of (p.validate || "").split(","))
validate[v.trim()] = true;
}
resolve(permissions);
});
})
);
promises.push(
new Promise((resolve, reject) => {
db.getUsers((err, _users) => {
if (err) return void reject(err);
const users = {};
for (const user of _users) {
users[user._id] = {
password: user.password,
salt: user.salt,
roles: user.roles.split(",").map(s => s.trim())
};
}
resolve(users);
});
})
);
promises.push(
new Promise((resolve, reject) => {
db.getConfig((err, conf) => {
if (err) return void reject(err);
conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0));
const ui = {
filters: {},
device: {},
index: {},
overview: { charts: {}, groups: {} }
};
const _config = {};
for (const c of conf) {
// Evaluate expressions to simplify them
const val = expression.evaluate(c.value);
_config[c.id] = val;
if (c.id.startsWith("ui.")) {
const keys = c.id.split(".");
// remove the first key(ui)
keys.shift();
let ref = ui;
while (keys.length > 1) {
const k = keys.shift();
if (typeof ref[k] !== "object") ref[k] = {};
ref = ref[k];
}
ref[keys[0]] = val;
}
}
if (!Object.keys(ui["index"]).length) {
ui["index"] = {
"0": {
label: "ID",
parameter: ["PARAM", "DeviceID.ID"]
}
};
}
resolve([_config, ui]);
});
})
);
Promise.all(promises)
.then(res => {
const snapshot = {
presets: res[0],
provisions: res[1],
virtualParameters: res[2],
files: res[3]
files: res[3],
permissions: res[4],
users: res[5],
config: res[6][0],
ui: res[6][1]
};
if (currentSnapshot) {
@@ -327,44 +466,128 @@ function refresh(callback) {
});
}
function getCurrentSnapshot(callback) {
if (Date.now() < nextRefresh) return void callback(null, currentSnapshot);
refresh(err => {
callback(err, currentSnapshot);
interface GetCurrentSnapshotCallback {
(err: null, currentSnapshot: string): void;
(err: Error, currentSnapshot?: null): void;
}
export function getCurrentSnapshot(callback: GetCurrentSnapshotCallback);
export function getCurrentSnapshot(): Promise<string>;
export function getCurrentSnapshot(
callback?: GetCurrentSnapshotCallback
): Promise<string> | void {
if (Date.now() < nextRefresh) {
if (callback) return callback(null, currentSnapshot);
return Promise.resolve(currentSnapshot);
}
if (callback) {
return refresh(err => {
callback(err, currentSnapshot);
});
}
return new Promise((resolve, reject) => {
refresh(err => {
if (err) return void reject(err);
resolve(currentSnapshot);
});
});
}
function hasSnapshot(hash) {
export function hasSnapshot(hash): boolean {
return snapshots.has(hash);
}
function getPresets(snapshotKey) {
export function getPresets(snapshotKey): Preset[] {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.presets;
}
function getProvisions(snapshotKey) {
export function getProvisions(
snapshotKey
): { [name: string]: { md5: string; script: vm.Script } } {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.provisions;
}
function getVirtualParameters(snapshotKey) {
export function getVirtualParameters(
snapshotKey
): { [name: string]: { md5: string; script: vm.Script } } {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.virtualParameters;
}
function getFiles(snapshotKey) {
export function getFiles(
snapshotKey
): { [name: string]: { length: number; md5: string; contentType: string } } {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.files;
}
exports.getCurrentSnapshot = getCurrentSnapshot;
exports.hasSnapshot = hasSnapshot;
exports.getPresets = getPresets;
exports.getProvisions = getProvisions;
exports.getVirtualParameters = getVirtualParameters;
exports.getFiles = getFiles;
export function getConfig(
snapshotKey,
key,
context?,
now?
): string | number | boolean | null {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
const oldOpts = {
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
"cwmp.debug": "DEBUG",
"cwmp.retryDelay": "RETRY_DELAY",
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
"cwmp.cookiesPath": "COOKIES_PATH",
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
"cwmp.connectionRequestAllowBasicAuth":
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT"
};
if (!(key in snapshot.config)) {
if (key in oldOpts)
return config.get(oldOpts[key], context ? context.id : null);
else return null;
}
return expression.evaluate(snapshot.config[key], context, now || Date.now());
}
export function getConfigExpression(snapshotKey, key): Expression {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.config[key];
}
export function getUsers(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.users;
}
export function getPermissions(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.permissions;
}
export function getUiConfig(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.ui;
}
+48 -32
View File
@@ -16,32 +16,46 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const fs = require("fs");
const os = require("os");
import * as fs from "fs";
import * as os from "os";
const config = require("./config");
import * as config from "./config";
const REOPEN_EVERY = 60000;
const LOG_FORMAT = config.get("LOG_FORMAT");
const ACCESS_LOG_FORMAT = config.get("ACCESS_LOG_FORMAT") || LOG_FORMAT;
const defaultMeta = {};
const defaultMeta: { [name: string]: any } = {};
let LOG_SYSTEMD = false;
let ACCESS_LOG_SYSTEMD = false;
let LOG_FILE, ACCESS_LOG_FILE;
declare global {
/* eslint-disable-next-line @typescript-eslint/no-namespace */
namespace NodeJS {
export interface WritableStream {
fd?: number;
}
}
}
declare module "fs" {
interface WriteStream {
fd?: number;
}
}
let logStream = fs.createWriteStream(null, { fd: process.stderr.fd });
let logStat = fs.fstatSync(logStream.fd);
let accessLogStream = fs.createWriteStream(null, { fd: process.stdout.fd });
let accessLogStat = fs.fstatSync(accessLogStream.fd);
// Reopen if original files have been moved (e.g. logrotate)
function reopen() {
function reopen(): void {
let counter = 1;
if (LOG_FILE) {
@@ -90,7 +104,7 @@ function reopen() {
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
}
function init(service, version) {
export function init(service, version): void {
defaultMeta.hostname = os.hostname();
defaultMeta.pid = process.pid;
defaultMeta.name = `genieacs-${service}`;
@@ -115,9 +129,7 @@ function init(service, version) {
const JOURNAL_STREAM = process.env["JOURNAL_STREAM"];
if (JOURNAL_STREAM) {
let [dev, inode] = JOURNAL_STREAM.split(":");
dev = parseInt(dev);
inode = parseInt(inode);
const [dev, inode] = JOURNAL_STREAM.split(":").map(parseInt);
LOG_SYSTEMD = logStat.dev === dev && logStat.ino === inode;
ACCESS_LOG_SYSTEMD =
@@ -129,12 +141,12 @@ function init(service, version) {
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
}
function close() {
export function close(): void {
accessLogStream.end();
logStream.end();
}
function flatten(details) {
export function flatten(details): {} {
if (details.sessionContext) {
details.deviceId = details.sessionContext.deviceId;
details.remoteAddress =
@@ -184,10 +196,21 @@ function flatten(details) {
delete details.fault;
}
// For genieacs-ui
if (details.context) {
details.remoteAddress = details.context.request.ip;
if (details.context.state.user)
details.user = details.context.state.user.username;
delete details.context;
}
for (const [k, v] of Object.entries(details))
if (v == null) delete details[k];
return details;
}
function formatJson(details, systemd) {
function formatJson(details, systemd): string {
if (systemd) {
let severity = "";
if (details.severity === "info") severity = "<6>";
@@ -200,19 +223,22 @@ function formatJson(details, systemd) {
return `${JSON.stringify(flatten(details))}${os.EOL}`;
}
function formatSimple(details, systemd) {
function formatSimple(details, systemd): string {
flatten(details);
let remote = "";
if (details.remoteAddress) {
if (details.deviceId)
remote = `${details.remoteAddress} ${details.deviceId}: `;
else if (details.user)
remote = `${details.user}@${details.remoteAddress}: `;
else remote = `${details.remoteAddress}: `;
}
const keys = Object.keys(details);
const skip = {
deviceId: 1,
user: 1,
remoteAddress: 1,
severity: 1,
timestamp: 1,
@@ -241,7 +267,7 @@ function formatSimple(details, systemd) {
}${meta}${os.EOL}`;
}
function log(details) {
function log(details): void {
details.timestamp = new Date().toISOString();
if (LOG_FORMAT === "json") {
details = Object.assign({}, defaultMeta, details);
@@ -251,22 +277,22 @@ function log(details) {
}
}
function info(details) {
export function info(details): void {
details.severity = "info";
log(details);
}
function warn(details) {
export function warn(details): void {
details.severity = "warn";
log(details);
}
function error(details) {
export function error(details): void {
details.severity = "error";
log(details);
}
function accessLog(details) {
export function accessLog(details): void {
details.timestamp = new Date().toISOString();
if (ACCESS_LOG_FORMAT === "json") {
Object.assign(details, defaultMeta);
@@ -276,27 +302,17 @@ function accessLog(details) {
}
}
function accessInfo(details) {
export function accessInfo(details): void {
details.severity = "info";
accessLog(details);
}
function accessWarn(details) {
export function accessWarn(details): void {
details.severity = "warn";
accessLog(details);
}
function accessError(details) {
export function accessError(details): void {
details.severity = "error";
accessLog(details);
}
exports.init = init;
exports.close = close;
exports.flatten = flatten;
exports.info = info;
exports.warn = warn;
exports.error = error;
exports.accessInfo = accessInfo;
exports.accessWarn = accessWarn;
exports.accessError = accessError;
+609
View File
@@ -0,0 +1,609 @@
import { ObjectID } from "mongodb";
import { map, parse, stringify } from "./common/expression-parser";
import { likePatternToRegExp, evaluate } from "./common/expression";
import { Expression, Fault, Task } from "./types";
const isArray = Array.isArray;
export function processDeviceFilter(filter): Expression {
return map(filter, exp => {
if (!isArray(exp)) return exp;
if (exp[0] === "PARAM") {
const p = exp[1];
if (p === "DeviceID.ID") return ["PARAM", "_id"];
else if (p === "DeviceID") return ["PARAM", "_deviceId"];
else if (p.startsWith("DeviceID."))
return ["PARAM", "_deviceId._" + p.slice(9)];
else if (p === "Tags") return ["PARAM", "_tags"];
else if (p === "tag") return ["PARAM", "_tags"];
else if (p === "Events.Inform") return ["PARAM", "_lastInform"];
else if (p === "Events.Registered") return ["PARAM", "_registered"];
else if (p === "Events.0_BOOTSTRAP") return ["PARAM", "_lastBootstrap"];
else if (p === "Events.1_BOOT") return ["PARAM", "_lastBoot"];
} else if (
isArray(exp[1]) &&
exp[1][0] === "PARAM" &&
exp[1][1].startsWith("Tags.")
) {
const t = exp[1][1].slice(5);
if (exp[0] === "IS NULL") return ["<>", ["PARAM", "_tags"], t];
else if (exp[0] === "IS NOT NULL") return ["=", ["PARAM", "_tags"], t];
else if (exp[0] === "=" && exp[2] === true)
return ["=", ["PARAM", "_tags"], t];
} else if (
["=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"].includes(exp[0])
) {
let e = map(exp, ee => {
if (
isArray(ee) &&
ee[0] === "PARAM" &&
typeof ee[1] === "string" &&
!ee[1].startsWith("_")
)
return ["PARAM", `${ee[1]}._value`];
return ee;
});
if (typeof e[2] === "number") {
const alt = (e as any[]).slice();
alt[2] = new Date(e[2]);
e = ["OR", e, alt];
}
return e;
}
return exp;
});
}
export function processTasksFilter(filter): Expression {
return map(filter, exp => {
if (!isArray(exp)) return exp;
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
const e = exp.slice();
if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectID(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "timestamp")
e[2] = new Date(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
e[2] = new Date(e[2]);
return e;
}
return exp;
});
}
export function processFaultsFilter(filter): Expression {
return map(filter, exp => {
if (!isArray(exp)) return exp;
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
const e = exp.slice();
if (e[1][0] === "PARAM" && e[1][1] === "timestamp") e[2] = new Date(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
e[2] = new Date(e[2]);
return e;
}
return exp;
});
}
export function filterToMongoQuery(exp: Expression): {} {
const ops = {
OR: 0,
AND: 0,
NOT: 0,
"=": 1,
"<>": 1,
">": 1,
">=": 1,
"<": 1,
"<=": 1,
LIKE: 1,
"NOT LIKE": 1,
"IS NULL": 1,
"IS NOT NULL": 1
};
function recursive(filter, negate, res = {}): {} {
const op = filter[0];
if (ops[op] === 0) {
for (let i = 1; i < filter.length; ++i) {
if (!isArray(filter[i]) || ops[filter[i][0]] == null)
throw new Error(`Invalid expression in ${op} clause`);
}
if ((!negate && op === "AND") || (negate && op === "OR")) {
res["$and"] = res["$and"] || [];
for (let i = 1; i < filter.length; ++i)
res["$and"].push(recursive(filter[i], negate));
} else if ((!negate && op === "OR") || (negate && op === "AND")) {
res["$or"] = res["$or"] || [];
for (let i = 1; i < filter.length; ++i)
res["$or"].push(recursive(filter[i], negate));
} else if (op === "NOT") {
recursive(filter[1], !negate, res);
}
} else if (ops[op] === 1) {
if (isArray(filter[2])) {
throw new Error(`Right operand of ${op} clause is not a primitive`);
} else if (
["LIKE", "NOT LIKE"].includes(op) &&
isArray(filter[1]) &&
filter[1][0] === "FUNC" &&
["UPPER", "LOWER"].includes(filter[1][1])
) {
if (
(filter[1][1] === "UPPER" && filter[2] !== filter[2].toUpperCase()) ||
(filter[1][1] === "LOWER" && filter[2] !== filter[2].toLowerCase())
) {
throw new Error(
`Cannot compare ${
filter[1][1]
}() against non-${filter[1][1].toLowerCase()}case pattern`
);
}
} else if (!isArray(filter[1]) || filter[1][0] !== "PARAM") {
throw new Error(`Left operand of ${op} clause is not a parameter`);
}
if (op === "=") {
const param = filter[1][1];
const p = (res[param] = res[param] || {});
if (negate) p["$ne"] = filter[2];
else p["$eq"] = filter[2];
} else if (op === "<>") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$ne"] = filter[2];
if (param !== "_tags") p["$exists"] = true;
} else if (op === ">") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$gt"] = filter[2];
} else if (op === ">=") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$gte"] = filter[2];
} else if (op === "<") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$lt"] = filter[2];
} else if (op === "<=") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$lte"] = filter[2];
} else if (op === "IS NULL") {
const param = filter[1][1];
res[param] = { $exists: negate };
} else if (op === "IS NOT NULL") {
const param = filter[1][1];
res[param] = { $exists: !negate };
} else {
if (op === "NOT LIKE") negate = !negate;
let param;
let flags;
if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") {
param = filter[1][2][1];
flags = "i";
} else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") {
param = filter[1][2][1];
flags = "i";
} else {
param = filter[1][1];
flags = "";
}
const r = likePatternToRegExp(filter[2], filter[3], flags);
if (negate) res[param] = { $not: r };
else res[param] = r;
}
} else {
throw new Error(`Unrecognized operator ${op}`);
}
return res;
}
const _exp = evaluate(exp, null, Date.now());
if (!isArray(_exp)) {
if (_exp === true) return {};
throw new Error("Primitives are not valid queries");
}
return recursive(_exp, false);
}
export function processDeviceProjection(projection: {}): {} {
if (!projection) return projection;
const p = {};
for (const [k, v] of Object.entries(projection)) {
if (k === "DeviceID.ID") {
p["_id"] = 1;
} else if (k.startsWith("DeviceID")) {
p["_deviceId._SerialNumber"] = v;
p["_deviceId._OUI"] = v;
p["_deviceId._ProductClass"] = v;
p["_deviceId._Manufacturer"] = v;
} else if (k.startsWith("Tags")) {
p["_tags"] = v;
} else if (k.startsWith("Events")) {
p["_lastInform"] = v;
p["_registered"] = v;
p["_lastBoot"] = v;
p["_lastBootstrap"] = v;
} else {
p[k] = v;
}
}
return p;
}
export function processDeviceSort(sort: {}): {} {
if (!sort) return sort;
const s = {};
for (const [k, v] of Object.entries(sort)) {
if (k === "DeviceID.ID") s["_id"] = v;
else if (k.startsWith("DeviceID.")) s[`_deviceId._${k.slice(9)}`] = v;
else if (k === "Events.Inform") s["_lastInform"] = v;
else if (k === "Events.Registered") s["_registered"] = v;
else if (k === "Events.1_BOOT") s["_lastBoot"] = v;
else if (k === "Events.0_BOOTSTRAP") s["_lastBootstrap"] = v;
else s[`${k}._value`] = v;
}
return s;
}
function parseDate(d): number | string {
const n = +d;
return isNaN(n) ? "" + d : n;
}
interface FlatDevice {
[param: string]: {
object: boolean;
objectTimestamp: number;
writable: boolean;
writableTimestamp: number;
value: [string | number | boolean, string];
valueTimestamp: number;
};
}
export function flattenDevice(device): FlatDevice {
function recursive(input, root, output, timestamp): void {
for (const [name, tree] of Object.entries(input)) {
if (!root) {
if (name === "_lastInform") {
output["Events.Inform"] = {
value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_registered") {
output["Events.Registered"] = {
value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_lastBoot") {
output["Events.1_BOOT"] = {
value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_lastBootstrap") {
output["Events.0_BOOTSTRAP"] = {
value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_id") {
output["DeviceID.ID"] = {
value: [tree, "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_deviceId") {
output["DeviceID.Manufacturer"] = {
value: [tree["_Manufacturer"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
output["DeviceID.OUI"] = {
value: [tree["_OUI"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
output["DeviceID.ProductClass"] = {
value: [tree["_ProductClass"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
output["DeviceID.SerialNumber"] = {
value: [tree["_SerialNumber"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_tags") {
output["Tags"] = {
writable: true,
writableTimestamp: timestamp,
object: true,
objectTimestamp: timestamp
};
for (const t of tree as string[]) {
output[`Tags.${t}`] = {
value: [true, "xsd:boolean"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
}
}
}
if (name.startsWith("_")) continue;
let childrenTimestamp = timestamp;
if (!root) childrenTimestamp = +(input["_timestamp"] || 1);
else if (+input["_timestamp"] > timestamp)
childrenTimestamp = +input["_timestamp"];
const attrs = {};
if (tree["_value"] != null) {
attrs["value"] = [
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
tree["_type"]
];
attrs["valueTimestamp"] = +(tree["_timestamp"] || childrenTimestamp);
attrs["object"] = false;
attrs["objectTimestamp"] = childrenTimestamp;
} else if (tree["_object"] != null) {
attrs["object"] = tree["_object"];
attrs["objectTimestamp"] = childrenTimestamp;
}
if (tree["_writable"] != null) {
attrs["writable"] = tree["_writable"];
attrs["writableTimestamp"] = childrenTimestamp;
}
const r = root ? `${root}.${name}` : name;
output[r] = attrs;
if (attrs["object"] || tree["object"] == null)
recursive(tree, r, output, childrenTimestamp);
}
}
const newDevice = {};
const timestamp = new Date(device["_lastInform"] || 1).getTime();
recursive(device, "", newDevice, timestamp);
return newDevice;
}
export function flattenFault(fault): Fault {
const f = Object.assign({}, fault);
if (f.timestamp) f.timestamp = +f.timestamp;
if (f.expiry) f.expiry = +f.expiry;
return f;
}
export function flattenTask(task): Task {
const t = Object.assign({}, task);
t._id = "" + t._id;
if (t.timestamp) t.timestamp = +t.timestamp;
if (t.expiry) t.expiry = +t.expiry;
return t;
}
export function mongoQueryToFilter(query): Expression {
function recursive(_query): Expression {
const expressions: Expression[] = [];
for (const [k, v] of Object.entries(_query)) {
if (k[0] === "$") {
if (k === "$and") {
const and: Expression = ["AND"];
for (const vv of Object.values(v)) and.push(recursive(vv));
expressions.push(and);
} else if (k === "$or") {
const or: Expression = ["OR"];
for (const vv of Object.values(v)) or.push(recursive(vv));
expressions.push(or);
} else {
throw new Error(`Operator ${k} not supported`);
}
} else if (k === "_tags") {
if (typeof v === "object") {
if (isArray(v)) throw new Error(`Invalid type`);
if (v.hasOwnProperty("$ne"))
expressions.push(["IS NULL", ["PARAM", `Tags.${v["$ne"]}`]]);
else if (v.hasOwnProperty("$eq"))
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v["$eq"]}`]]);
else throw new Error(`Invalid tag query`);
} else {
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]);
}
} else if (k.startsWith("Tags.")) {
let exists: boolean;
if (typeof v === "boolean") exists = v;
else if (v.hasOwnProperty("$eq")) exists = !!v["$eq"];
else if (v.hasOwnProperty("$ne")) exists = !v["$ne"];
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
else throw new Error(`Invalid tag query`);
expressions.push([exists ? "IS NOT NULL" : "IS NULL", ["PARAM", k]]);
} else if (typeof v === "object") {
if (isArray(v)) throw new Error(`Invalid type`);
const exps: Expression[] = [];
for (const [kk, vv] of Object.entries(v)) {
let op;
switch (kk) {
case "$eq":
op = "=";
break;
case "$ne":
op = "<>";
break;
case "$lt":
op = "<";
break;
case "$lte":
op = "<=";
break;
case "$gt":
op = ">";
break;
case "$gte":
op = ">=";
break;
default:
throw new Error(`Operator ${kk} not supported`);
}
exps.push([op, ["PARAM", k], vv]);
}
if (exps.length === 1) return exps[0];
const and: Expression = ["AND"];
expressions.push(and.concat(exps));
} else {
expressions.push(["=", ["PARAM", k], v]);
}
}
if (expressions.length === 1) return expressions[0];
const and: Expression = ["AND"];
return and.concat(expressions);
}
// empty filter
if (!Object.keys(query).length) return true;
return recursive(query);
}
export function flattenPreset(preset): {} {
const p = Object.assign({}, preset);
if (p.precondition) {
try {
// Try parse to check expression validity
parse(p.precondition);
} catch (error) {
p.precondition = mongoQueryToFilter(JSON.parse(p.precondition));
p.precondition = p.precondition.length ? stringify(p.precondition) : "";
}
}
if (p.events) {
const e = [];
for (const [k, v] of Object.entries(p.events)) e.push(v ? k : `-${k}`);
p.events = e.join(", ");
}
const provision = p.configurations[0];
if (
p.configurations.length !== 1 ||
provision.type !== "provision" ||
!provision.name ||
!provision.name.length
)
throw new Error("Invalid preset provision");
p.provision = provision.name;
p.provisionArgs = provision.args
? JSON.stringify(provision.args).slice(1, -1)
: "";
delete p.configurations;
return p;
}
export function flattenFile(file): {} {
const f = {};
f["_id"] = file["_id"];
if (file.metadata) {
f["metadata.fileType"] = file.metadata.fileType || "";
f["metadata.oui"] = file.metadata.oui || "";
f["metadata.productClass"] = file.metadata.productClass || "";
f["metadata.version"] = file.metadata.version || "";
}
return f;
}
export function preProcessPreset(data): {} {
const preset = Object.assign({}, data);
if (!preset.precondition) preset.precondition = "";
// Try parse to check expression validity
parse(preset.precondition);
preset.weight = parseInt(preset.weight) || 0;
const events = {};
if (preset.events) {
for (let e of preset.events.split(",")) {
let v = true;
e = e.trim();
if (e.startsWith("-")) {
v = false;
e = e.slice(1).trim();
}
if (e) events[e] = v;
}
}
preset.events = events;
if (!preset.provision) throw new Error("Invalid preset provision");
const configuration = {
type: "provision",
name: preset.provision,
args: null
};
if (preset.provisionArgs)
configuration.args = JSON.parse(`[${preset.provisionArgs}]`);
delete preset.provision;
delete preset.provisionArgs;
preset.configurations = [configuration];
return preset;
}
+39 -35
View File
@@ -16,20 +16,18 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const url = require("url");
const mongodb = require("mongodb");
const querystring = require("querystring");
const vm = require("vm");
const childProcess = require("child_process");
const config = require("./config");
const db = require("./db");
const query = require("./query");
const apiFunctions = require("./api-functions");
const cache = require("./cache");
const VERSION = require("../package.json").version;
import * as url from "url";
import * as mongodb from "mongodb";
import * as querystring from "querystring";
import * as vm from "vm";
import { exec } from "child_process";
import * as config from "./config";
import * as db from "./db";
import * as query from "./query";
import * as apiFunctions from "./api-functions";
import * as cache from "./cache";
import { version as VERSION } from "../package.json";
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
@@ -44,7 +42,7 @@ const PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-_%]+)\/?$/;
const VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-_%]+)\/?$/;
const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/;
function throwError(err, httpResponse) {
function throwError(err, httpResponse?): never {
if (httpResponse) {
httpResponse.writeHead(500, { Connection: "close" });
httpResponse.end(`${err.name}: ${err.message}`);
@@ -52,17 +50,20 @@ function throwError(err, httpResponse) {
throw err;
}
function listener(request, response) {
export function listener(request, response): void {
const chunks = [];
let bytes = 0;
response.setHeader("GenieACS-Version", VERSION);
request.addListener("data", chunk => {
chunks.push(chunk);
bytes += chunk.length;
});
request.addListener(
"data",
(chunk): void => {
chunks.push(chunk);
bytes += chunk.length;
}
);
function getBody() {
function getBody(): Buffer {
// Write all chunks into a Buffer
const body = Buffer.allocUnsafe(bytes);
let offset = 0;
@@ -81,7 +82,7 @@ function listener(request, response) {
PRESETS_REGEX.exec(urlParts.pathname)[1]
);
if (request.method === "PUT") {
const preset = JSON.parse(body);
const preset = JSON.parse(body.toString());
preset._id = presetName;
db.presetsCollection.replaceOne(
{ _id: presetName },
@@ -118,7 +119,7 @@ function listener(request, response) {
OBJECTS_REGEX.exec(urlParts.pathname)[1]
);
if (request.method === "PUT") {
const object = JSON.parse(body);
const object = JSON.parse(body.toString());
object._id = objectName;
db.objectsCollection.replaceOne(
{ _id: objectName },
@@ -321,7 +322,7 @@ function listener(request, response) {
DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1]
);
if (body.length) {
const task = JSON.parse(body);
const task = JSON.parse(body.toString());
task.device = deviceId;
apiFunctions.insertTasks(task, err => {
if (err) return void throwError(err, response);
@@ -341,7 +342,7 @@ function listener(request, response) {
const taskTimeout =
(urlParts.query.timeout &&
parseInt(urlParts.query.timeout)) ||
parseInt(urlParts.query.timeout as string)) ||
config.get("DEVICE_ONLINE_THRESHOLD", deviceId);
apiFunctions.watchTask(
@@ -500,15 +501,18 @@ function listener(request, response) {
const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
metadata: metadata
});
uploadStream.end(body, err => {
if (err) return void throwError(err, response);
uploadStream.on("error", err => {
throwError(err, response);
});
uploadStream.end(body, () => {
response.writeHead(201);
response.end();
});
} else if (request.method === "DELETE") {
const bucket = new mongodb.GridFSBucket(db.client.db());
bucket.delete(filename, err => {
bucket.delete((filename as unknown) as mongodb.ObjectId, err => {
if (err) return void throwError(err, response);
response.writeHead(200);
@@ -520,7 +524,7 @@ function listener(request, response) {
}
} else if (PING_REGEX.test(urlParts.pathname)) {
const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]);
childProcess.exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => {
exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => {
if (err) {
response.writeHead(404, { "Cache-Control": "no-cache" });
response.end(`${err.name}: ${err.message}`);
@@ -578,7 +582,7 @@ function listener(request, response) {
let q = {};
if (urlParts.query.query) {
try {
q = JSON.parse(urlParts.query.query);
q = JSON.parse(urlParts.query.query as string);
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
@@ -607,14 +611,14 @@ function listener(request, response) {
let projection = null;
if (urlParts.query.projection) {
projection = {};
for (const p of urlParts.query.projection.split(","))
for (const p of (urlParts.query.projection as string).split(","))
projection[p.trim()] = 1;
}
const cur = collection.find(q, { projection: projection });
if (urlParts.query.sort) {
const s = JSON.parse(urlParts.query.sort);
const s = JSON.parse(urlParts.query.sort as string);
const sort = {};
for (const [k, v] of Object.entries(s)) {
if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices")
@@ -625,9 +629,11 @@ function listener(request, response) {
cur.sort(sort);
}
if (urlParts.query.skip) cur.skip(parseInt(urlParts.query.skip));
if (urlParts.query.skip)
cur.skip(parseInt(urlParts.query.skip as string));
if (urlParts.query.limit) cur.limit(parseInt(urlParts.query.limit));
if (urlParts.query.limit)
cur.limit(parseInt(urlParts.query.limit as string));
cur.count(false, (err, total) => {
if (err) return void throwError(err);
@@ -661,5 +667,3 @@ function listener(request, response) {
}
});
}
exports.listener = listener;
+13 -149
View File
@@ -16,11 +16,10 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const common = require("./common");
import * as common from "./common";
function stringToRegexp(input, flags) {
function stringToRegexp(input, flags?): RegExp | false {
if (input.indexOf("*") === -1) return false;
let output = input.replace(/[[\]\\^$.|?+()]/, "\\$&");
@@ -34,7 +33,7 @@ function stringToRegexp(input, flags) {
return new RegExp(output, flags);
}
function normalize(input) {
function normalize(input): any {
if (common.typeOf(input) === common.STRING_TYPE) {
const vals = [input];
const m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input);
@@ -53,7 +52,7 @@ function normalize(input) {
return input;
}
function expandValue(value) {
export function expandValue(value): any[] {
if (common.typeOf(value) === common.ARRAY_TYPE) {
let a = [];
for (const j of value) a = a.concat(expandValue(j));
@@ -89,7 +88,7 @@ function expandValue(value) {
return objs;
}
function permute(param, val) {
function permute(param, val): any[] {
const conditions = [];
const values = expandValue(val);
@@ -104,12 +103,12 @@ function permute(param, val) {
return conditions;
}
function expand(query) {
export function expand(query): {} {
const newQuery = {};
for (const [k, v] of Object.entries(query)) {
if (k[0] === "$") {
// Operator
newQuery[k] = v.map(e => expand(e));
newQuery[k] = (v as any[]).map(e => expand(e));
} else {
const conditions = permute(k, v);
if (conditions.length > 1) {
@@ -130,7 +129,7 @@ function expand(query) {
return newQuery;
}
function testExpressions(params, expressions, lop) {
function testExpressions(params, expressions, lop): boolean {
for (const f of expressions) {
const res = test(params, f);
switch (lop) {
@@ -157,7 +156,7 @@ function testExpressions(params, expressions, lop) {
}
}
function test(params, query) {
export function test(params, query): boolean {
let res;
for (const [k, v] of Object.entries(query)) {
if (k.charAt(0) === "$") {
@@ -170,7 +169,8 @@ function test(params, query) {
// TODO comparing array to regex, array to array, and object to object
res = value.indexOf(v) !== -1;
} else {
if (common.typeOf(v) === common.REGEXP_TYPE) res = v.test(value);
if (common.typeOf(v) === common.REGEXP_TYPE)
res = (v as RegExp).test(value);
else res = v === value;
}
} else {
@@ -216,141 +216,11 @@ function test(params, query) {
return true;
}
function matchType(src, dst) {
const type = typeof src;
if (type === "string") {
return String(dst);
} else if (type === "number") {
if (+dst === parseFloat(dst)) return +dst;
else if (!isNaN(Date.parse(dst))) return Date.parse(dst);
} else if (type === "boolean") {
const v = String(dst)
.trim()
.toLowerCase();
if (v === "true" || v === "1") return true;
else if (v === "false" || v === "0") return false;
}
return dst;
}
function testFilter(obj, filter) {
for (const [k, v] of Object.entries(filter)) {
const [param, op] = k.split(/([^a-zA-Z0-9\-_.].*)/, 2);
const val = matchType(obj[param], v);
switch (op) {
case "=":
case undefined:
if (obj[param] !== val) return false;
break;
case ">":
if (!(obj[param] > val)) return false;
break;
case ">=":
if (!(obj[param] >= val)) return false;
break;
case "<":
if (!(obj[param] < val)) return false;
break;
case "<=":
if (!(obj[param] <= val)) return false;
break;
case "!":
if (obj[param] === val) return false;
break;
default:
throw new Error(`Unrecognized operator ${op}`);
}
}
return true;
}
function convertMongoQueryToFilters(query, filters) {
filters = filters || {};
for (let [k, v] of Object.entries(query)) {
if (k[0] === "$") {
if (k === "$and")
for (const vv of v) convertMongoQueryToFilters(vv, filters);
else throw new Error(`Operator ${k} not supported`);
} else if (k === "_tags") {
if (common.typeOf(v) === common.OBJECT_TYPE) {
for (let [kk, vv] of Object.entries(v)) {
vv = vv.replace(/[^a-zA-Z0-9-]+/g, "_");
if (kk === "$ne") filters[`Tags.${vv}!`] = true;
else throw new Error(`Operator ${kk} not supported`);
}
} else {
v = v.replace(/[^a-zA-Z0-9-]+/g, "_");
filters[`Tags.${v}`] = true;
}
} else {
switch (k) {
case "_id":
k = "DeviceID.ID";
break;
case "_deviceId._Manufacturer":
k = "DeviceID.Manufacturer";
break;
case "_deviceId._OUI":
k = "DeviceID.OUI";
break;
case "_deviceId._ProductClass":
k = "DeviceID.ProductClass";
break;
case "_deviceId._SerialNumber":
k = "DeviceID.SerialNumber";
break;
case "_lastInform":
k = "Events.Inform";
break;
case "_lastBootstrap":
k = "Events.0_BOOTSTRAP";
break;
case "_lastBoot":
k = "Events.1_BOOT";
break;
case "_registered":
k = "Events.Registered";
}
if (common.typeOf(v) === common.OBJECT_TYPE) {
for (const [kk, vv] of Object.entries(v)) {
switch (kk) {
case "$eq":
filters[k] = vv;
break;
case "$ne":
filters[`${k}!`] = vv;
break;
case "$lt":
filters[`${k}<`] = vv;
break;
case "$lte":
filters[`${k}<=`] = vv;
break;
case "$gt":
filters[`${k}>`] = vv;
break;
case "$gte":
filters[`${k}>=`] = vv;
break;
default:
throw new Error(`Oprator ${kk} not supported`);
}
}
} else {
filters[k] = v;
}
}
}
return filters;
}
function sanitizeQueryTypes(query, types) {
export function sanitizeQueryTypes(query, types): {} {
for (const [k, v] of Object.entries(query)) {
if (k[0] === "$") {
// Logical operator
for (const vv of v) sanitizeQueryTypes(vv, types);
for (const vv of v as any[]) sanitizeQueryTypes(vv, types);
} else if (k in types) {
if (common.typeOf(v) === common.OBJECT_TYPE) {
for (const [kk, vv] of Object.entries(v)) {
@@ -383,9 +253,3 @@ function sanitizeQueryTypes(query, types) {
return query;
}
exports.expand = expand;
exports.test = test;
exports.sanitizeQueryTypes = sanitizeQueryTypes;
exports.convertMongoQueryToFilters = convertMongoQueryToFilters;
exports.testFilter = testFilter;
+58 -44
View File
@@ -16,17 +16,15 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const vm = require("vm");
const seedrandom = require("seedrandom");
const common = require("./common");
const device = require("./device");
const extensions = require("./extensions");
const logger = require("./logger");
const scheduling = require("./scheduling");
import * as vm from "vm";
import seedrandom from "seedrandom";
import * as device from "./device";
import * as extensions from "./extensions";
import * as logger from "./logger";
import * as scheduling from "./scheduling";
import Path from "./common/path";
import { Fault, SessionContext } from "./types";
// Used for throwing to exit user script and commit
const COMMIT = Symbol();
@@ -41,7 +39,7 @@ const context = vm.createContext();
let state;
const runningExtensions = new WeakMap();
function runExtension(sessionContext, key, extCall, callback) {
function runExtension(sessionContext, key, extCall, callback): void {
let re = runningExtensions.get(sessionContext);
if (!re) {
re = {};
@@ -62,13 +60,23 @@ function runExtension(sessionContext, key, extCall, callback) {
}
class SandboxDate {
constructor() {
if (arguments.length) return new Date(...arguments);
public constructor(
...argumentList: [
number?,
number?,
number?,
number?,
number?,
number?,
number?
]
) {
if (argumentList.length) return new Date(...argumentList);
return new Date(state.sessionContext.timestamp);
}
static now(intervalOrCron, variance) {
public static now(intervalOrCron, variance): number {
let t = state.sessionContext.timestamp;
if (typeof intervalOrCron === "number") {
@@ -93,7 +101,7 @@ class SandboxDate {
}
}
function random() {
function random(): number {
if (!state.rng) state.rng = seedrandom(state.sessionContext.deviceId);
return state.rng();
@@ -104,7 +112,7 @@ random.seed = function(s) {
};
class ParameterWrapper {
constructor(path, attributes, unpacked, unpackedRevision) {
public constructor(path: Path, attributes, unpacked?, unpackedRevision?) {
for (const attrName of attributes) {
Object.defineProperty(this, attrName, {
get: function() {
@@ -148,7 +156,7 @@ class ParameterWrapper {
if (!unpacked.length) return UNDEFINED;
return unpacked[0].join(".");
return unpacked[0].toString();
}
});
@@ -189,13 +197,17 @@ class ParameterWrapper {
}
}
function declare(path, timestamps, values) {
function declare(
path: string,
timestamps: { [attr: string]: number },
values: { [attr: string]: any }
): ParameterWrapper {
state.uncommitted = true;
if (!timestamps) timestamps = {};
if (!values) values = {};
const parsedPath = common.parsePath(path);
const parsedPath = Path.parse(path);
const declaration = {
path: parsedPath,
@@ -237,14 +249,14 @@ function declare(path, timestamps, values) {
return new ParameterWrapper(parsedPath, attrs);
}
function clear(path, timestamp, attributes) {
function clear(path: string, timestamp: number, attributes?): void {
state.uncommitted = true;
if (state.revision === state.maxRevision)
state.clear.push([common.parsePath(path), timestamp, attributes]);
state.clear.push([Path.parse(path), timestamp, attributes]);
}
function commit() {
function commit(): void {
++state.revision;
state.uncommitted = false;
@@ -258,7 +270,7 @@ function commit() {
}
}
function ext() {
function ext(): any {
++state.extCounter;
const extCall = Array.from(arguments).map(String);
const key = `${state.revision}: ${JSON.stringify(extCall)}`;
@@ -270,19 +282,19 @@ function ext() {
throw EXT;
}
function log(msg, meta) {
function log(msg: string, meta: {}): void {
if (state.revision === state.maxRevision && state.extCounter >= 0) {
const details = Object.assign({}, meta, {
sessionContext: state.sessionContext,
message: `Script: ${msg}`
});
delete details.hostname;
delete details.pid;
delete details.name;
delete details.version;
delete details.deviceId;
delete details.remoteAddress;
delete details["hostname"];
delete details["pid"];
delete details["name"];
delete details["version"];
delete details["deviceId"];
delete details["remoteAddress"];
logger.accessInfo(details);
}
@@ -300,12 +312,12 @@ context.random = random;
vm.runInContext("Math.random = random;", context);
delete context.random;
function errorToFault(err) {
function errorToFault(err: Error): Fault {
if (!err) return null;
if (!err.name) return { code: "script", message: `${err}` };
const fault = {
const fault: Fault = {
code: `script.${err.name}`,
message: err.message,
detail: {
@@ -315,27 +327,31 @@ function errorToFault(err) {
};
if (err.stack) {
fault.detail.stack = err.stack;
fault.detail["stack"] = err.stack;
// Trim the stack trace at the self-executing anonymous wrapper function
const stackTrimIndex = fault.detail.stack.match(
const stackTrimIndex = fault.detail["stack"].match(
/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/
);
if (stackTrimIndex)
fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index);
if (stackTrimIndex) {
fault.detail["stack"] = fault.detail["stack"].slice(
0,
stackTrimIndex.index
);
}
}
return fault;
}
function run(
script,
export function run(
script: vm.Script,
globals,
sessionContext,
startRevision,
maxRevision,
sessionContext: SessionContext,
startRevision: number,
maxRevision: number,
callback,
extCounter = 0
) {
): void {
state = {
sessionContext: sessionContext,
revision: startRevision,
@@ -416,5 +432,3 @@ function run(
}
}
}
exports.run = run;
+7 -14
View File
@@ -16,13 +16,11 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const crypto = require("crypto");
import * as crypto from "crypto";
import * as later from "later";
const later = require("later");
function md532(str) {
function md532(str): number {
const digest = crypto
.createHash("md5")
.update(str)
@@ -35,22 +33,22 @@ function md532(str) {
);
}
function variance(deviceId, vrnc) {
export function variance(deviceId, vrnc): number {
return (md532(deviceId) >>> 0) % vrnc;
}
function interval(timestamp, intrvl, offset = 0) {
export function interval(timestamp, intrvl, offset = 0): number {
return Math.trunc((timestamp + offset) / intrvl) * intrvl - offset;
}
function parseCron(cronExp) {
export function parseCron(cronExp): any {
const 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) {
export function cron(timestamp, schedule, offset = 0): number[] {
// TODO later.js doesn't throw erorr if expression is invalid!
const ret = [0, 0];
@@ -62,8 +60,3 @@ function cron(timestamp, schedule, offset = 0) {
return ret;
}
exports.variance = variance;
exports.interval = interval;
exports.parseCron = parseCron;
exports.cron = cron;
-132
View File
@@ -1,132 +0,0 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
"use strict";
const service = process.argv[2];
if (service == null) throw new Error("Missing argument cwmp, fs, or nbi");
const cluster = require("cluster");
const path = require("path");
const fs = require("fs");
const http = require("http");
const https = require("https");
const logger = require("./logger");
logger.init(service, require("../package.json").version);
const config = require("./config");
const db = require("./db");
const cache = require("./cache");
const extensions = require("./extensions");
const networkInterface = config.get(`${service.toUpperCase()}_INTERFACE`);
const port = config.get(`${service.toUpperCase()}_PORT`);
const useHttps = config.get(`${service.toUpperCase()}_SSL`);
const { listener, onConnection } = require(`./${service}`);
let server;
function closeServer(timeout, callback) {
if (!server) return void callback();
setTimeout(() => {
if (!callback) return;
// Ignore HTTP requests from connection that may still be open
server.removeListener("request", listener);
server.setTimeout(1);
const cb = callback;
callback = null;
setTimeout(cb, 1000);
}, timeout).unref();
server.close(() => {
if (!callback) return;
const cb = callback;
callback = null;
cb();
});
}
function exit() {
setTimeout(() => {
extensions.killAll(() => {
process.exit(1);
});
}, 30000).unref();
closeServer(20000, () => {
db.disconnect();
cache.disconnect();
extensions.killAll();
if (cluster.worker) cluster.worker.disconnect();
});
}
process.on("uncaughtException", err => {
logger.error({
message: "Uncaught exception",
exception: err,
pid: process.pid
});
exit();
});
if (useHttps) {
const httpsKey = path.resolve(config.get("CONFIG_DIR"), `${service}.key`);
const httpsCert = path.resolve(config.get("CONFIG_DIR"), `${service}.crt`);
const httpsCa = path.resolve(
config.get("CONFIG_DIR"),
`${service}.ca-bundle`
);
const options = {
key: fs.readFileSync(httpsKey),
cert: fs.readFileSync(httpsCert)
};
try {
// Use intermediate certificates if available
options.ca = fs
.readFileSync(httpsCa)
.toString()
.match(/-+BEGIN CERTIFICATE-+[0-9a-zA-Z+\-/=\s]+?-+END CERTIFICATE-+/g);
} catch (error) {
// No intermediate certificate
}
server = https.createServer(options, listener);
if (onConnection != null) server.on("secureConnection", onConnection);
} else {
server = http.createServer(listener);
if (onConnection != null) server.on("connection", onConnection);
}
db.connect(err => {
if (err) throw err;
cache.connect(err => {
if (err) throw err;
server.listen(port, networkInterface);
});
});
process.on("SIGINT", exit);
process.on("SIGTERM", exit);
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright 2013-2018 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 <http://www.gnu.org/licenses/>.
*/
import * as fs from "fs";
import * as http from "http";
import * as https from "https";
let server: http.Server | https.Server;
let listener: (...args) => void;
function closeServer(timeout, callback): void {
if (!server) return void callback();
setTimeout(() => {
if (!callback) return;
// Ignore HTTP requests from connection that may still be open
server.removeListener("request", listener);
server.setTimeout(1);
const cb = callback;
callback = null;
setTimeout(cb, 1000);
}, timeout).unref();
server.close(() => {
if (!callback) return;
const cb = callback;
callback = null;
cb();
});
}
export function start(
port,
networkInterface,
ssl,
_listener,
onConnection?
): void {
listener = _listener;
if (ssl) {
const options = {
key: fs.readFileSync(ssl.key),
cert: fs.readFileSync(ssl.cert),
ca: null
};
try {
// Use intermediate certificates if available
options.ca = fs
.readFileSync(ssl.ca)
.toString()
.match(/-+BEGIN CERTIFICATE-+[0-9a-zA-Z+\-/=\s]+?-+END CERTIFICATE-+/g);
} catch (error) {
// No intermediate certificate
}
server = https.createServer(options, listener);
if (onConnection != null) server.on("secureConnection", onConnection);
} else {
server = http.createServer(listener);
if (onConnection != null) server.on("connection", onConnection);
}
server.listen(port, networkInterface);
}
export function stop(): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Could not close server in a timely manner"));
}, 30000).unref();
closeServer(20000, resolve);
});
}
File diff suppressed because it is too large. Load diff
+96 -60
View File
@@ -16,13 +16,23 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const libxmljs = require("libxmljs");
import { parseXml, Document, Element, ParserOptions } from "libxmljs";
import * as config from "./config";
import { version as VERSION } from "../package.json";
import {
CpeResponse,
CpeGetResponse,
CpeSetResponse,
InformRequest,
AcsResponse,
CpeRequest,
FaultStruct,
SpvFault,
CpeFault
} from "./types";
const config = require("./config");
const SERVER_NAME = `GenieACS/${require("../package.json").version}`;
const SERVER_NAME = `GenieACS/${VERSION}`;
const NAMESPACES = {
"1.0": {
@@ -62,21 +72,36 @@ const NAMESPACES = {
}
};
const LIBXMLJS_PARSE_OPTIONS = {
const LIBXMLJS_PARSE_OPTIONS: ParserOptions = {
nocdata: true,
recover: config.get("XML_RECOVER"),
ignore_enc: config.get("XML_IGNORE_ENC")
recover: !!config.get("XML_RECOVER"),
/* eslint-disable-next-line @typescript-eslint/camelcase */
ignore_enc: !!config.get("XML_IGNORE_ENC")
};
const LIBXMLJS_SAVE_OPTIONS = {
format: config.get("XML_FORMAT"),
interface WriterOptions {
declaration?: boolean;
format?: boolean;
selfCloseEmpty?: boolean;
whitespace?: boolean;
type?: "XML" | "HTML" | "XHTML";
}
declare module "libxmljs" {
interface Document {
toString(formatting: boolean | WriterOptions): string;
}
}
const LIBXMLJS_SAVE_OPTIONS: WriterOptions = {
format: !!config.get("XML_FORMAT"),
declaration: !config.get("XML_NO_DECL"),
selfCloseEmpty: !config.get("XML_NO_EMPTY")
};
let warnings;
function parseBool(v) {
function parseBool(v): boolean {
v = "" + v;
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
@@ -84,7 +109,7 @@ function parseBool(v) {
else return null;
}
function event(xml) {
function event(xml): string[] {
return xml
.childNodes()
.filter(n => n.name() === "EventStruct")
@@ -97,7 +122,7 @@ function event(xml) {
);
}
function parameterInfoList(xml) {
function parameterInfoList(xml): [string, boolean][] {
return xml
.childNodes()
.filter(e => e.name() === "ParameterInfoStruct")
@@ -128,7 +153,9 @@ function parameterInfoList(xml) {
});
}
function parameterValueList(xml) {
function parameterValueList(
xml
): [string, string | number | boolean, string][] {
return xml
.childNodes()
.filter(e => e.name() === "ParameterValueStruct")
@@ -181,13 +208,13 @@ function parameterValueList(xml) {
});
}
function GetParameterNames(xml, methodRequest) {
function GetParameterNames(xml, methodRequest): void {
const el = xml.node("cwmp:GetParameterNames");
el.node("ParameterPath").text(methodRequest.parameterPath);
el.node("NextLevel").text(+methodRequest.nextLevel);
}
function GetParameterNamesResponse(xml) {
function GetParameterNamesResponse(xml): CpeGetResponse {
return {
name: "GetParameterNamesResponse",
parameterList: parameterInfoList(
@@ -196,7 +223,7 @@ function GetParameterNamesResponse(xml) {
};
}
function GetParameterValues(xml, methodRequest) {
function GetParameterValues(xml, methodRequest): void {
const el = xml.node("cwmp:GetParameterValues").node("ParameterNames");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]`
@@ -205,7 +232,7 @@ function GetParameterValues(xml, methodRequest) {
for (const p of methodRequest.parameterNames) el.node("string").text(p);
}
function GetParameterValuesResponse(xml) {
function GetParameterValuesResponse(xml): CpeGetResponse {
return {
name: "GetParameterValuesResponse",
parameterList: parameterValueList(
@@ -214,7 +241,7 @@ function GetParameterValuesResponse(xml) {
};
}
function SetParameterValues(xml, methodRequest) {
function SetParameterValues(xml, methodRequest): void {
const el = xml.node("cwmp:SetParameterValues");
const paramList = el.node("ParameterList");
paramList.attr({
@@ -244,7 +271,7 @@ function SetParameterValues(xml, methodRequest) {
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function SetParameterValuesResponse(xml) {
function SetParameterValuesResponse(xml): CpeSetResponse {
return {
name: "SetParameterValuesResponse",
status: parseInt(
@@ -256,13 +283,13 @@ function SetParameterValuesResponse(xml) {
};
}
function AddObject(xml, methodRequest) {
function AddObject(xml, methodRequest): void {
const el = xml.node("cwmp:AddObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function AddObjectResponse(xml) {
function AddObjectResponse(xml): CpeSetResponse {
let instanceNumber, status;
for (const c of xml.childNodes()) {
switch (c.name()) {
@@ -282,13 +309,13 @@ function AddObjectResponse(xml) {
};
}
function DeleteObject(xml, methodRequest) {
function DeleteObject(xml, methodRequest): void {
const el = xml.node("cwmp:DeleteObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function DeleteObjectResponse(xml) {
function DeleteObjectResponse(xml): CpeSetResponse {
return {
name: "DeleteObjectResponse",
status: parseInt(
@@ -300,28 +327,28 @@ function DeleteObjectResponse(xml) {
};
}
function Reboot(xml, methodRequest) {
function Reboot(xml, methodRequest): void {
const el = xml.node("cwmp:Reboot");
el.node("CommandKey").text(methodRequest.commandKey || "");
}
function RebootResponse() {
function RebootResponse(): CpeSetResponse {
return {
name: "RebootResponse"
};
}
function FactoryReset(xml) {
function FactoryReset(xml): void {
xml.node("cwmp:FactoryReset");
}
function FactoryResetResponse() {
function FactoryResetResponse(): CpeSetResponse {
return {
name: "FactoryResetResponse"
};
}
function Download(xml, methodRequest) {
function Download(xml, methodRequest): void {
const el = xml.node("cwmp:Download");
el.node("CommandKey").text(methodRequest.commandKey || "");
el.node("FileType").text(methodRequest.fileType);
@@ -335,7 +362,7 @@ function Download(xml, methodRequest) {
el.node("FailureURL").text(methodRequest.failureUrl || "");
}
function DownloadResponse(xml) {
function DownloadResponse(xml): CpeSetResponse {
let status, startTime, completeTime;
for (const c of xml.childNodes()) {
switch (c.name()) {
@@ -359,7 +386,7 @@ function DownloadResponse(xml) {
};
}
function Inform(xml) {
function Inform(xml): InformRequest {
let retryCount, evnt, parameterList;
const deviceId = {
Manufacturer: null,
@@ -397,18 +424,18 @@ function Inform(xml) {
};
}
function InformResponse(xml) {
function InformResponse(xml): void {
xml
.node("cwmp:InformResponse")
.node("MaxEnvelopes")
.text(1);
}
function GetRPCMethods() {
function GetRPCMethods(): AcsResponse {
return { name: "GetRPCMethods" };
}
function GetRPCMethodsResponse(xml, methodResponse) {
function GetRPCMethodsResponse(xml, methodResponse): void {
const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]`
@@ -417,7 +444,7 @@ function GetRPCMethodsResponse(xml, methodResponse) {
for (const m of methodResponse.methodList) el.node("string").text(m);
}
function TransferComplete(xml) {
function TransferComplete(xml): AcsResponse {
let commandKey, _faultStruct, startTime, completeTime;
for (const c of xml.childNodes()) {
switch (c.name()) {
@@ -445,11 +472,11 @@ function TransferComplete(xml) {
};
}
function TransferCompleteResponse(xml) {
function TransferCompleteResponse(xml): void {
xml.node("cwmp:TransferCompleteResponse").text("");
}
function RequestDownload(xml) {
function RequestDownload(xml): CpeRequest {
return {
name: "RequestDownload",
fileType: xml
@@ -459,12 +486,12 @@ function RequestDownload(xml) {
};
}
function RequestDownloadResponse(xml) {
function RequestDownloadResponse(xml): void {
xml.node("cwmp:RequestDownloadResponse").text("");
}
function faultStruct(xml) {
let faultCode, faultString, setParameterValuesFault, pn, fc, fs;
function faultStruct(xml): FaultStruct {
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "FaultCode":
@@ -499,7 +526,7 @@ function faultStruct(xml) {
return { faultCode, faultString, setParameterValuesFault };
}
function fault(xml) {
function fault(xml): CpeFault {
let faultCode, faultString, detail;
for (const c of xml.childNodes()) {
switch (c.name()) {
@@ -518,7 +545,18 @@ function fault(xml) {
return { faultCode, faultString, detail };
}
function request(data, cwmpVersion, warn) {
export function request(
data,
cwmpVersion,
warn
): {
id: string;
cwmpVersion: string;
sessionTimeout: number;
cpeRequest?: CpeRequest;
cpeFault?: CpeFault;
cpeResponse?: CpeResponse;
} {
warnings = warn;
const rpc = {
@@ -526,12 +564,13 @@ function request(data, cwmpVersion, warn) {
cwmpVersion: cwmpVersion,
sessionTimeout: null,
cpeRequest: null,
cpeFault: null
cpeFault: null,
cpeResponse: null
};
if (!data.length) return rpc;
const xml = libxmljs.parseXml(data, LIBXMLJS_PARSE_OPTIONS);
const xml = parseXml(data, LIBXMLJS_PARSE_OPTIONS);
let headerElement, bodyElement;
@@ -589,7 +628,7 @@ function request(data, cwmpVersion, warn) {
rpc.cpeRequest = Inform(methodElement);
break;
case "GetRPCMethods":
rpc.cpeRequest = GetRPCMethods(methodElement);
rpc.cpeRequest = GetRPCMethods();
break;
case "TransferComplete":
rpc.cpeRequest = TransferComplete(methodElement);
@@ -613,16 +652,16 @@ function request(data, cwmpVersion, warn) {
rpc.cpeResponse = DeleteObjectResponse(methodElement);
break;
case "RebootResponse":
rpc.cpeResponse = RebootResponse(methodElement);
rpc.cpeResponse = RebootResponse();
break;
case "FactoryResetResponse":
rpc.cpeResponse = FactoryResetResponse(methodElement);
rpc.cpeResponse = FactoryResetResponse();
break;
case "DownloadResponse":
rpc.cpeResponse = DownloadResponse(methodElement);
break;
case "Fault":
rpc.cpeFault = fault(methodElement, rpc.cwmpVersion);
rpc.cpeFault = fault(methodElement);
break;
default:
throw new Error(`8000 Method not supported ${methodName}`);
@@ -632,8 +671,8 @@ function request(data, cwmpVersion, warn) {
return rpc;
}
function createSoapEnv(cwmpVersion) {
const xml = libxmljs.Document();
function createSoapEnv(cwmpVersion: string): Element {
const xml = new Document();
const env = xml.node("soap-env:Envelope");
for (const prefix of Object.keys(NAMESPACES[cwmpVersion]))
@@ -642,20 +681,20 @@ function createSoapEnv(cwmpVersion) {
return env;
}
function response(rpc) {
export function response(rpc): { code: number; headers: {}; data: string } {
const headers = {
Server: SERVER_NAME,
SOAPServer: SERVER_NAME
};
if (!rpc) return { code: 204, headers: headers, data: Buffer.allocUnsafe(0) };
if (!rpc) return { code: 204, headers: headers, data: "" };
const env = createSoapEnv(rpc.cwmpVersion);
const header = env.node("soap-env:Header");
header
.node("cwmp:ID")
.attr({
"soap-env:mustUnderstand": 1
"soap-env:mustUnderstand": "1"
})
.text(rpc.id);
@@ -664,16 +703,16 @@ function response(rpc) {
if (rpc.acsResponse) {
switch (rpc.acsResponse.name) {
case "InformResponse":
InformResponse(body, rpc.acsResponse);
InformResponse(body);
break;
case "GetRPCMethodsResponse":
GetRPCMethodsResponse(body, rpc.acsResponse);
break;
case "TransferCompleteResponse":
TransferCompleteResponse(body, rpc.acsResponse);
TransferCompleteResponse(body);
break;
case "RequestDownloadResponse":
RequestDownloadResponse(body, rpc.acsResponse);
RequestDownloadResponse(body);
break;
default:
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
@@ -699,7 +738,7 @@ function response(rpc) {
Reboot(body, rpc.acsRequest);
break;
case "FactoryReset":
FactoryReset(body, rpc.acsRequest);
FactoryReset(body);
break;
case "Download":
Download(body, rpc.acsRequest);
@@ -716,6 +755,3 @@ function response(rpc) {
data: env.doc().toString(LIBXMLJS_SAVE_OPTIONS)
};
}
exports.request = request;
exports.response = response;
+311
View File
@@ -0,0 +1,311 @@
import Path from "./common/path";
import PathSet from "./common/path-set";
import VersionedMap from "./versioned-map";
import InstanceSet from "./instance-set";
import { IncomingMessage, ServerResponse } from "http";
export type Expression = string | number | boolean | null | any[];
export interface Fault {
code: string;
message: string;
detail?:
| FaultStruct
| {
name: string;
message: string;
stack?: string;
};
}
export interface SessionFault extends Fault {
timestamp: number;
provisions: string[][];
retryNow?: boolean;
precondition?: boolean;
retries?: number;
expiry?: number;
}
export interface Attributes {
object?: [number, 1 | 0];
writable?: [number, 1 | 0];
value?: [number, [string | number | boolean, string]];
}
export interface AttributeTimestamps {
object?: number;
writable?: number;
value?: number;
}
export interface AttributeValues {
object?: boolean;
writable?: boolean;
value?: [string | number | boolean, string?];
}
export interface DeviceData {
paths: PathSet;
timestamps: VersionedMap<Path, number>;
attributes: VersionedMap<Path, Attributes>;
loaded: Map<Path, number>;
trackers: Map<Path, { [name: string]: number }>;
changes: Set<string>;
}
export type VirtualParameterDeclaration = [
Path,
{ path?: number; object?: number; writable?: number; value?: number }?,
{
path?: [number, number];
object?: boolean;
writable?: boolean;
value?: [string | number | boolean, string?];
}?
];
export interface SyncState {
refreshAttributes: {
exist: Set<Path>;
object: Set<Path>;
writable: Set<Path>;
value: Set<Path>;
};
spv: Map<Path, [string | number | boolean, string]>;
gpn: Set<Path>;
gpnPatterns: Map<Path, number>;
tags: Map<Path, boolean>;
virtualParameterDeclarations: VirtualParameterDeclaration[][];
instancesToDelete: Map<Path, Set<Path>>;
instancesToCreate: Map<Path, InstanceSet>;
downloadsToDelete: Set<Path>;
downloadsToCreate: InstanceSet;
downloadsValues: Map<Path, string | number>;
downloadsDownload: Map<Path, number>;
reboot: number;
factoryReset: number;
}
export interface SessionContext {
sessionId?: string;
timestamp: number;
deviceId: string;
deviceData: DeviceData;
cwmpVersion: string;
timeout: number;
provisions: any[];
channels: { [channel: string]: number };
virtualParameters: [
string,
AttributeTimestamps,
AttributeValues,
AttributeTimestamps,
AttributeValues
][][];
revisions: number[];
rpcCount: number;
iteration: number;
cycle: number;
extensionsCache: any;
declarations: Declaration[][];
faults?: { [channel: string]: SessionFault };
retries?: { [channel: string]: number };
cacheSnapshot?: string;
configContext?: { [name: string]: any };
httpResponse?: ServerResponse;
httpRequest?: IncomingMessage;
faultsTouched?: { [channel: string]: boolean };
presetCycles?: number;
toLoad?: Map<Path, number>;
new?: boolean;
debug?: boolean;
tasks?: Task[];
operations?: { [commandKey: string]: Operation };
cacheUntil?: number;
syncState?: SyncState;
lastActivity?: number;
rpcRequest?: AcsRequest;
operationsTouched?: { [commandKey: string]: 1 | 0 };
provisionsRet?: any[];
doneTasks?: string[];
}
export interface Task {
_id: string;
name: string;
parameterNames?: string[];
parameterValues?: [string, string | number | boolean, string?][];
objectName?: string;
fileType?: string;
fileName?: string;
targetFileName?: string;
expiry?: number;
}
export interface Operation {
name: string;
timestamp: number;
provisions: string[][];
channels: { [channel: string]: number };
retries: { [channel: string]: number };
args: {
instance: string;
fileType: string;
fileName: string;
targetFileName: string;
};
}
export interface AcsRequest {
name: string;
next?: string;
}
export interface GetAcsRequest extends AcsRequest {
name: "GetParameterNames" | "GetParameterValues";
objectName?: string;
parameterNames?: string[];
parameterPath?: string;
nextLevel?: boolean;
instanceValues?: { [name: string]: string };
}
export interface SetAcsRequest extends AcsRequest {
name:
| "SetParameterValues"
| "AddObject"
| "DeleteObject"
| "FactoryReset"
| "Reboot"
| "Download";
parameterList?: [string, string | number | boolean, string][];
instanceValues?: { [param: string]: string | number | boolean };
objectName?: string;
DATETIME_MILLISECONDS?: boolean;
BOOLEAN_LITERAL?: boolean;
commandKey?: string;
instance?: string;
fileType?: string;
fileSize?: number;
url?: string;
fileName?: string;
targetFileName?: string;
}
export interface CpeResponse {
name: string;
}
export interface SpvFault {
parameterName: string;
faultCode: string;
faultString: string;
}
export interface FaultStruct {
faultCode: string;
faultString: string;
setParameterValuesFault?: SpvFault[];
}
export interface CpeFault {
faultCode: string;
faultString: string;
detail?: FaultStruct;
}
export interface CpeGetResponse extends CpeResponse {
name: "GetParameterNamesResponse" | "GetParameterValuesResponse";
parameterList?:
| [string, boolean][]
| [string, string | number | boolean, string][];
}
export interface CpeSetResponse extends CpeResponse {
name:
| "SetParameterValuesResponse"
| "AddObjectResponse"
| "DeleteObjectResponse"
| "RebootResponse"
| "FactoryResetResponse"
| "DownloadResponse";
status?: number;
instanceNumber?: string;
startTime?: number;
completeTime?: number;
}
export interface CpeRequest {
name: string;
fileType?: string;
}
export interface InformRequest extends CpeRequest {
name: "Inform";
deviceId: {
Manufacturer: string;
OUI: string;
ProductClass?: string;
SerialNumber: string;
};
event: string[];
retryCount: number;
parameterList: [string, string | number | boolean, string][];
}
export interface AcsResponse {
name: string;
commandKey?: string;
faultStruct?: FaultStruct;
startTime?: number;
completeTime?: number;
}
export interface QueryOptions {
projection?: any;
skip?: number;
limit?: number;
sort?: {
[param: string]: number;
};
}
export interface Declaration {
path: Path;
pathGet: number;
pathSet?: [number, number];
attrGet?: { object?: number; writable?: number; value?: number };
attrSet?: {
object?: boolean;
writable?: boolean;
value?: [string | number | boolean, string?];
};
defer: boolean;
}
export type Clear = [
Path,
number,
{ object?: number; writable?: number; value?: number }?,
number?
];
export interface Preset {
name: string;
channel: string;
schedule?: { md5: string; duration: number; schedule: any };
events?: { [event: string]: boolean };
precondition?: {};
provisions: string[][];
}
export type PermissionSet = {
[resource: string]: {
access: number;
validate?: {
[validator: string]: boolean;
};
filter: Expression;
};
}[];
+160
View File
@@ -0,0 +1,160 @@
import { Z_SYNC_FLUSH } from "zlib";
import Koa from "koa";
import Router from "koa-router";
import * as jwt from "jsonwebtoken";
import koaStatic from "koa-static";
import koaCompress from "koa-compress";
import koaBodyParser from "koa-bodyparser";
import koaJwt from "koa-jwt";
import * as config from "./config";
import api from "./ui/api";
import Authorizer from "./common/authorizer";
import * as logger from "./logger";
import * as localCache from "./local-cache";
import { PermissionSet } from "./types";
import { authSimple } from "./ui/api-functions";
declare module "koa" {
interface Request {
body: any;
}
}
const koa = new Koa();
const router = new Router();
const JWT_SECRET = "" + config.get("UI_JWT_SECRET");
const JWT_COOKIE = "genieacs-ui-jwt";
function getPermissionSets(ctx): PermissionSet[] {
const allPermissions = localCache.getPermissions(ctx.state.configSnapshot);
const permissionSets = ctx.state.user.roles.map(role =>
Object.values(allPermissions[role] || {})
);
return permissionSets;
}
koa.on("error", async err => {
throw err;
});
koa.use(async (ctx, next) => {
const configSnapshot = await localCache.getCurrentSnapshot();
ctx.state.configSnapshot = configSnapshot;
return next();
});
koa.use(
koaJwt({
secret: JWT_SECRET,
passthrough: true,
cookie: JWT_COOKIE
})
);
koa.use(async (ctx, next) => {
if (ctx.state.user && ctx.state.user.roles)
ctx.state.authorizer = new Authorizer(getPermissionSets(ctx));
else ctx.state.authorizer = new Authorizer([]);
return next();
});
router.post("/login", async ctx => {
if (!JWT_SECRET) {
ctx.status = 500;
ctx.body = "UI_JWT_SECRET is not set";
logger.error({ message: "UI_JWT_SECRET is not set" });
return;
}
const username = ctx.request.body.username;
const password = ctx.request.body.password;
const log = {
message: "Log in",
context: ctx,
username: username,
method: null
};
function success(roles, method): void {
log.method = method;
const token = jwt.sign({ username, roles }, JWT_SECRET);
ctx.cookies.set(JWT_COOKIE, token);
ctx.body = JSON.stringify(token);
logger.accessInfo(log);
}
function failure(): void {
ctx.status = 400;
ctx.body = "Incorrect username or password";
log.message += " failed";
logger.accessWarn(log);
}
const roles = await authSimple(ctx.state.configSnapshot, username, password);
if (roles) return void success(roles, "simple");
failure();
});
router.post("/logout", async ctx => {
ctx.cookies.set(JWT_COOKIE); // Delete cookie
ctx.body = "";
logger.accessInfo({
message: "Log out",
context: ctx
});
});
koa.use(async (ctx, next) => {
if (ctx.request.type === "application/octet-stream")
ctx.disableBodyParser = true;
return next();
});
koa.use(koaBodyParser());
router.use("/api", api.routes(), api.allowedMethods());
router.get("/", async ctx => {
let permissionSets = [];
if (ctx.state.user && ctx.state.user.roles)
permissionSets = getPermissionSets(ctx);
ctx.body = `
<html>
<head>
<title>GenieACS</title>
<link rel="shortcut icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="app.css">
</head>
<body>
<script>
window.clientConfig = ${JSON.stringify({
ui: localCache.getUiConfig(ctx.state.configSnapshot)
})};
window.username = ${JSON.stringify(
ctx.state.user ? ctx.state.user.username : ""
)};
window.permissionSets = ${JSON.stringify(permissionSets)};
</script>
<script src="app.js"></script>
</body>
</html>
`;
});
koa.use(
koaCompress({
flush: Z_SYNC_FLUSH
})
);
koa.use(router.routes());
koa.use(koaStatic("./public"));
export const listener = koa.callback();
+230
View File
@@ -0,0 +1,230 @@
import { exec } from "child_process";
import { ObjectID } from "mongodb";
import * as db from "./db";
import { del } from "../cache";
import { getUsers } from "../local-cache";
import { hashPassword } from "../auth";
import {
insertTasks,
watchTask,
connectionRequest,
deleteDevice
} from "../api-functions";
function deleteFault(id): Promise<void> {
const deviceId = id.split(":", 1)[0];
const channel = id.slice(deviceId.length + 1);
return new Promise((resolve, reject) => {
Promise.all([
db.deleteFault(id),
channel.startsWith("task_")
? db.deleteTask(new ObjectID(channel.slice(5)))
: null
])
.then(() => {
del(`${deviceId}_tasks_faults_operations`, err => {
err ? reject(err) : resolve();
});
})
.catch(reject);
});
}
export function deleteResource(resource, id): Promise<void> {
return new Promise((resolve, reject) => {
let promise;
switch (resource) {
case "devices":
promise = new Promise((res, rej) => {
deleteDevice(id, err => {
if (err) return void rej(err);
res();
});
});
break;
case "files":
promise = db.deleteFile(id);
break;
case "faults":
promise = deleteFault(id);
break;
case "provisions":
promise = db.deleteProvision(id);
break;
case "presets":
promise = db.deletePreset(id);
break;
case "virtualParameters":
promise = db.deleteVirtualParameter(id);
break;
case "config":
promise = db.deleteConfig(id);
break;
case "permissions":
promise = db.deletePermission(id);
break;
case "users":
promise = db.deleteUser(id);
break;
}
promise
.then(() => {
del("presets_hash", err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
}
export function postTasks(
deviceId,
tasks,
timeout
): Promise<{ connectionRequest: string; tasks: any[] }> {
return new Promise((resolve, reject) => {
for (const task of tasks) task.device = deviceId;
insertTasks(tasks, err => {
if (err) return void reject(err);
const statuses = tasks.map(t => {
return { _id: t._id, status: "pending" };
});
del(`${deviceId}_tasks_faults_operations`, err => {
if (err) return void reject(err);
connectionRequest(deviceId, err => {
if (err) {
return void resolve({
connectionRequest: err.message,
tasks: statuses
});
}
const sample = tasks[tasks.length - 1];
// Waiting for session to finish or timeout
watchTask(deviceId, sample._id, timeout, err => {
if (err) return void reject(err);
const promises2 = [];
for (const s of statuses) {
promises2.push(db.query("tasks", ["=", ["PARAM", "_id"], s._id]));
promises2.push(
db.query("faults", [
"=",
["PARAM", "_id"],
`${deviceId}:task_${s._id}`
])
);
}
Promise.all(promises2)
.then(res => {
for (const [i, r] of statuses.entries()) {
if (res[i * 2].length === 0) {
r.status = "done";
} else if (res[i * 2 + 1].length === 1) {
r.status = "fault";
r.fault = res[i * 2 + 1][0];
}
db.deleteTask(r._id);
}
resolve({ connectionRequest: "OK", tasks: statuses });
})
.catch(reject);
});
});
});
});
});
}
interface PingResponse {
packetsTransmitted: number;
packetsReceived: number;
packetLoss: string;
min: number;
avg: number;
max: number;
mdev: number;
}
export function ping(host): Promise<PingResponse> {
return new Promise((resolve, reject) => {
exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => {
if (err) return void reject(err);
const m = stdout.match(
/(\d) packets transmitted, (\d) received, ([\d.%]+) packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/
);
if (!m) return void reject(new Error("Could not parse ping response"));
resolve({
packetsTransmitted: +m[1],
packetsReceived: +m[2],
packetLoss: m[3],
min: +m[4],
avg: +m[5],
max: +m[6],
mdev: +m[7]
});
});
});
}
export function putResource(resource, id, data): Promise<void> {
return new Promise((resolve, reject) => {
let promise;
if (resource === "presets") {
promise = db.putPreset(id, data);
} else if (resource === "provisions") {
promise = db.putProvision(id, data);
} else if (resource === "virtualParameters") {
promise = db.putVirtualParameter(id, data);
} else if (resource === "config") {
promise = db.putConfig(id, data);
} else if (resource === "permissions") {
promise = db.putPermission(id, data);
} else if (resource === "users") {
delete data.password;
delete data.salt;
promise = db.putUser(id, data);
}
promise
.then(() => {
del("presets_hash", err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
}
export function authSimple(snapshot, username, password): Promise<string[]> {
return new Promise((resolve, reject) => {
const users = getUsers(snapshot);
const user = users[username];
if (!user || !user.password) return void resolve(null);
hashPassword(password, user.salt)
.then(hash => {
if (hash === user.password) resolve(user.roles);
else resolve(null);
})
.catch(reject);
});
}
+562
View File
@@ -0,0 +1,562 @@
import * as stream from "stream";
import Router from "koa-router";
import * as db from "./db";
import * as apiFunctions from "./api-functions";
import { evaluate, and, extractParams } from "../common/expression";
import { parse } from "../common/expression-parser";
import * as logger from "../logger";
import { getConfig } from "../local-cache";
import { QueryOptions, Expression } from "../types";
import { generateSalt, hashPassword } from "../auth";
import { del } from "../cache";
const router = new Router();
export default router;
function logUnauthorizedWarning(log): void {
log.message += " not authorized";
logger.accessWarn(log);
}
const RESOURCE_DELETE = 1 << 0;
const RESOURCE_PUT = 1 << 1;
const RESOURCE_IDS = {
devices: "DeviceID.ID",
presets: "_id",
provisions: "_id",
files: "_id",
virtualParameters: "_id",
config: "_id",
permissions: "_id",
users: "_id",
faults: "_id",
tasks: "_id"
};
const resources = {
devices: 0 | RESOURCE_DELETE,
presets: 0 | RESOURCE_DELETE | RESOURCE_PUT,
provisions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
files: 0 | RESOURCE_DELETE,
virtualParameters: 0 | RESOURCE_DELETE | RESOURCE_PUT,
config: 0 | RESOURCE_DELETE | RESOURCE_PUT,
permissions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
users: 0 | RESOURCE_DELETE | RESOURCE_PUT,
faults: 0 | RESOURCE_DELETE,
tasks: 0
};
router.get(`/devices/:id.csv`, async ctx => {
const log = {
message: "Query device (CSV)",
context: ctx,
id: ctx.params.id
};
const filter = ["=", ["PARAM", RESOURCE_IDS.devices], ctx.params.id];
if (!ctx.state.authorizer.hasAccess("devices", 2)) {
logUnauthorizedWarning(log);
return void (ctx.status = 404);
}
const res = await db.query("devices", filter);
if (!res[0]) return void (ctx.status = 404);
ctx.type = "text/csv";
ctx.attachment(
`device-${ctx.params.id}-${new Date()
.toISOString()
.replace(/[:.]/g, "")}.csv`
);
ctx.body = new stream.PassThrough();
ctx.body.write(
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp\n"
);
for (const k of Object.keys(res[0]).sort()) {
const p = res[0][k];
const row = [
k,
p.object,
p.objectTimestamp,
p.writable,
p.writableTimestamp,
p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "",
p.value != null ? p.value[1] : "",
p.valueTimestamp
];
ctx.body.write(row.map(r => (r != null ? r : "")).join(",") + "\n");
}
ctx.body.end();
logger.accessInfo(log);
});
for (const [resource, flags] of Object.entries(resources)) {
router.head(`/${resource}`, async (ctx, next) => {
let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
const log = {
message: `Count ${resource}`,
context: ctx,
filter: ctx.request.query.filter,
count: null
};
if (!ctx.state.authorizer.hasAccess(resource, 1)) {
logUnauthorizedWarning(log);
return void next();
}
// Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") {
filter = and(filter, [
"NOT",
["<", ["PARAM", "expiry"], Date.now() + 60000]
]);
}
const count = await db.count(resource, filter);
ctx.set("X-Total-Count", `${count}`);
ctx.body = "";
log.count = count;
logger.accessInfo(log);
});
router.get(`/${resource}`, async (ctx, next) => {
const options: QueryOptions = {};
let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
if (ctx.request.query.sort)
options.sort = JSON.parse(ctx.request.query.sort);
if (ctx.request.query.projection) {
options.projection = ctx.request.query.projection
.split(",")
.reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {});
}
const log = {
message: `Query ${resource}`,
context: ctx,
filter: ctx.request.query.filter,
limit: options.limit,
skip: options.skip,
sort: options.sort,
projection: options.projection
};
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
}
// Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") {
filter = and(filter, [
"NOT",
["<", ["PARAM", "expiry"], Date.now() + 60000]
]);
}
ctx.body = new stream.PassThrough();
ctx.type = "application/json";
let c = 0;
ctx.body.write("[\n");
await db.query(resource, filter, options, obj => {
ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n");
});
ctx.body.end("]");
logger.accessInfo(log);
});
// CSV download
router.get(`/${resource}.csv`, async (ctx, next) => {
const options: QueryOptions = { projection: {} };
let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
if (ctx.request.query.sort)
options.sort = JSON.parse(ctx.request.query.sort);
const log = {
message: `Query ${resource} (CSV)`,
context: ctx,
filter: ctx.request.query.filter,
limit: options.limit,
skip: options.skip,
sort: options.sort
};
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
}
const columns = JSON.parse(ctx.request.query.columns);
const now = Date.now();
for (const [k, v] of Object.entries(columns)) {
const e = evaluate(parse(v), null, now);
columns[k] = e;
for (const p of extractParams(e)) options.projection[p] = 1;
}
// Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") {
filter = and(filter, [
"NOT",
["<", ["PARAM", "expiry"], Date.now() + 60000]
]);
}
ctx.body = new stream.PassThrough();
ctx.type = "text/csv";
ctx.attachment(
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`
);
ctx.body.write(
Object.keys(columns).map(k => `"${k.replace(/"/, '""')}"`) + "\n"
);
await db.query(resource, filter, options, obj => {
const arr = Object.values(columns).map(exp => {
const v = evaluate(exp, obj, null, e => {
if (Array.isArray(e)) {
if (e[0] === "PARAM") {
if (resource === "devices") {
if (e[1] === "Tags") {
const tags = [];
for (const p in obj)
if (p.startsWith("Tags.")) tags.push(p.slice(5));
return tags.join(", ");
}
}
} else if (e[0] === "FUNC") {
if (e[1] === "DATE_STRING") {
if (e[2] && !Array.isArray(e[2]))
return new Date(e[2]).toISOString();
}
}
}
return e;
});
if (Array.isArray(v) || v == null) return "";
if (typeof v === "string") return `"${v.replace(/"/g, '""')}"`;
return v;
});
ctx.body.write(arr.join(",") + "\n");
});
ctx.body.end();
logger.accessInfo(log);
});
router.head(`/${resource}/:id`, async (ctx, next) => {
const log = {
message: `Count ${resource}`,
context: ctx,
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
};
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
}
const res = await db.query(resource, filter);
if (!res.length) return void next();
logger.accessInfo(log);
ctx.body = "";
});
router.get(`/${resource}/:id`, async (ctx, next) => {
const log = {
message: `Query ${resource}`,
context: ctx,
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
};
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void next();
}
const res = await db.query(resource, filter);
if (!res.length) return void next();
logger.accessInfo(log);
ctx.body = res[0];
});
// TODO add PUT, PATCH routes
if (flags & RESOURCE_DELETE) {
router.delete(`/${resource}/:id`, async (ctx, next) => {
const log = {
message: `Delete ${resource}`,
context: ctx,
id: ctx.params.id
};
const authorizer = ctx.state.authorizer;
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
}
const res = await db.query(resource, filter);
if (!res.length) return void next();
const validate = authorizer.getValidator(resource, res[0]);
if (!validate("delete")) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
await apiFunctions.deleteResource(resource, ctx.params.id);
logger.accessInfo(log);
ctx.body = "";
});
}
if (flags & RESOURCE_PUT) {
router.put(`/${resource}/:id`, async (ctx, next) => {
const id = ctx.params.id;
const log = {
message: `Put ${resource}`,
context: ctx,
id: id
};
const authorizer = ctx.state.authorizer;
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
}
const obj = ctx.request.body;
const validate = authorizer.getValidator(resource, obj);
if (!validate("put")) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
try {
await apiFunctions.putResource(resource, id, obj);
} catch (err) {
log.message += " failed";
logger.accessWarn(log);
ctx.body = err;
return void (ctx.status = 400);
}
logger.accessInfo(log);
ctx.body = "";
});
}
}
router.put("/files/:id", async (ctx, next) => {
const resource = "files";
const id = ctx.params.id;
const log = {
message: `Upload ${resource}`,
context: ctx,
id: id,
metadata: null
};
const authorizer = ctx.state.authorizer;
if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log);
return void next();
}
const metadata = {
fileType: ctx.request.headers["metadata.filetype"] || "",
oui: ctx.request.headers["metadata.oui"] || "",
productClass: ctx.request.headers["metadata.productclass"] || "",
version: ctx.request.headers["metadata.version"] || ""
};
const validate = authorizer.getValidator(resource, metadata);
if (!validate("put")) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
await db.putFile(id, metadata, ctx.req);
log.metadata = metadata;
logger.accessInfo(log);
ctx.body = "";
});
router.post("/devices/:id/tasks", async (ctx, next) => {
const log = {
message: "Commit tasks",
context: ctx,
deviceId: ctx.params.id,
tasks: null
};
const authorizer = ctx.state.authorizer;
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log);
return void next();
}
const devices = await db.query("devices", filter);
if (!devices.length) return void next();
const validate = authorizer.getValidator("devices", devices[0]);
for (const t of ctx.request.body) {
if (!validate("task", t)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
}
// TODO Pass context and timestamp
const onlineThreshold = getConfig(
ctx.state.configSnapshot,
"cwmp.deviceOnlineThreshold"
);
const res = await apiFunctions.postTasks(
ctx.params.id,
ctx.request.body,
onlineThreshold
);
log.tasks = res.tasks.map(t => t._id).join(",");
logger.accessInfo(log);
ctx.set("Connection-Request", res.connectionRequest);
ctx.body = res.tasks;
});
router.post("/devices/:id/tags", async (ctx, next) => {
const log = {
message: "Update tags",
context: ctx,
deviceId: ctx.params.id,
tags: ctx.request.body
};
const authorizer = ctx.state.authorizer;
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log);
return void next();
}
const res = await db.query("devices", filter);
if (!res.length) return void next();
const validate = authorizer.getValidator("devices", res[0]);
if (!validate("tags", ctx.request.body)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
await db.updateDeviceTags(ctx.params.id, ctx.request.body);
logger.accessInfo(log);
ctx.body = "";
});
router.get("/ping/:host", async ctx => {
try {
ctx.body = await apiFunctions.ping(ctx.params.host);
} catch (err) {
ctx.status = 404;
ctx.response.headers["Cache-Control"] = "no-cache";
ctx.body = `${err.name}: ${err.message}`;
}
});
router.put("/users/:id/password", async (ctx, next) => {
const username = ctx.params.id;
const log = {
message: "Change password",
context: ctx,
username: username
};
const authorizer = ctx.state.authorizer;
if (!ctx.state.user) {
// User not logged in
if (
!(await apiFunctions.authSimple(
ctx.state.configSnapshot,
username,
ctx.request.body.authPassword
))
) {
logUnauthorizedWarning(log);
ctx.status = 401;
ctx.body = "Authentication failed, check your username and password";
return;
}
} else if (!authorizer.hasAccess("users", 3)) {
logUnauthorizedWarning(log);
return void next();
}
const filter = ["=", ["PARAM", RESOURCE_IDS.users], username];
const res = await db.query("users", filter);
if (!res.length) return void next();
const newPassword = ctx.request.body.newPassword;
if (ctx.state.user) {
const validate = authorizer.getValidator("users", res[0]);
if (!validate("password", { password: newPassword })) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
}
const salt = await generateSalt(64);
const password = await hashPassword(newPassword, salt);
await db.putUser(username, { password, salt });
function delCache(): Promise<void> {
return new Promise((resolve, reject) => {
del("presets_hash", err => {
if (err) return void reject(err);
resolve();
});
});
}
await delCache();
logger.accessInfo(log);
ctx.body = "";
});
+375
View File
@@ -0,0 +1,375 @@
import { MongoClient, GridFSBucket } from "mongodb";
import * as config from "../config";
import * as mongodbFunctions from "../mongodb-functions";
import * as expression from "../common/expression";
import { QueryOptions } from "../types";
const CACHE_TTL = 300000;
let _clientPromise = null;
const RESOURCE_COLLECTION = {
files: "fs.files"
};
function ensureIndexes(client): void {
client
.db()
.collection("cache")
.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
}
function getClient(): Promise<MongoClient> {
if (!_clientPromise) {
_clientPromise = new Promise((resolve, reject) => {
const CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
MongoClient.connect(
CONNECTION_URL,
{ useNewUrlParser: true },
(err, client) => {
if (err) return void reject(err);
ensureIndexes(client);
resolve(client);
}
);
});
}
return _clientPromise;
}
export function cache<T>(key, valueGetter: () => Promise<T>, ttl): Promise<T> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const collection = client.db().collection("cache");
collection.findOne({ _id: key }, (err, doc) => {
if (err) return void reject(err);
if (doc != null) return void resolve(JSON.parse(doc.value) as T);
valueGetter()
.then(res => {
const expire = Date.now() + (ttl || CACHE_TTL);
const cacheDoc = {
_id: key,
value: JSON.stringify(res),
expire: new Date(expire)
};
collection.updateOne(
{ _id: key },
{ $set: cacheDoc },
{ upsert: true },
err => {
if (err) reject(err);
else resolve(res);
}
);
})
.catch(reject);
});
})
.catch(reject);
});
}
export function query(
resource,
filter,
options?: QueryOptions,
callback?: (doc: any) => void
): Promise<any[]> {
options = options || {};
let q;
filter = expression.evaluate(filter, null, Date.now());
if (Array.isArray(filter)) {
if (resource === "devices") {
filter = mongodbFunctions.processDeviceFilter(filter);
} else if (resource === "tasks") {
filter = mongodbFunctions.processTasksFilter(filter);
} else if (resource === "faults") {
filter = mongodbFunctions.processFaultsFilter(filter);
} else if (resource === "users") {
// Protect against brute force, and dictionary attacks
const params = expression.extractParams(filter);
if (params.includes("password") || params.includes("salt"))
return Promise.reject(new Error("Invalid users filter"));
}
q = mongodbFunctions.filterToMongoQuery(filter);
} else if (filter != null && !filter) {
return Promise.resolve([]);
}
return new Promise((resolve, reject) => {
getClient().then(client => {
const collection = client
.db()
.collection(RESOURCE_COLLECTION[resource] || resource);
const cursor = collection.find(q);
if (options.projection) {
let projection = options.projection;
if (resource === "devices") {
projection = mongodbFunctions.processDeviceProjection(
options.projection
);
}
if (resource === "presets") projection.configurations = 1;
cursor.project(projection);
}
if (resource === "users") cursor.project({ password: 0, salt: 0 });
if (options.skip) cursor.skip(options.skip);
if (options.limit) cursor.limit(options.limit);
if (options.sort) {
let s = Object.entries(options.sort)
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
.reduce(
(obj, [k, v]) =>
Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }),
{}
);
if (resource === "devices") s = mongodbFunctions.processDeviceSort(s);
cursor.sort(s);
}
if (!callback) {
cursor.toArray((err, docs) => {
if (err) return reject(err);
if (resource === "devices")
docs = docs.map(d => mongodbFunctions.flattenDevice(d));
else if (resource === "faults")
docs = docs.map(d => mongodbFunctions.flattenFault(d));
else if (resource === "tasks")
docs = docs.map(d => mongodbFunctions.flattenTask(d));
else if (resource === "presets")
docs = docs.map(d => mongodbFunctions.flattenPreset(d));
else if (resource === "files")
docs = docs.map(d => mongodbFunctions.flattenFile(d));
return resolve(docs);
});
} else {
cursor.forEach(
doc => {
if (resource === "devices")
doc = mongodbFunctions.flattenDevice(doc);
else if (resource === "faults")
doc = mongodbFunctions.flattenFault(doc);
else if (resource === "tasks")
doc = mongodbFunctions.flattenTask(doc);
else if (resource === "presets")
doc = mongodbFunctions.flattenPreset(doc);
else if (resource === "files")
doc = mongodbFunctions.flattenFile(doc);
callback(doc);
},
err => {
if (err) reject(err);
else resolve();
}
);
}
});
});
}
export function count(resource, filter): Promise<number> {
let q;
filter = expression.evaluate(filter, null, Date.now());
if (Array.isArray(filter)) {
if (resource === "devices")
filter = mongodbFunctions.processDeviceFilter(filter);
else if (resource === "tasks")
filter = mongodbFunctions.processTasksFilter(filter);
else if (resource === "faults")
filter = mongodbFunctions.processFaultsFilter(filter);
q = mongodbFunctions.filterToMongoQuery(filter);
} else if (filter != null && !filter) {
return Promise.resolve(0);
}
return new Promise((resolve, reject) => {
getClient().then(client => {
const collection = client
.db()
.collection(RESOURCE_COLLECTION[resource] || resource);
collection.find(q).count((err, c) => {
if (err) reject(err);
else resolve(c);
});
});
});
}
export function updateDeviceTags(deviceId, tags): Promise<void> {
return new Promise((resolve, reject) => {
const add = [];
const pull = [];
for (const [tag, onOff] of Object.entries(tags)) {
if (onOff) add.push(tag);
else pull.push(tag);
}
getClient()
.then(client => {
const collection = client.db().collection("devices");
const object = {};
if (add && add.length) object["$addToSet"] = { _tags: { $each: add } };
if (pull && pull.length) object["$pullAll"] = { _tags: pull };
collection.updateOne({ _id: deviceId }, object, err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
}
function putResource(resource, id, object): Promise<void> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const collection = client
.db()
.collection(RESOURCE_COLLECTION[resource] || resource);
collection.replaceOne({ _id: id }, object, { upsert: true }, err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
}
function deleteResource(resource, id): Promise<void> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const collection = client
.db()
.collection(RESOURCE_COLLECTION[resource] || resource);
collection.deleteOne({ _id: id }, err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
}
export function putPreset(id, object): Promise<void> {
object = mongodbFunctions.preProcessPreset(object);
return putResource("presets", id, object);
}
export function deletePreset(id): Promise<void> {
return deleteResource("presets", id);
}
export function putProvision(id, object): Promise<void> {
return putResource("provisions", id, object);
}
export function deleteProvision(id): Promise<void> {
return deleteResource("provisions", id);
}
export function putVirtualParameter(id, object): Promise<void> {
return putResource("virtualParameters", id, object);
}
export function deleteVirtualParameter(id): Promise<void> {
return deleteResource("virtualParameters", id);
}
export function putConfig(id, object): Promise<void> {
return putResource("config", id, object);
}
export function deleteConfig(id): Promise<void> {
return deleteResource("config", id);
}
export function putPermission(id, object): Promise<void> {
return putResource("permissions", id, object);
}
export function deletePermission(id): Promise<void> {
return deleteResource("permissions", id);
}
export function putUser(id, object): Promise<void> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const collection = client.db().collection("users");
// update instead of replace to keep the password if not set by user
collection.updateOne(
{ _id: id },
{ $set: object },
{ upsert: true },
err => {
if (err) return void reject(err);
resolve();
}
);
})
.catch(reject);
});
}
export function deleteUser(id): Promise<void> {
return deleteResource("users", id);
}
export function putFile(filename, metadata, contentStream): Promise<void> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const bucket = new GridFSBucket(client.db());
const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
metadata: metadata
});
uploadStream.on("error", reject);
contentStream.on("error", reject);
uploadStream.on("finish", resolve);
contentStream.pipe(uploadStream);
})
.catch(reject);
});
}
export function deleteFile(filename): Promise<void> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const bucket = new GridFSBucket(client.db());
bucket.delete(filename, err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
}
export function deleteFault(id): Promise<void> {
return deleteResource("faults", id);
}
export function deleteTask(id): Promise<void> {
return deleteResource("tasks", id);
}
export function disconnect(): void {
if (_clientPromise) {
_clientPromise.then(client => {
client.close();
});
}
}
+35 -28
View File
@@ -16,44 +16,53 @@
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
const NONEXISTENT = new Object();
const NONEXISTENT = Symbol();
const UNDEFINED = undefined;
class VersionedMap {
constructor() {
interface Revisions<V> {
[rev: number]: V;
delete?: number;
}
export default class VersionedMap<K, V> {
private _sizeDiff: number[];
private _revision: number;
private map: Map<K, (V | symbol)[]>;
public dirty: number;
public constructor() {
this._sizeDiff = [0];
this._revision = 0;
this.map = new Map();
this.dirty = 0;
}
get size() {
public get size(): number {
return this.map.size + this._sizeDiff[this.revision];
}
get revision() {
public get revision(): number {
return this._revision;
}
set revision(rev) {
public set revision(rev) {
for (let i = this._sizeDiff.length; i <= rev; ++i)
this._sizeDiff[i] = this._sizeDiff[i - 1];
this._revision = rev;
}
get(key, rev = this._revision) {
public get(key: K, rev = this._revision): V {
const revisions = this.map.get(key);
if (!revisions) return UNDEFINED;
const v = revisions[Math.min(revisions.length - 1, rev)];
if (v === NONEXISTENT) return UNDEFINED;
return v;
return v as V;
}
has(key, rev = this._revision) {
public has(key: K, rev = this._revision): boolean {
const revisions = this.map.get(key);
if (!revisions) return false;
@@ -62,7 +71,7 @@ class VersionedMap {
return true;
}
set(key, value, rev = this._revision) {
public set(key: K, value: V, rev = this._revision): this {
let revisions = this.map.get(key);
if (!revisions) {
this.dirty |= 1 << rev;
@@ -72,7 +81,7 @@ class VersionedMap {
for (let i = 0; i < rev; ++i) revisions[i] = NONEXISTENT;
revisions[rev] = value;
this.map.set(key, revisions);
return true;
return this;
}
// Can't modify old revisions
@@ -86,10 +95,10 @@ class VersionedMap {
for (let i = revisions.length; i < rev; ++i) revisions[i] = old;
revisions[rev] = value;
return true;
return this;
}
delete(key, rev = this._revision) {
public delete(key: K, rev = this._revision): boolean {
const revisions = this.map.get(key);
if (!revisions) return false;
@@ -108,24 +117,24 @@ class VersionedMap {
return true;
}
getRevisions(key) {
public getRevisions(key: K): Revisions<V> {
const revisions = this.map.get(key);
if (!revisions) return null;
const res = {};
const res: Revisions<V> = {};
let prev = NONEXISTENT;
let prev: V | symbol = NONEXISTENT;
for (const [i, v] of revisions.entries()) {
if (v === prev) continue;
if (v === NONEXISTENT) res.delete |= 1 << i;
else res[i] = v;
else res[i] = v as V;
prev = v;
}
return res;
}
setRevisions(key, revisionsObj) {
public setRevisions(key: K, revisionsObj: Revisions<V>): void {
const del = revisionsObj.delete || 0;
const mutations = Object.keys(revisionsObj).reduce(
(acc, cur) => (cur === "delete" ? acc : acc | (1 << +cur)),
@@ -134,7 +143,7 @@ class VersionedMap {
const revisions = [];
let prev = NONEXISTENT;
let prev: V | symbol = NONEXISTENT;
for (let i = 0; mutations >> i; ++i) {
let v = prev;
if (del & (1 << i)) v = NONEXISTENT;
@@ -147,28 +156,28 @@ class VersionedMap {
this.map.set(key, revisions);
}
getDiff(key) {
public getDiff(key: K): [V, V] {
const revisions = this.map.get(key);
if (!revisions) return [UNDEFINED, UNDEFINED];
let first = revisions[0];
if (first === NONEXISTENT) first = UNDEFINED;
let last = revisions[revisions.length - 1];
if (last === NONEXISTENT) last = UNDEFINED;
return [first, last];
return [first as V, last as V];
}
*diff() {
public *diff(): IterableIterator<[K, V, V]> {
for (const [key, revisions] of this.map) {
let first = revisions[0];
let last = revisions[revisions.length - 1];
if ((first === last) === NONEXISTENT) continue;
if (first === NONEXISTENT && last === NONEXISTENT) continue;
if (first === NONEXISTENT) first = UNDEFINED;
if (last === NONEXISTENT) last = UNDEFINED;
yield [key, first, last];
yield [key, first as V, last as V];
}
}
collapse(revision) {
public collapse(revision: number): void {
if (this._sizeDiff.length <= revision) return;
this._sizeDiff[revision] = this._sizeDiff[this._sizeDiff.length - 1];
@@ -189,5 +198,3 @@ class VersionedMap {
}
}
}
module.exports = VersionedMap;