Merge branch 'v1.2'
This commit is contained in:
commit
c6db1edc5f
122 files changed
+26076
-2742
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
!/bin/.*.ts
|
||||||
|
!/tools/.*.ts
|
||||||
+7
-4
@@ -3,11 +3,12 @@
|
|||||||
"es6": true,
|
"es6": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"sourceType": "script",
|
"project": "./tsconfig.json"
|
||||||
"ecmaVersion": 8
|
|
||||||
},
|
},
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "prettier/@typescript-eslint"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-shadow": ["error", { "allow": ["err"] }],
|
"no-shadow": ["error", { "allow": ["err"] }],
|
||||||
"handle-callback-err": "error",
|
"handle-callback-err": "error",
|
||||||
@@ -16,6 +17,8 @@
|
|||||||
"consistent-return": ["error", {"treatUndefinedAsUnspecified": true}],
|
"consistent-return": ["error", {"treatUndefinedAsUnspecified": true}],
|
||||||
"no-buffer-constructor": "error",
|
"no-buffer-constructor": "error",
|
||||||
"prefer-const": ["error", {"destructuring": "all"}],
|
"prefer-const": ["error", {"destructuring": "all"}],
|
||||||
"eqeqeq": ["error", "always", {"null": "ignore"}]
|
"eqeqeq": ["error", "always", {"null": "ignore"}],
|
||||||
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
|
"@typescript-eslint/no-use-before-define": ["error", { "functions": false }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -1,8 +1,9 @@
|
|||||||
*~
|
*~
|
||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
debug
|
debug
|
||||||
config/auth.js
|
|
||||||
config/config.json
|
config/config.json
|
||||||
|
config/config-ui.json
|
||||||
config/cwmp.key
|
config/cwmp.key
|
||||||
config/cwmp.crt
|
config/cwmp.crt
|
||||||
config/cwmp.ca-bundle
|
config/cwmp.ca-bundle
|
||||||
@@ -13,3 +14,5 @@ config/fs.key
|
|||||||
config/fs.crt
|
config/fs.crt
|
||||||
config/fs.ca-bundle
|
config/fs.ca-bundle
|
||||||
config/ext
|
config/ext
|
||||||
|
bin/.*.ts
|
||||||
|
tools/.*.ts
|
||||||
+6
-1
@@ -1,7 +1,6 @@
|
|||||||
*~
|
*~
|
||||||
.npmignore
|
.npmignore
|
||||||
|
|
||||||
config/auth.js
|
|
||||||
config/config.json
|
config/config.json
|
||||||
config/cwmp.key
|
config/cwmp.key
|
||||||
config/cwmp.crt
|
config/cwmp.crt
|
||||||
@@ -17,4 +16,10 @@ config/ext
|
|||||||
debug/*
|
debug/*
|
||||||
!debug/.gitkeep
|
!debug/.gitkeep
|
||||||
|
|
||||||
|
dist
|
||||||
.eslintrc.json
|
.eslintrc.json
|
||||||
|
.eslintignore
|
||||||
|
ava.config.js
|
||||||
|
tsconfig.json
|
||||||
|
bin/.*
|
||||||
|
tools/.*
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
"compileEnhancements": false,
|
||||||
|
"extensions": ["ts"],
|
||||||
|
"require": ["esm", "ts-node/register/transpile-only"]
|
||||||
|
};
|
||||||
|
|
||||||
+111
-7
@@ -1,9 +1,113 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||||
var path = require('path');
|
import * as path from "path";
|
||||||
var fs = require('fs');
|
import * as fs from "fs";
|
||||||
|
|
||||||
var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..');
|
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
|
||||||
process.chdir(dir);
|
|
||||||
|
|
||||||
var cluster = require(path.resolve(dir, 'lib/cluster'));
|
import * as config from "../lib/config";
|
||||||
cluster.start('cwmp');
|
import * as logger from "../lib/logger";
|
||||||
|
import * as cluster from "../lib/cluster";
|
||||||
|
import * as server from "../lib/server";
|
||||||
|
import * as cwmp from "../lib/cwmp";
|
||||||
|
import * as db from "../lib/db";
|
||||||
|
import * as extensions from "../lib/extensions";
|
||||||
|
import * as cache from "../lib/cache";
|
||||||
|
import { version as VERSION } from "../package.json";
|
||||||
|
|
||||||
|
logger.init("cwmp", VERSION);
|
||||||
|
|
||||||
|
const SERVICE_ADDRESS = config.get("CWMP_INTERFACE");
|
||||||
|
const SERVICE_PORT = config.get("CWMP_PORT");
|
||||||
|
|
||||||
|
function exitWorkerGracefully(): void {
|
||||||
|
db.disconnect();
|
||||||
|
cache.disconnect();
|
||||||
|
extensions.killAll();
|
||||||
|
cluster.worker.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWorkerUngracefully(): void {
|
||||||
|
extensions.killAll(() => {
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cluster.worker) {
|
||||||
|
const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES");
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
message: `genieacs-cwmp starting`,
|
||||||
|
pid: process.pid,
|
||||||
|
version: VERSION
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGINT, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGTERM, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let ssl;
|
||||||
|
if (config.get("CWMP_SSL")) {
|
||||||
|
ssl = {
|
||||||
|
key: path.resolve("" + config.get("CONFIG_DIR"), "cwmp.key"),
|
||||||
|
cert: path.resolve("" + config.get("CONFIG_DIR"), "cwmp.crt"),
|
||||||
|
ca: path.resolve("" + config.get("CONFIG_DIR"), "cwmp.ca-bundle")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("uncaughtException", err => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
|
logger.error({
|
||||||
|
message: "Uncaught exception",
|
||||||
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
db.connect(err => {
|
||||||
|
if (err) throw err;
|
||||||
|
cache.connect(err => {
|
||||||
|
if (err) throw err;
|
||||||
|
server.start(
|
||||||
|
SERVICE_PORT,
|
||||||
|
SERVICE_ADDRESS,
|
||||||
|
ssl,
|
||||||
|
cwmp.listener,
|
||||||
|
cwmp.onConnection
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Regular → Executable
+15
-8
@@ -1,3 +1,5 @@
|
|||||||
|
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright 2013-2018 Zaid Abdulla
|
* Copyright 2013-2018 Zaid Abdulla
|
||||||
*
|
*
|
||||||
@@ -16,18 +18,19 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
import { Fault } from "../lib/types";
|
||||||
|
|
||||||
const jobs = new Set();
|
const jobs = new Set();
|
||||||
const fileName = process.argv[2];
|
const fileName = process.argv[2];
|
||||||
let script;
|
let script;
|
||||||
|
|
||||||
function errorToFault(err) {
|
function errorToFault(err: Error): Fault {
|
||||||
if (!err) return null;
|
if (!err) return null;
|
||||||
|
|
||||||
if (!err.name) return { code: "ext", message: `${err}` };
|
if (!err.name) return { code: "ext", message: `${err}` };
|
||||||
|
|
||||||
const fault = {
|
const fault: Fault = {
|
||||||
code: `ext.${err.name}`,
|
code: `ext.${err.name}`,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
detail: {
|
detail: {
|
||||||
@@ -37,13 +40,17 @@ function errorToFault(err) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (err.stack) {
|
if (err.stack) {
|
||||||
fault.detail.stack = err.stack;
|
fault.detail["stack"] = err.stack;
|
||||||
// Trim the stack trace
|
// Trim the stack trace
|
||||||
const stackTrimIndex = fault.detail.stack.match(
|
const stackTrimIndex = fault.detail["stack"].match(
|
||||||
/\s+at\s[^\s]+\s\(.*extension-wrapper\.js:.+\)/
|
/\s+at\s[^\s]+\s\(.*genieacs-ext:.+\)/
|
||||||
);
|
);
|
||||||
if (stackTrimIndex)
|
if (stackTrimIndex) {
|
||||||
fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index);
|
fault.detail["stack"] = fault.detail["stack"].slice(
|
||||||
|
0,
|
||||||
|
stackTrimIndex.index
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fault;
|
return fault;
|
||||||
+101
-7
@@ -1,9 +1,103 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||||
var path = require('path');
|
import * as path from "path";
|
||||||
var fs = require('fs');
|
import * as fs from "fs";
|
||||||
|
|
||||||
var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..');
|
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
|
||||||
process.chdir(dir);
|
|
||||||
|
|
||||||
var cluster = require(path.resolve(dir, 'lib/cluster'));
|
import * as config from "../lib/config";
|
||||||
cluster.start('fs');
|
import * as logger from "../lib/logger";
|
||||||
|
import * as cluster from "../lib/cluster";
|
||||||
|
import * as server from "../lib/server";
|
||||||
|
import { listener } from "../lib/fs";
|
||||||
|
import * as db from "../lib/db";
|
||||||
|
import * as cache from "../lib/cache";
|
||||||
|
import { version as VERSION } from "../package.json";
|
||||||
|
|
||||||
|
logger.init("fs", VERSION);
|
||||||
|
|
||||||
|
const SERVICE_ADDRESS = config.get("FS_INTERFACE");
|
||||||
|
const SERVICE_PORT = config.get("FS_PORT");
|
||||||
|
|
||||||
|
function exitWorkerGracefully(): void {
|
||||||
|
db.disconnect();
|
||||||
|
cache.disconnect();
|
||||||
|
cluster.worker.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWorkerUngracefully(): void {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cluster.worker) {
|
||||||
|
const WORKER_COUNT = config.get("FS_WORKER_PROCESSES");
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
message: `genieacs-fs starting`,
|
||||||
|
pid: process.pid,
|
||||||
|
version: VERSION
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGINT, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGTERM, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let ssl;
|
||||||
|
if (config.get("FS_SSL")) {
|
||||||
|
ssl = {
|
||||||
|
key: path.resolve("" + config.get("CONFIG_DIR"), "fs.key"),
|
||||||
|
cert: path.resolve("" + config.get("CONFIG_DIR"), "fs.crt"),
|
||||||
|
ca: path.resolve("" + config.get("CONFIG_DIR"), "fs.ca-bundle")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("uncaughtException", err => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
|
logger.error({
|
||||||
|
message: "Uncaught exception",
|
||||||
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
db.connect(err => {
|
||||||
|
if (err) throw err;
|
||||||
|
cache.connect(err => {
|
||||||
|
if (err) throw err;
|
||||||
|
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+105
-7
@@ -1,9 +1,107 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||||
var path = require('path');
|
import * as path from "path";
|
||||||
var fs = require('fs');
|
import * as fs from "fs";
|
||||||
|
|
||||||
var dir = path.resolve(path.dirname(fs.realpathSync(__filename)), '..');
|
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
|
||||||
process.chdir(dir);
|
|
||||||
|
|
||||||
var cluster = require(path.resolve(dir, 'lib/cluster'));
|
import * as config from "../lib/config";
|
||||||
cluster.start('nbi');
|
import * as logger from "../lib/logger";
|
||||||
|
import * as cluster from "../lib/cluster";
|
||||||
|
import * as server from "../lib/server";
|
||||||
|
import { listener } from "../lib/nbi";
|
||||||
|
import * as db from "../lib/db";
|
||||||
|
import * as extensions from "../lib/extensions";
|
||||||
|
import * as cache from "../lib/cache";
|
||||||
|
import { version as VERSION } from "../package.json";
|
||||||
|
|
||||||
|
logger.init("nbi", VERSION);
|
||||||
|
|
||||||
|
const SERVICE_ADDRESS = config.get("NBI_INTERFACE");
|
||||||
|
const SERVICE_PORT = config.get("NBI_PORT");
|
||||||
|
|
||||||
|
function exitWorkerGracefully(): void {
|
||||||
|
db.disconnect();
|
||||||
|
cache.disconnect();
|
||||||
|
extensions.killAll();
|
||||||
|
cluster.worker.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWorkerUngracefully(): void {
|
||||||
|
extensions.killAll(() => {
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cluster.worker) {
|
||||||
|
const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES");
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
message: `genieacs-nbi starting`,
|
||||||
|
pid: process.pid,
|
||||||
|
version: VERSION
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGINT, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGTERM, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let ssl;
|
||||||
|
if (config.get("NBI_SSL")) {
|
||||||
|
ssl = {
|
||||||
|
key: path.resolve("" + config.get("CONFIG_DIR"), "nbi.key"),
|
||||||
|
cert: path.resolve("" + config.get("CONFIG_DIR"), "nbi.crt"),
|
||||||
|
ca: path.resolve("" + config.get("CONFIG_DIR"), "nbi.ca-bundle")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("uncaughtException", err => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
|
logger.error({
|
||||||
|
message: "Uncaught exception",
|
||||||
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
db.connect(err => {
|
||||||
|
if (err) throw err;
|
||||||
|
cache.connect(err => {
|
||||||
|
if (err) throw err;
|
||||||
|
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
|
||||||
|
|
||||||
|
import * as config from "../lib/config";
|
||||||
|
import * as logger from "../lib/logger";
|
||||||
|
import * as cluster from "../lib/cluster";
|
||||||
|
import * as server from "../lib/server";
|
||||||
|
import { listener } from "../lib/ui";
|
||||||
|
import * as db from "../lib/ui/db";
|
||||||
|
import * as db2 from "../lib/db";
|
||||||
|
import * as cache from "../lib/cache";
|
||||||
|
import { version as VERSION } from "../package.json";
|
||||||
|
|
||||||
|
logger.init("ui", VERSION);
|
||||||
|
|
||||||
|
const SERVICE_ADDRESS = config.get("UI_INTERFACE");
|
||||||
|
const SERVICE_PORT = config.get("UI_PORT");
|
||||||
|
|
||||||
|
function exitWorkerGracefully(): void {
|
||||||
|
db.disconnect();
|
||||||
|
db2.disconnect();
|
||||||
|
cache.disconnect();
|
||||||
|
cluster.worker.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWorkerUngracefully(): void {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cluster.worker) {
|
||||||
|
const WORKER_COUNT = config.get("UI_WORKER_PROCESSES");
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
message: `genieacs-ui starting`,
|
||||||
|
pid: process.pid,
|
||||||
|
version: VERSION
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGINT, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGTERM, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let ssl;
|
||||||
|
if (config.get("UI_SSL")) {
|
||||||
|
ssl = {
|
||||||
|
key: path.resolve("" + config.get("CONFIG_DIR"), "ui.key"),
|
||||||
|
cert: path.resolve("" + config.get("CONFIG_DIR"), "ui.crt"),
|
||||||
|
ca: path.resolve("" + config.get("CONFIG_DIR"), "ui.ca-bundle")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let stopping = false;
|
||||||
|
|
||||||
|
process.on("uncaughtException", err => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
|
logger.error({
|
||||||
|
message: "Uncaught exception",
|
||||||
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
stopping = true;
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
stopping = true;
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
stopping = true;
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
const _listener = (req, res): void => {
|
||||||
|
if (stopping) res.setHeader("Connection", "close");
|
||||||
|
listener(req, res);
|
||||||
|
};
|
||||||
|
|
||||||
|
db2.connect(err => {
|
||||||
|
if (err) throw err;
|
||||||
|
cache.connect(err => {
|
||||||
|
if (err) throw err;
|
||||||
|
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+284
@@ -0,0 +1,284 @@
|
|||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import { rollup, WarningHandler } from "rollup";
|
||||||
|
import rollupReplace from "rollup-plugin-replace";
|
||||||
|
import rollupJson from "rollup-plugin-json";
|
||||||
|
import typescript from "rollup-plugin-typescript";
|
||||||
|
import { terser } from "rollup-plugin-terser";
|
||||||
|
import webpack from "webpack";
|
||||||
|
import postcss from "postcss";
|
||||||
|
import postcssImport from "postcss-import";
|
||||||
|
import postcssCssNext from "postcss-cssnext";
|
||||||
|
import cssnano from "cssnano";
|
||||||
|
|
||||||
|
const MODE = process.env["NODE_ENV"] || "production";
|
||||||
|
|
||||||
|
const BUILD_METADATA = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.split(".")[0]
|
||||||
|
.replace(/[^0-9]/g, "");
|
||||||
|
|
||||||
|
const INPUT_DIR = path.resolve(__dirname, "..");
|
||||||
|
const OUTPUT_DIR = path.resolve(__dirname, "../dist");
|
||||||
|
|
||||||
|
const externals = [
|
||||||
|
"path",
|
||||||
|
"fs",
|
||||||
|
"cluster",
|
||||||
|
"os",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"zlib",
|
||||||
|
"crypto",
|
||||||
|
"mongodb",
|
||||||
|
"libxmljs",
|
||||||
|
"vm",
|
||||||
|
"later",
|
||||||
|
"parsimmon",
|
||||||
|
"seedrandom",
|
||||||
|
"querystring",
|
||||||
|
"child_process",
|
||||||
|
"dgram",
|
||||||
|
"url",
|
||||||
|
"koa",
|
||||||
|
"koa-router",
|
||||||
|
"koa-compress",
|
||||||
|
"koa-bodyparser",
|
||||||
|
"koa-jwt",
|
||||||
|
"koa-static",
|
||||||
|
"jsonwebtoken",
|
||||||
|
"stream",
|
||||||
|
"mithril",
|
||||||
|
"parsimmon",
|
||||||
|
"codemirror",
|
||||||
|
"codemirror/mode/javascript/javascript",
|
||||||
|
"codemirror/mode/yaml/yaml"
|
||||||
|
];
|
||||||
|
|
||||||
|
function rmDirSync(dirPath): void {
|
||||||
|
if (!fs.existsSync(dirPath)) return;
|
||||||
|
const files = fs.readdirSync(dirPath);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = `${dirPath}/${file}`;
|
||||||
|
if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath);
|
||||||
|
else rmDirSync(filePath);
|
||||||
|
}
|
||||||
|
fs.rmdirSync(dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init(): Promise<void> {
|
||||||
|
// Delete any old output directory
|
||||||
|
rmDirSync(OUTPUT_DIR);
|
||||||
|
|
||||||
|
// Create output directory layout
|
||||||
|
fs.mkdirSync(OUTPUT_DIR);
|
||||||
|
fs.mkdirSync(OUTPUT_DIR + "/bin");
|
||||||
|
fs.mkdirSync(OUTPUT_DIR + "/config");
|
||||||
|
fs.mkdirSync(OUTPUT_DIR + "/debug");
|
||||||
|
fs.mkdirSync(OUTPUT_DIR + "/public");
|
||||||
|
fs.mkdirSync(OUTPUT_DIR + "/tools");
|
||||||
|
|
||||||
|
// Create package.json
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString()
|
||||||
|
);
|
||||||
|
delete packageJson["devDependencies"];
|
||||||
|
packageJson["scripts"] = {
|
||||||
|
install: packageJson["scripts"].install,
|
||||||
|
configure: packageJson["scripts"].configure
|
||||||
|
};
|
||||||
|
packageJson["version"] = `${packageJson["version"]}+${BUILD_METADATA}`;
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(OUTPUT_DIR, "package.json"),
|
||||||
|
JSON.stringify(packageJson, null, 4)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyStatic(): Promise<void> {
|
||||||
|
const files = [
|
||||||
|
"LICENSE",
|
||||||
|
"README.md",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"npm-shrinkwrap.json",
|
||||||
|
"config/config-sample.json",
|
||||||
|
"config/ext-sample.js",
|
||||||
|
"public/logo.svg",
|
||||||
|
"public/favicon.png"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.resolve(INPUT_DIR, file),
|
||||||
|
path.resolve(OUTPUT_DIR, file)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateCss(): Promise<void> {
|
||||||
|
const cssInPath = path.resolve(INPUT_DIR, "ui/css/app.css");
|
||||||
|
const cssOutPath = path.resolve(OUTPUT_DIR, "public/app.css");
|
||||||
|
const cssIn = fs.readFileSync(cssInPath);
|
||||||
|
const cssOut = await postcss([
|
||||||
|
postcssImport,
|
||||||
|
postcssCssNext({ warnForDuplicates: false }),
|
||||||
|
cssnano
|
||||||
|
]).process(cssIn, { from: cssInPath, to: cssOutPath });
|
||||||
|
fs.writeFileSync(cssOutPath, cssOut.css);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateToolsJs(): Promise<void> {
|
||||||
|
for (const bin of ["configure-ui", "dump-data-model"]) {
|
||||||
|
const inputFile = path.resolve(INPUT_DIR, `tools/${bin}`);
|
||||||
|
const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`);
|
||||||
|
const bundle = await rollup({
|
||||||
|
input: inputFile,
|
||||||
|
external: externals,
|
||||||
|
acorn: {
|
||||||
|
allowHashBang: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
rollupReplace({
|
||||||
|
delimiters: ["", ""],
|
||||||
|
"#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": ""
|
||||||
|
}),
|
||||||
|
typescript({
|
||||||
|
tsconfig: "./tsconfig.json",
|
||||||
|
include: [`tools/${bin}`, "lib/**/*.ts"]
|
||||||
|
}),
|
||||||
|
MODE === "production" ? terser() : null
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
await bundle.write({
|
||||||
|
format: "cjs",
|
||||||
|
preferConst: true,
|
||||||
|
banner: "#!/usr/bin/env node",
|
||||||
|
file: outputFile
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as executable
|
||||||
|
const mode = fs.statSync(outputFile).mode;
|
||||||
|
fs.chmodSync(outputFile, mode | 73);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateBackendJs(): Promise<void> {
|
||||||
|
for (const bin of [
|
||||||
|
"genieacs-cwmp",
|
||||||
|
"genieacs-ext",
|
||||||
|
"genieacs-nbi",
|
||||||
|
"genieacs-fs",
|
||||||
|
"genieacs-ui"
|
||||||
|
]) {
|
||||||
|
const inputFile = path.resolve(INPUT_DIR, `bin/${bin}`);
|
||||||
|
const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`);
|
||||||
|
const bundle = await rollup({
|
||||||
|
input: inputFile,
|
||||||
|
external: externals,
|
||||||
|
acorn: {
|
||||||
|
allowHashBang: true
|
||||||
|
},
|
||||||
|
treeshake: {
|
||||||
|
propertyReadSideEffects: false,
|
||||||
|
pureExternalModules: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
rollupReplace({
|
||||||
|
delimiters: ["", ""],
|
||||||
|
"#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": ""
|
||||||
|
}),
|
||||||
|
rollupJson({ preferConst: true }),
|
||||||
|
{
|
||||||
|
resolveId: (importee, importer) => {
|
||||||
|
if (importee.endsWith("/package.json")) {
|
||||||
|
const p = path.resolve(path.dirname(importer), importee);
|
||||||
|
if (p === path.resolve(INPUT_DIR, "package.json"))
|
||||||
|
return path.resolve(OUTPUT_DIR, "package.json");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typescript({
|
||||||
|
tsconfig: "./tsconfig.json",
|
||||||
|
include: [`bin/${bin}`, "lib/**/*.ts"]
|
||||||
|
}),
|
||||||
|
MODE === "production" ? terser() : null
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
await bundle.write({
|
||||||
|
format: "cjs",
|
||||||
|
preferConst: true,
|
||||||
|
banner: "#!/usr/bin/env node",
|
||||||
|
file: outputFile
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as executable
|
||||||
|
const mode = fs.statSync(outputFile).mode;
|
||||||
|
fs.chmodSync(outputFile, mode | 73);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateFrontendJs(): Promise<void> {
|
||||||
|
const inputFile = path.resolve(INPUT_DIR, "ui/app.ts");
|
||||||
|
const outputFile = path.resolve(OUTPUT_DIR, "public/app.js");
|
||||||
|
|
||||||
|
const bundle = await rollup({
|
||||||
|
input: inputFile,
|
||||||
|
external: externals,
|
||||||
|
plugins: [
|
||||||
|
rollupJson({ preferConst: true }),
|
||||||
|
typescript({ tsconfig: "./tsconfig.json" })
|
||||||
|
],
|
||||||
|
inlineDynamicImports: true,
|
||||||
|
treeshake: {
|
||||||
|
propertyReadSideEffects: false,
|
||||||
|
pureExternalModules: true
|
||||||
|
},
|
||||||
|
onwarn: ((warning, warn) => {
|
||||||
|
// Ignore circular dependency warnings
|
||||||
|
if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning);
|
||||||
|
}) as WarningHandler
|
||||||
|
});
|
||||||
|
|
||||||
|
await bundle.write({
|
||||||
|
preferConst: true,
|
||||||
|
format: "esm",
|
||||||
|
file: outputFile
|
||||||
|
});
|
||||||
|
|
||||||
|
const webpackConf = {
|
||||||
|
mode: MODE,
|
||||||
|
entry: outputFile,
|
||||||
|
resolve: {
|
||||||
|
aliasFields: ["module"]
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(OUTPUT_DIR, "public"),
|
||||||
|
filename: "app.js"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = await promisify(webpack)(webpackConf);
|
||||||
|
process.stdout.write(stats.toString({ colors: true }) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
.then(() => {
|
||||||
|
Promise.all([
|
||||||
|
copyStatic(),
|
||||||
|
generateCss(),
|
||||||
|
generateToolsJs(),
|
||||||
|
generateBackendJs(),
|
||||||
|
generateFrontendJs()
|
||||||
|
])
|
||||||
|
.then(() => {})
|
||||||
|
.catch(err => {
|
||||||
|
process.stderr.write(err.stack + "\n");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
process.stderr.write(err.stack + "\n");
|
||||||
|
});
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function connectionRequest(deviceId, url, username, password, callback) {
|
|
||||||
return callback(username || deviceId, password || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.connectionRequest = connectionRequest;
|
|
||||||
@@ -8,5 +8,8 @@
|
|||||||
"FS_INTERFACE" : "0.0.0.0",
|
"FS_INTERFACE" : "0.0.0.0",
|
||||||
"FS_PORT" : 7567,
|
"FS_PORT" : 7567,
|
||||||
"FS_HOSTNAME" : "acs.example.com",
|
"FS_HOSTNAME" : "acs.example.com",
|
||||||
|
"UI_INTERFACE" : "0.0.0.0",
|
||||||
|
"UI_PORT" : 3000,
|
||||||
|
"UI_JWT_SECRET": "",
|
||||||
"DEBUG" : false
|
"DEBUG" : false
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
@@ -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
@@ -16,11 +16,10 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* 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();
|
authHeader = authHeader.trim();
|
||||||
const method = authHeader.split(" ", 1)[0];
|
const method = authHeader.split(" ", 1)[0];
|
||||||
const res = { method: method };
|
const res = { method: method };
|
||||||
@@ -53,11 +52,18 @@ function parseAuthHeader(authHeader) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function basic(username, password) {
|
export function basic(username, password): string {
|
||||||
return "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
|
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 cnonce = "0a4f113b";
|
||||||
const nc = "00000001";
|
const nc = "00000001";
|
||||||
let qop;
|
let qop;
|
||||||
@@ -68,7 +74,7 @@ function digest(username, password, uri, httpMethod, body, authHeader) {
|
|||||||
else qop = authHeader.qop;
|
else qop = authHeader.qop;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ha1 = crypto.createHash("md5");
|
const ha1 = createHash("md5");
|
||||||
ha1
|
ha1
|
||||||
.update(username)
|
.update(username)
|
||||||
.update(":")
|
.update(":")
|
||||||
@@ -76,19 +82,19 @@ function digest(username, password, uri, httpMethod, body, authHeader) {
|
|||||||
.update(":")
|
.update(":")
|
||||||
.update(password);
|
.update(password);
|
||||||
// TODO support "MD5-sess" algorithm directive
|
// 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
|
ha2
|
||||||
.update(httpMethod)
|
.update(httpMethod)
|
||||||
.update(":")
|
.update(":")
|
||||||
.update(uri);
|
.update(uri);
|
||||||
if (qop === "auth-int") ha2.update(":").update(body);
|
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
|
hash
|
||||||
.update(ha1)
|
.update(ha1d)
|
||||||
.update(":")
|
.update(":")
|
||||||
.update(authHeader.nonce);
|
.update(authHeader.nonce);
|
||||||
if (qop) {
|
if (qop) {
|
||||||
@@ -100,7 +106,7 @@ function digest(username, password, uri, httpMethod, body, authHeader) {
|
|||||||
.update(":")
|
.update(":")
|
||||||
.update(qop);
|
.update(qop);
|
||||||
}
|
}
|
||||||
hash.update(":").update(ha2);
|
hash.update(":").update(ha2d);
|
||||||
|
|
||||||
let authString = `Digest username="${username}"`;
|
let authString = `Digest username="${username}"`;
|
||||||
authString += `,realm="${authHeader.realm}"`;
|
authString += `,realm="${authHeader.realm}"`;
|
||||||
@@ -114,6 +120,20 @@ function digest(username, password, uri, httpMethod, body, authHeader) {
|
|||||||
return authString;
|
return authString;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.parseAuthHeader = parseAuthHeader;
|
export function generateSalt(length): Promise<string> {
|
||||||
exports.basic = basic;
|
return new Promise((resolve, reject) => {
|
||||||
exports.digest = digest;
|
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
@@ -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
@@ -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
@@ -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;
|
|
||||||
@@ -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
@@ -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;
|
|
||||||
@@ -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, "\\$&");
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -16,19 +16,24 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
class PathSet {
|
import Path from "./path";
|
||||||
constructor() {
|
|
||||||
|
export default class PathSet {
|
||||||
|
private lengthIndex: Set<Path>[];
|
||||||
|
private fragmentIndex: Map<string, Set<Path>>[];
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
this.lengthIndex = [];
|
this.lengthIndex = [];
|
||||||
this.fragmentIndex = [];
|
this.fragmentIndex = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get depth() {
|
public get depth(): number {
|
||||||
return this.lengthIndex.length;
|
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);
|
const p = this.get(path);
|
||||||
|
|
||||||
if (p) return p;
|
if (p) return p;
|
||||||
@@ -41,43 +46,36 @@ class PathSet {
|
|||||||
const lengthIndex = this.lengthIndex[path.length];
|
const lengthIndex = this.lengthIndex[path.length];
|
||||||
lengthIndex.add(path);
|
lengthIndex.add(path);
|
||||||
|
|
||||||
let wildcard = 0;
|
|
||||||
for (let i = 0; i < path.length; ++i) {
|
for (let i = 0; i < path.length; ++i) {
|
||||||
const fragment = path[i];
|
const fragment = path.segments[i] as string;
|
||||||
if (fragment === "*") wildcard = wildcard | (1 << i);
|
|
||||||
|
|
||||||
const fragmentIndex = this.fragmentIndex[i];
|
const fragmentIndex = this.fragmentIndex[i];
|
||||||
|
|
||||||
let fragmentIndexSet = fragmentIndex.get(fragment);
|
let fragmentIndexSet = fragmentIndex.get(fragment);
|
||||||
if (!fragmentIndexSet) {
|
if (!fragmentIndexSet) {
|
||||||
fragmentIndexSet = new Set();
|
fragmentIndexSet = new Set<Path>();
|
||||||
fragmentIndex.set(fragment, fragmentIndexSet);
|
fragmentIndex.set(fragment, fragmentIndexSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
fragmentIndexSet.add(path);
|
fragmentIndexSet.add(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.wildcard == null) {
|
|
||||||
path.alias = 0;
|
|
||||||
path.wildcard = wildcard;
|
|
||||||
Object.freeze(path);
|
|
||||||
}
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(pattern) {
|
public get(path: Path): Path {
|
||||||
const lengthIndex = this.lengthIndex[pattern.length];
|
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 || !lengthIndex.size) return null;
|
||||||
|
|
||||||
if (lengthIndex.has(pattern)) return pattern;
|
if (lengthIndex.has(path)) return path;
|
||||||
|
|
||||||
const sets = [lengthIndex];
|
const sets = [lengthIndex];
|
||||||
|
|
||||||
for (let i = 0; i < pattern.length; ++i) {
|
for (let i = 0; i < path.length; ++i) {
|
||||||
const fragmentIndex = this.fragmentIndex[i];
|
const fragmentIndex = this.fragmentIndex[i];
|
||||||
if (!fragmentIndex || !fragmentIndex.size) return null;
|
if (!fragmentIndex || !fragmentIndex.size) return null;
|
||||||
|
|
||||||
const fragment = pattern[i];
|
const fragment = path.segments[i] as string;
|
||||||
|
|
||||||
const fragmentIndexSet = fragmentIndex.get(fragment);
|
const fragmentIndexSet = fragmentIndex.get(fragment);
|
||||||
if (!fragmentIndexSet) return null;
|
if (!fragmentIndexSet) return null;
|
||||||
@@ -88,34 +86,40 @@ class PathSet {
|
|||||||
sets.sort((a, b) => a.size - b.size);
|
sets.sort((a, b) => a.size - b.size);
|
||||||
|
|
||||||
const smallestSet = sets.shift();
|
const smallestSet = sets.shift();
|
||||||
for (let path of smallestSet) {
|
for (let p of smallestSet) {
|
||||||
for (const s of sets) {
|
for (const s of sets) {
|
||||||
if (!s.has(path)) {
|
if (!s.has(p)) {
|
||||||
path = null;
|
p = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path) return path;
|
if (p) return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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 res = [];
|
||||||
const groups = [[]];
|
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 (this.lengthIndex[i]) groups[0].push(this.lengthIndex[i]);
|
||||||
|
|
||||||
if (!groups[0].length) return res;
|
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];
|
const fragmentIndex = this.fragmentIndex[i];
|
||||||
if (!fragmentIndex) return res;
|
if (!fragmentIndex) return res;
|
||||||
|
|
||||||
const fragment = pattern[i];
|
const fragment = path.segments[i] as string;
|
||||||
|
|
||||||
if (fragment === "*" && subset) continue;
|
if (fragment === "*" && subset) continue;
|
||||||
|
|
||||||
@@ -140,26 +144,24 @@ class PathSet {
|
|||||||
|
|
||||||
const smallestGroup = groups.shift();
|
const smallestGroup = groups.shift();
|
||||||
for (const set of smallestGroup) {
|
for (const set of smallestGroup) {
|
||||||
for (let path of set) {
|
for (let p of set) {
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
let found = false;
|
let found = false;
|
||||||
for (const s of group) {
|
for (const s of group) {
|
||||||
if (s.has(path)) {
|
if (s.has(p)) {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
path = null;
|
p = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (path) res.push(path);
|
if (p) res.push(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = PathSet;
|
|
||||||
@@ -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();
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function pass(): number {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* 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 = {
|
const options = {
|
||||||
CONFIG_DIR: { type: "path", default: "config" },
|
CONFIG_DIR: { type: "path", default: "config" },
|
||||||
@@ -26,9 +26,6 @@ const options = {
|
|||||||
type: "string",
|
type: "string",
|
||||||
default: "mongodb://127.0.0.1/genieacs"
|
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_WORKER_PROCESSES: { type: "int", default: 0 },
|
||||||
CWMP_PORT: { type: "int", default: 7547 },
|
CWMP_PORT: { type: "int", default: 7547 },
|
||||||
@@ -52,6 +49,14 @@ const options = {
|
|||||||
FS_LOG_FILE: { type: "path", default: "" },
|
FS_LOG_FILE: { type: "path", default: "" },
|
||||||
FS_ACCESS_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 },
|
UDP_CONNECTION_REQUEST_PORT: { type: "int", default: 0 },
|
||||||
|
|
||||||
DOWNLOAD_TIMEOUT: { type: "int", default: 3600 },
|
DOWNLOAD_TIMEOUT: { type: "int", default: 3600 },
|
||||||
@@ -84,9 +89,9 @@ const options = {
|
|||||||
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }
|
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;
|
if (allConfig[name] != null) return true;
|
||||||
|
|
||||||
// For compatibility with v1.0
|
// 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);
|
if (name === "FS_IP" || name === "fs-ip") setConfig("FS_HOSTNAME", value);
|
||||||
|
|
||||||
function cast(val, type) {
|
function cast(val, type): string | number | boolean {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "int":
|
case "int":
|
||||||
return Number(val);
|
return Number(val);
|
||||||
@@ -127,7 +132,7 @@ function setConfig(name, value, commandLineArgument) {
|
|||||||
return String(val);
|
return String(val);
|
||||||
case "path":
|
case "path":
|
||||||
if (!val) return "";
|
if (!val) return "";
|
||||||
return path.resolve(val);
|
return resolve(val);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -175,7 +180,11 @@ for (const [k, v] of Object.entries(process.env))
|
|||||||
setConfig("CONFIG_DIR", options["CONFIG_DIR"]["default"]);
|
setConfig("CONFIG_DIR", options["CONFIG_DIR"]["default"]);
|
||||||
|
|
||||||
// Configuration file
|
// 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)) {
|
for (const [k, v] of Object.entries(configFile)) {
|
||||||
if (!setConfig(k, v))
|
if (!setConfig(k, v))
|
||||||
// Pass as environment variable to be accessable by extensions
|
// 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))
|
for (const [k, v] of Object.entries(options))
|
||||||
if (v["default"] != null) setConfig(k, v["default"]);
|
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];
|
if (!deviceId) return allConfig[optionName];
|
||||||
|
|
||||||
optionName = `${optionName}-${deviceId}`;
|
optionName = `${optionName}-${deviceId}`;
|
||||||
@@ -214,22 +226,12 @@ function get(optionName, deviceId) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefault(optionName) {
|
export function getDefault(optionName): string | number | boolean {
|
||||||
const option = options[optionName];
|
const option = options[optionName];
|
||||||
if (!option) return null;
|
if (!option) return null;
|
||||||
|
|
||||||
let val = option["default"];
|
let val = option["default"];
|
||||||
if (val && option.type === "path") val = path.resolve(val);
|
if (val && option.type === "path") val = resolve(val);
|
||||||
|
|
||||||
return 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;
|
|
||||||
@@ -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
@@ -16,25 +16,36 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const zlib = require("zlib");
|
import * as zlib from "zlib";
|
||||||
const crypto = require("crypto");
|
import * as crypto from "crypto";
|
||||||
const fs = require("fs");
|
import * as fs from "fs";
|
||||||
const config = require("./config");
|
import { Socket } from "net";
|
||||||
const common = require("./common");
|
import * as config from "./config";
|
||||||
const soap = require("./soap");
|
import * as common from "./common";
|
||||||
const session = require("./session");
|
import * as soap from "./soap";
|
||||||
const query = require("./query");
|
import * as session from "./session";
|
||||||
const device = require("./device");
|
import { evaluate, extractParams } from "./common/expression";
|
||||||
const cache = require("./cache");
|
import * as device from "./device";
|
||||||
const localCache = require("./local-cache");
|
import * as cache from "./cache";
|
||||||
const db = require("./db");
|
import * as localCache from "./local-cache";
|
||||||
const logger = require("./logger");
|
import * as db from "./db";
|
||||||
const scheduling = require("./scheduling");
|
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_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 = {
|
const stats = {
|
||||||
concurrentRequests: 0,
|
concurrentRequests: 0,
|
||||||
@@ -43,7 +54,7 @@ const stats = {
|
|||||||
initiatedSessions: 0
|
initiatedSessions: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
function throwError(err, httpResponse) {
|
function throwError(err, httpResponse?): never {
|
||||||
if (httpResponse) {
|
if (httpResponse) {
|
||||||
currentSessions.delete(httpResponse.connection);
|
currentSessions.delete(httpResponse.connection);
|
||||||
httpResponse.writeHead(500, { Connection: "close" });
|
httpResponse.writeHead(500, { Connection: "close" });
|
||||||
@@ -53,11 +64,15 @@ function throwError(err, httpResponse) {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeResponse(sessionContext, res, close) {
|
function writeResponse(
|
||||||
|
sessionContext: SessionContext,
|
||||||
|
res,
|
||||||
|
close = false
|
||||||
|
): void {
|
||||||
// Close connection after last request in session
|
// Close connection after last request in session
|
||||||
if (close) res.headers["Connection"] = "close";
|
if (close) res.headers["Connection"] = "close";
|
||||||
|
|
||||||
if (config.get("DEBUG", sessionContext.deviceId)) {
|
if (sessionContext.debug) {
|
||||||
const dump =
|
const dump =
|
||||||
`# RESPONSE ${new Date(Date.now())}\n` +
|
`# RESPONSE ${new Date(Date.now())}\n` +
|
||||||
JSON.stringify(res.headers) +
|
JSON.stringify(res.headers) +
|
||||||
@@ -69,7 +84,7 @@ function writeResponse(sessionContext, res, close) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function finish() {
|
function finish(): void {
|
||||||
stats.concurrentRequests -= 1;
|
stats.concurrentRequests -= 1;
|
||||||
if (sessionContext.httpRequest.connection.destroyed) {
|
if (sessionContext.httpRequest.connection.destroyed) {
|
||||||
logger.accessError({
|
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) {
|
if (!provisions) {
|
||||||
provisions = sessionContext.provisions;
|
provisions = sessionContext.provisions;
|
||||||
channels = sessionContext.channels;
|
channels = sessionContext.channels;
|
||||||
@@ -141,7 +168,10 @@ function recordFault(sessionContext, fault, provisions, channels) {
|
|||||||
const provs = sessionContext.faults[channel]
|
const provs = sessionContext.faults[channel]
|
||||||
? sessionContext.faults[channel].provisions
|
? 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_")) {
|
if (channel.startsWith("task_")) {
|
||||||
const taskId = channel.slice(5);
|
const taskId = channel.slice(5);
|
||||||
for (const t of sessionContext.tasks)
|
for (const t of sessionContext.tasks)
|
||||||
@@ -185,7 +215,7 @@ function recordFault(sessionContext, fault, provisions, channels) {
|
|||||||
session.clearProvisions(sessionContext);
|
session.clearProvisions(sessionContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inform(sessionContext, rpc) {
|
function inform(sessionContext, rpc): void {
|
||||||
session.inform(sessionContext, rpc.cpeRequest, (err, acsResponse) => {
|
session.inform(sessionContext, rpc.cpeRequest, (err, acsResponse) => {
|
||||||
if (err) return void throwError(err, sessionContext.httpResponse);
|
if (err) return void throwError(err, sessionContext.httpResponse);
|
||||||
|
|
||||||
@@ -195,7 +225,12 @@ function inform(sessionContext, rpc) {
|
|||||||
cwmpVersion: sessionContext.cwmpVersion
|
cwmpVersion: sessionContext.cwmpVersion
|
||||||
});
|
});
|
||||||
|
|
||||||
const cookiesPath = config.get("COOKIES_PATH", sessionContext.deviceId);
|
const cookiesPath = localCache.getConfig(
|
||||||
|
sessionContext.cacheSnapshot,
|
||||||
|
"cwmp.cookiesPath",
|
||||||
|
sessionContext.configContext
|
||||||
|
);
|
||||||
|
|
||||||
if (cookiesPath) {
|
if (cookiesPath) {
|
||||||
res.headers["Set-Cookie"] = `session=${
|
res.headers["Set-Cookie"] = `session=${
|
||||||
sessionContext.sessionId
|
sessionContext.sessionId
|
||||||
@@ -208,7 +243,7 @@ function inform(sessionContext, rpc) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function transferComplete(sessionContext, rpc) {
|
function transferComplete(sessionContext, rpc): void {
|
||||||
session.transferComplete(
|
session.transferComplete(
|
||||||
sessionContext,
|
sessionContext,
|
||||||
rpc.cpeRequest,
|
rpc.cpeRequest,
|
||||||
@@ -244,8 +279,8 @@ function transferComplete(sessionContext, rpc) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append providions and remove duplicates
|
// Append provisions and remove duplicates
|
||||||
function appendProvisions(original, toAppend) {
|
function appendProvisions(original, toAppend): boolean {
|
||||||
let modified = false;
|
let modified = false;
|
||||||
const stringified = new WeakMap();
|
const stringified = new WeakMap();
|
||||||
|
|
||||||
@@ -276,7 +311,7 @@ function appendProvisions(original, toAppend) {
|
|||||||
return modified;
|
return modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPresets(sessionContext) {
|
function applyPresets(sessionContext: SessionContext): void {
|
||||||
const deviceData = sessionContext.deviceData;
|
const deviceData = sessionContext.deviceData;
|
||||||
const presets = localCache.getPresets(sessionContext.cacheSnapshot);
|
const presets = localCache.getPresets(sessionContext.cacheSnapshot);
|
||||||
|
|
||||||
@@ -284,7 +319,11 @@ function applyPresets(sessionContext) {
|
|||||||
const blackList = {};
|
const blackList = {};
|
||||||
let whiteList = null;
|
let whiteList = null;
|
||||||
let whiteListProvisions = 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) {
|
if (sessionContext.faults) {
|
||||||
for (const [channel, fault] of Object.entries(sessionContext.faults)) {
|
for (const [channel, fault] of Object.entries(sessionContext.faults)) {
|
||||||
@@ -309,13 +348,13 @@ function applyPresets(sessionContext) {
|
|||||||
deviceData.attributes.revision = 1;
|
deviceData.attributes.revision = 1;
|
||||||
|
|
||||||
const deviceEvents = {};
|
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);
|
const attrs = deviceData.attributes.get(p);
|
||||||
if (attrs && attrs.value && attrs.value[1][0] >= sessionContext.timestamp)
|
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 = [];
|
const filteredPresets = [];
|
||||||
|
|
||||||
for (const preset of presets) {
|
for (const preset of presets) {
|
||||||
@@ -345,11 +384,8 @@ function applyPresets(sessionContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filteredPresets.push(preset);
|
filteredPresets.push(preset);
|
||||||
for (const k of Object.keys(preset.precondition)) {
|
for (const k of extractParams(preset.precondition))
|
||||||
sessionContext.channels[preset.channel] = 0;
|
parameters[k] = Path.parse(k);
|
||||||
const p = k.split(/([^a-zA-Z0-9\-_.].*)/, 1)[0];
|
|
||||||
parameters[p] = common.parsePath(p);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const declarations = Object.values(parameters).map(v => ({
|
const declarations = Object.values(parameters).map(v => ({
|
||||||
@@ -380,7 +416,7 @@ function applyPresets(sessionContext) {
|
|||||||
const parameterValues = {};
|
const parameterValues = {};
|
||||||
for (const [k, v] of Object.entries(parameters)) {
|
for (const [k, v] of Object.entries(parameters)) {
|
||||||
const unpacked = device.unpack(deviceData, v);
|
const unpacked = device.unpack(deviceData, v);
|
||||||
if (!unpacked[0]) continue;
|
if (!unpacked.length) continue;
|
||||||
const attrs = deviceData.attributes.get(unpacked[0]);
|
const attrs = deviceData.attributes.get(unpacked[0]);
|
||||||
if (attrs && attrs.value && attrs.value[1])
|
if (attrs && attrs.value && attrs.value[1])
|
||||||
parameterValues[k] = attrs.value[1][0];
|
parameterValues[k] = attrs.value[1][0];
|
||||||
@@ -390,8 +426,9 @@ function applyPresets(sessionContext) {
|
|||||||
session.addProvisions(sessionContext, whiteList, whiteListProvisions);
|
session.addProvisions(sessionContext, whiteList, whiteListProvisions);
|
||||||
|
|
||||||
const appendProvisionsToFaults = {};
|
const appendProvisionsToFaults = {};
|
||||||
|
const now = Date.now();
|
||||||
for (const p of filteredPresets) {
|
for (const p of filteredPresets) {
|
||||||
if (query.testFilter(parameterValues, p.precondition)) {
|
if (evaluate(p.precondition, parameterValues, now)) {
|
||||||
if (blackList[p.channel] === 2) {
|
if (blackList[p.channel] === 2) {
|
||||||
appendProvisionsToFaults[p.channel] = (
|
appendProvisionsToFaults[p.channel] = (
|
||||||
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) => {
|
session.rpcRequest(sessionContext, null, (err, fault, id, acsRequest) => {
|
||||||
if (err) return void throwError(err, sessionContext.httpResponse);
|
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 saveCache = sessionContext.cacheUntil != null;
|
||||||
let counter = 3;
|
let counter = 3;
|
||||||
|
|
||||||
@@ -705,24 +742,29 @@ function endSession(sessionContext, callback) {
|
|||||||
if ((counter -= 2) === 1) callback(null, sessionContext.new);
|
if ((counter -= 2) === 1) callback(null, sessionContext.new);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendAcsRequest(sessionContext, id, acsRequest) {
|
function sendAcsRequest(
|
||||||
|
sessionContext: SessionContext,
|
||||||
|
id?: string,
|
||||||
|
acsRequest?: AcsRequest
|
||||||
|
): void {
|
||||||
if (!acsRequest)
|
if (!acsRequest)
|
||||||
return void writeResponse(sessionContext, soap.response(null), true);
|
return void writeResponse(sessionContext, soap.response(null), true);
|
||||||
|
|
||||||
if (acsRequest.name === "Download") {
|
if (acsRequest.name === "Download") {
|
||||||
acsRequest.fileSize = 0;
|
const downloadRequest = acsRequest as SetAcsRequest;
|
||||||
if (!acsRequest.url) {
|
downloadRequest.fileSize = 0;
|
||||||
|
if (!downloadRequest.url) {
|
||||||
const FS_PORT = config.get("FS_PORT");
|
const FS_PORT = config.get("FS_PORT");
|
||||||
const FS_HOSTNAME = config.get("FS_HOSTNAME");
|
const FS_HOSTNAME = config.get("FS_HOSTNAME");
|
||||||
const FS_SSL = config.get("FS_SSL");
|
const FS_SSL = config.get("FS_SSL");
|
||||||
acsRequest.url = FS_SSL ? "https://" : "http://";
|
downloadRequest.url = FS_SSL ? "https://" : "http://";
|
||||||
acsRequest.url += FS_HOSTNAME;
|
downloadRequest.url += FS_HOSTNAME;
|
||||||
if (FS_PORT !== 80) acsRequest.url += ":" + FS_PORT;
|
if (FS_PORT !== 80) downloadRequest.url += ":" + FS_PORT;
|
||||||
acsRequest.url += "/" + encodeURI(acsRequest.fileName);
|
downloadRequest.url += "/" + encodeURI(downloadRequest.fileName);
|
||||||
|
|
||||||
const files = localCache.getFiles(sessionContext.cacheSnapshot);
|
const files = localCache.getFiles(sessionContext.cacheSnapshot);
|
||||||
if (files[acsRequest.fileName])
|
if (files[downloadRequest.fileName])
|
||||||
acsRequest.fileSize = files[acsRequest.fileName].length;
|
downloadRequest.fileSize = files[downloadRequest.fileName].length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,7 +784,11 @@ function sendAcsRequest(sessionContext, id, acsRequest) {
|
|||||||
writeResponse(sessionContext, res);
|
writeResponse(sessionContext, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSession(connection, sessionId, callback) {
|
function getSession(
|
||||||
|
connection,
|
||||||
|
sessionId,
|
||||||
|
callback: (err?: Error, sessionContext?: SessionContext) => void
|
||||||
|
): void {
|
||||||
const sessionContext = currentSessions.get(connection);
|
const sessionContext = currentSessions.get(connection);
|
||||||
if (sessionContext) {
|
if (sessionContext) {
|
||||||
currentSessions.delete(connection);
|
currentSessions.delete(connection);
|
||||||
@@ -759,10 +805,8 @@ function getSession(connection, sessionId, callback) {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSessions = new WeakMap();
|
|
||||||
|
|
||||||
// When socket closes, store active sessions in cache
|
// 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
|
// The property remoteAddress may be undefined after the connection is
|
||||||
// closed, unless we read it at least once (caching?)
|
// closed, unless we read it at least once (caching?)
|
||||||
socket.remoteAddress;
|
socket.remoteAddress;
|
||||||
@@ -831,7 +875,29 @@ setInterval(() => {
|
|||||||
stats.initiatedSessions = 0;
|
stats.initiatedSessions = 0;
|
||||||
}, 10000).unref();
|
}, 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) => {
|
cache.get(`${deviceId}_tasks_faults_operations`, (err, res) => {
|
||||||
if (err) return void callback(err);
|
if (err) return void callback(err);
|
||||||
|
|
||||||
@@ -841,7 +907,8 @@ function getDueTasksAndFaultsAndOperations(deviceId, timestamp, callback) {
|
|||||||
null,
|
null,
|
||||||
res.tasks || [],
|
res.tasks || [],
|
||||||
res.faults || {},
|
res.faults || {},
|
||||||
res.operations || {}
|
res.operations || {},
|
||||||
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,8 +957,12 @@ function cacheDueTasksAndFaultsAndOperations(
|
|||||||
operations,
|
operations,
|
||||||
cacheUntil,
|
cacheUntil,
|
||||||
callback
|
callback
|
||||||
) {
|
): void {
|
||||||
const v = {};
|
const v = {
|
||||||
|
tasks: null,
|
||||||
|
faults: null,
|
||||||
|
operations: null
|
||||||
|
};
|
||||||
if (tasks.length) v.tasks = tasks;
|
if (tasks.length) v.tasks = tasks;
|
||||||
if (Object.keys(faults).length) v.faults = faults;
|
if (Object.keys(faults).length) v.faults = faults;
|
||||||
if (Object.keys(operations).length) v.operations = operations;
|
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) {
|
||||||
if (rpc.cpeRequest.name === "Inform") {
|
if (rpc.cpeRequest.name === "Inform") {
|
||||||
logger.accessInfo({
|
logger.accessInfo({
|
||||||
@@ -987,7 +1058,7 @@ function processRequest(sessionContext, rpc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function listener(httpRequest, httpResponse) {
|
export function listener(httpRequest, httpResponse): void {
|
||||||
stats.totalRequests += 1;
|
stats.totalRequests += 1;
|
||||||
|
|
||||||
if (httpRequest.method !== "POST") {
|
if (httpRequest.method !== "POST") {
|
||||||
@@ -1055,14 +1126,14 @@ function listener(httpRequest, httpResponse) {
|
|||||||
offset += chunk.length;
|
offset += chunk.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsedRpc(sessionContext, rpc, parseWarnings) {
|
function parsedRpc(sessionContext, rpc, parseWarnings): void {
|
||||||
for (const w of parseWarnings) {
|
for (const w of parseWarnings) {
|
||||||
w.sessionContext = sessionContext;
|
w.sessionContext = sessionContext;
|
||||||
w.rpc = rpc;
|
w.rpc = rpc;
|
||||||
logger.accessWarn(w);
|
logger.accessWarn(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.get("DEBUG", sessionContext.deviceId)) {
|
if (sessionContext.debug) {
|
||||||
const dump =
|
const dump =
|
||||||
`# REQUEST ${new Date(Date.now())}\n` +
|
`# REQUEST ${new Date(Date.now())}\n` +
|
||||||
JSON.stringify(httpRequest.headers) +
|
JSON.stringify(httpRequest.headers) +
|
||||||
@@ -1134,7 +1205,7 @@ function listener(httpRequest, httpResponse) {
|
|||||||
if (sessionContext) {
|
if (sessionContext) {
|
||||||
if (
|
if (
|
||||||
(rpc.cpeRequest && rpc.cpeRequest.name === "Inform") ||
|
(rpc.cpeRequest && rpc.cpeRequest.name === "Inform") ||
|
||||||
!sessionContext.rpcRequest ^ !(rpc.cpeResponse || rpc.cpeFault)
|
!sessionContext.rpcRequest !== !(rpc.cpeResponse || rpc.cpeFault)
|
||||||
) {
|
) {
|
||||||
logger.accessError({
|
logger.accessError({
|
||||||
message: "Bad session state",
|
message: "Bad session state",
|
||||||
@@ -1165,48 +1236,72 @@ function listener(httpRequest, httpResponse) {
|
|||||||
stats.initiatedSessions += 1;
|
stats.initiatedSessions += 1;
|
||||||
const deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId);
|
const deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId);
|
||||||
|
|
||||||
session.init(
|
localCache.getCurrentSnapshot((err, cacheSnapshot) => {
|
||||||
deviceId,
|
if (err) return void throwError(err, httpResponse);
|
||||||
rpc.cwmpVersion,
|
|
||||||
rpc.sessionTimeout || config.get("SESSION_TIMEOUT", deviceId),
|
|
||||||
(err, _sessionContext) => {
|
|
||||||
if (err) return void throwError(err, httpResponse);
|
|
||||||
|
|
||||||
_sessionContext.httpRequest = httpRequest;
|
const configContext = {
|
||||||
_sessionContext.httpResponse = httpResponse;
|
id: deviceId,
|
||||||
_sessionContext.sessionId = crypto.randomBytes(8).toString("hex");
|
serialNumber: rpc.cpeRequest.deviceId["SerialNumber"],
|
||||||
httpRequest.connection.setTimeout(_sessionContext.timeout * 1000);
|
productClass: rpc.cpeRequest.deviceId["ProductClass"],
|
||||||
|
oui: rpc.cpeRequest.deviceId["OUI"]
|
||||||
|
};
|
||||||
|
|
||||||
getDueTasksAndFaultsAndOperations(
|
const sessionTimeout =
|
||||||
deviceId,
|
rpc.sessionTimeout ||
|
||||||
_sessionContext.timestamp,
|
localCache.getConfig(
|
||||||
(err, dueTasks, faults, operations, cacheUntil) => {
|
cacheSnapshot,
|
||||||
if (err) return void throwError(err, httpResponse);
|
"cwmp.sessionTimeout",
|
||||||
|
configContext
|
||||||
_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);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
);
|
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
@@ -16,13 +16,15 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const mongodb = require("mongodb");
|
import { MongoClient, ObjectID } from "mongodb";
|
||||||
const config = require("./config");
|
import { get } from "./config";
|
||||||
const common = require("./common");
|
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,
|
devicesCollection,
|
||||||
presetsCollection,
|
presetsCollection,
|
||||||
objectsCollection,
|
objectsCollection,
|
||||||
@@ -30,47 +32,49 @@ let tasksCollection,
|
|||||||
virtualParametersCollection,
|
virtualParametersCollection,
|
||||||
faultsCollection,
|
faultsCollection,
|
||||||
filesCollection,
|
filesCollection,
|
||||||
operationsCollection;
|
operationsCollection,
|
||||||
|
permissionsCollection,
|
||||||
|
usersCollection,
|
||||||
|
configCollection;
|
||||||
|
|
||||||
function connect(callback) {
|
export let client;
|
||||||
mongodb.MongoClient.connect(
|
|
||||||
config.get("MONGODB_CONNECTION_URL"),
|
export function connect(callback): void {
|
||||||
|
MongoClient.connect(
|
||||||
|
"" + get("MONGODB_CONNECTION_URL"),
|
||||||
{ useNewUrlParser: true },
|
{ useNewUrlParser: true },
|
||||||
(err, client) => {
|
(err, _client) => {
|
||||||
if (err) return void callback(err);
|
if (err) return void callback(err);
|
||||||
exports.client = client;
|
client = _client;
|
||||||
const db = client.db();
|
const db = client.db();
|
||||||
|
|
||||||
exports.tasksCollection = tasksCollection = db.collection("tasks");
|
tasksCollection = db.collection("tasks");
|
||||||
tasksCollection.createIndex({ device: 1, timestamp: 1 });
|
tasksCollection.createIndex({ device: 1, timestamp: 1 });
|
||||||
|
|
||||||
exports.devicesCollection = devicesCollection = db.collection("devices");
|
devicesCollection = db.collection("devices");
|
||||||
exports.presetsCollection = presetsCollection = db.collection("presets");
|
presetsCollection = db.collection("presets");
|
||||||
exports.objectsCollection = objectsCollection = db.collection("objects");
|
objectsCollection = db.collection("objects");
|
||||||
exports.filesCollection = filesCollection = db.collection("fs.files");
|
filesCollection = db.collection("fs.files");
|
||||||
exports.provisionsCollection = provisionsCollection = db.collection(
|
provisionsCollection = db.collection("provisions");
|
||||||
"provisions"
|
virtualParametersCollection = db.collection("virtualParameters");
|
||||||
);
|
faultsCollection = db.collection("faults");
|
||||||
exports.virtualParametersCollection = virtualParametersCollection = db.collection(
|
operationsCollection = db.collection("operations");
|
||||||
"virtualParameters"
|
permissionsCollection = db.collection("permissions");
|
||||||
);
|
usersCollection = db.collection("users");
|
||||||
exports.faultsCollection = faultsCollection = db.collection("faults");
|
configCollection = db.collection("config");
|
||||||
exports.operationsCollection = operationsCollection = db.collection(
|
|
||||||
"operations"
|
|
||||||
);
|
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnect() {
|
export function disconnect(): void {
|
||||||
if (exports.client) exports.client.close();
|
if (client) client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimize projection by removing overlaps
|
// Optimize projection by removing overlaps
|
||||||
// This can modify the object
|
// This can modify the object
|
||||||
function optimizeProjection(obj) {
|
function optimizeProjection(obj: {}): {} {
|
||||||
if (obj[""]) return { "": obj[""] };
|
if (obj[""]) return { "": obj[""] };
|
||||||
|
|
||||||
const keys = Object.keys(obj).sort();
|
const keys = Object.keys(obj).sort();
|
||||||
@@ -89,55 +93,66 @@ function optimizeProjection(obj) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchDevice(id, timestamp, patterns, callback) {
|
export function fetchDevice(
|
||||||
const MAX_DEPTH = config.get("MAX_DEPTH", id);
|
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 = {};
|
let projection = {};
|
||||||
const projectionTree = {};
|
const projectionTree = {};
|
||||||
function func(path, pats, projTree) {
|
function func(
|
||||||
const children = {};
|
path: string,
|
||||||
|
pathLength: number,
|
||||||
|
pats: [Path, number][],
|
||||||
|
projTree
|
||||||
|
): void {
|
||||||
|
const children: { [fragment: string]: [Path, number][] } = {};
|
||||||
for (const pat of pats) {
|
for (const pat of pats) {
|
||||||
const fragment = pat[0][path.length] || "*";
|
const fragment = (pat[0].segments[pathLength] as string) || "*";
|
||||||
if (fragment === "*") {
|
if (fragment === "*") {
|
||||||
if (pat[1] << path.length) projection[path.join(".")] = 1;
|
if (pat[1] << pathLength) projection[path.slice(0, -1)] = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!projTree[fragment]) projTree[fragment] = {};
|
if (!projTree[fragment]) projTree[fragment] = {};
|
||||||
|
|
||||||
if (pat[1] & (1 << path.length)) {
|
if (pat[1] & (1 << pathLength)) {
|
||||||
projection[path.concat("_timestamp").join(".")] = 1;
|
projection[path + "_timestamp"] = 1;
|
||||||
projection[path.concat("_object").join(".")] = 1;
|
projection[path + "_object"] = 1;
|
||||||
projection[path.concat("_instance").join(".")] = 1;
|
projection[path + "_instance"] = 1;
|
||||||
projection[path.concat([fragment, "_timestamp"]).join(".")] = 1;
|
projection[path + fragment + "._timestamp"] = 1;
|
||||||
projection[path.concat([fragment, "_value"]).join(".")] = 1;
|
projection[path + fragment + "._value"] = 1;
|
||||||
projection[path.concat([fragment, "_type"]).join(".")] = 1;
|
projection[path + fragment + "._type"] = 1;
|
||||||
projection[path.concat([fragment, "_object"]).join(".")] = 1;
|
projection[path + fragment + "._object"] = 1;
|
||||||
projection[path.concat([fragment, "_instance"]).join(".")] = 1;
|
projection[path + fragment + "._instance"] = 1;
|
||||||
projection[path.concat([fragment, "_writable"]).join(".")] = 1;
|
projection[path + fragment + "._writable"] = 1;
|
||||||
projection[path.concat([fragment, "_orig"]).join(".")] = 1;
|
projection[path + fragment + "._orig"] = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pat[1] >> (path.length + 1)) {
|
if (pat[1] >> (pathLength + 1)) {
|
||||||
if (!children[fragment]) children[fragment] = [];
|
if (!children[fragment]) children[fragment] = [];
|
||||||
children[fragment].push(pat);
|
children[fragment].push(pat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(children))
|
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["DeviceID"];
|
||||||
delete projectionTree["Events"];
|
delete projectionTree["Events"];
|
||||||
delete projectionTree["Tags"];
|
delete projectionTree["Tags"];
|
||||||
|
|
||||||
const res = [];
|
const res: [Path, number, Attributes?][] = [];
|
||||||
const loaded = [];
|
const loaded: [Path, number][] = [];
|
||||||
|
|
||||||
projection = optimizeProjection(projection);
|
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.startsWith("Events.")) {
|
||||||
if (k === "" || k === "Events" || k === "Events._writable") {
|
if (k === "" || k === "Events" || k === "Events._writable") {
|
||||||
res.push([
|
res.push([
|
||||||
["Events"],
|
Path.parse("Events"),
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1], writable: [timestamp, 0] }
|
{ 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") {
|
if (k === "Events") {
|
||||||
projection["_lastInform"] = 1;
|
projection["_lastInform"] = 1;
|
||||||
projection["_lastBoot"] = 1;
|
projection["_lastBoot"] = 1;
|
||||||
projection["_lastBootstrap"] = 1;
|
projection["_lastBootstrap"] = 1;
|
||||||
projection["_registered"] = 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") {
|
} else if (k === "Events.Inform._writable" || k === "Events.Inform") {
|
||||||
projection["_lastInform"] = 1;
|
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") {
|
} else if (k === "Events.1_BOOT._writable" || k === "Events.1_BOOT") {
|
||||||
projection["_lastBoot"] = 1;
|
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 (
|
} else if (
|
||||||
k === "Events.0_BOOTSTRAP._writable" ||
|
k === "Events.0_BOOTSTRAP._writable" ||
|
||||||
k === "Events.0_BOOTSTRAP"
|
k === "Events.0_BOOTSTRAP"
|
||||||
) {
|
) {
|
||||||
projection["_lastBootstrap"] = 1;
|
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 (
|
} else if (
|
||||||
k === "Events.Registered._writable" ||
|
k === "Events.Registered._writable" ||
|
||||||
k === "Events.Registered"
|
k === "Events.Registered"
|
||||||
) {
|
) {
|
||||||
projection["_registered"] = 1;
|
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") {
|
} 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];
|
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.startsWith("DeviceID.")) {
|
||||||
if (k === "" || k === "DeviceID" || k === "DeviceID._writable") {
|
if (k === "" || k === "DeviceID" || k === "DeviceID._writable") {
|
||||||
res.push([
|
res.push([
|
||||||
["DeviceID"],
|
Path.parse("DeviceID"),
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1], writable: [timestamp, 0] }
|
{ 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") {
|
if (k === "DeviceID") {
|
||||||
projection["_id"] = 1;
|
projection["_id"] = 1;
|
||||||
projection["_deviceId"] = 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") {
|
} else if (k === "DeviceID.ID._writable" || k === "DeviceID.ID") {
|
||||||
projection["_id"] = 1;
|
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 (
|
} else if (
|
||||||
k === "DeviceID.Manufacturer._writable" ||
|
k === "DeviceID.Manufacturer._writable" ||
|
||||||
k === "DeviceID.DeManufacturer"
|
k === "DeviceID.DeManufacturer"
|
||||||
) {
|
) {
|
||||||
projection["_deviceId._Manufacturer"] = 1;
|
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 (
|
} else if (
|
||||||
k === "DeviceID.ProductClass._writable" ||
|
k === "DeviceID.ProductClass._writable" ||
|
||||||
k === "DeviceID.ProductClass"
|
k === "DeviceID.ProductClass"
|
||||||
) {
|
) {
|
||||||
projection["_deviceId._ProductClass"] = 1;
|
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") {
|
} else if (k === "DeviceID.OUI._writable" || k === "DeviceID.OUI") {
|
||||||
projection["_deviceId._OUI"] = 1;
|
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 (
|
} else if (
|
||||||
k === "DeviceID.SerialNumber._writable" ||
|
k === "DeviceID.SerialNumber._writable" ||
|
||||||
k === "DeviceID.SerialNumber"
|
k === "DeviceID.SerialNumber"
|
||||||
) {
|
) {
|
||||||
projection["_deviceId._SerialNumber"] = 1;
|
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") {
|
} 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];
|
if (k !== "") delete projection[k];
|
||||||
}
|
}
|
||||||
@@ -227,49 +267,54 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
if (k === "Tags" || k.startsWith("Tags.")) {
|
if (k === "Tags" || k.startsWith("Tags.")) {
|
||||||
if (!projection["_tags"]) {
|
if (!projection["_tags"]) {
|
||||||
projection["_tags"] = 1;
|
projection["_tags"] = 1;
|
||||||
loaded.push([["Tags"], (1 << MAX_DEPTH) - 1]);
|
loaded.push([Path.parse("Tags"), (1 << MAX_DEPTH) - 1]);
|
||||||
}
|
}
|
||||||
delete projection[k];
|
delete projection[k];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {};
|
let options;
|
||||||
if (!projection[""]) {
|
if (!projection[""]) {
|
||||||
if (!Object.keys(projection).length) options.projection = { _id: 1 };
|
if (!Object.keys(projection).length) options = { projection: { _id: 1 } };
|
||||||
else options.projection = projection;
|
else options = { projection: projection };
|
||||||
}
|
}
|
||||||
|
|
||||||
devicesCollection.findOne({ _id: id }, options, (err, device) => {
|
devicesCollection.findOne({ _id: id }, options, (err, device) => {
|
||||||
if (err || !device) return void callback(err);
|
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;
|
let thisFetched = false;
|
||||||
if (descendantsFetched) {
|
if (descendantsFetched) {
|
||||||
thisFetched = true;
|
thisFetched = true;
|
||||||
} else if (projection[path.join(".")]) {
|
} else if (projection[path.slice(0, -1)]) {
|
||||||
descendantsFetched = true;
|
descendantsFetched = true;
|
||||||
if (
|
if (
|
||||||
path.length &&
|
pathLength &&
|
||||||
projection[
|
projection[
|
||||||
path
|
path.slice(0, path.lastIndexOf(".", path.length - 2) + 1) +
|
||||||
.slice(0, -1)
|
"_timestamp"
|
||||||
.concat("_timestamp")
|
|
||||||
.join(".")
|
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
thisFetched = true;
|
thisFetched = true;
|
||||||
loaded.push([
|
loaded.push([
|
||||||
path,
|
Path.parse(path.slice(0, -1)),
|
||||||
((1 << (path.length - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1)
|
((1 << (pathLength - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1)
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
loaded.push([
|
loaded.push([
|
||||||
path,
|
Path.parse(path.slice(0, -1)),
|
||||||
((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)
|
((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else if (projection[path.concat("_writable").join(".")]) {
|
} else if (projection[path + "_writable"]) {
|
||||||
loaded.push([path, 1 << (path.length - 1)]);
|
loaded.push([Path.parse(path.slice(0, -1)), 1 << (pathLength - 1)]);
|
||||||
thisFetched = true;
|
thisFetched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +324,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
if (obj["_instance"] && obj["_object"] == null) obj["_object"] = true;
|
if (obj["_instance"] && obj["_object"] == null) obj["_object"] = true;
|
||||||
|
|
||||||
if (thisFetched) {
|
if (thisFetched) {
|
||||||
const attrs = {};
|
const attrs: Attributes = {};
|
||||||
let t = obj["_timestamp"] || 1;
|
let t = obj["_timestamp"] || 1;
|
||||||
if (ts > t) t = ts;
|
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];
|
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)) {
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
@@ -303,7 +348,8 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
obj["_object"] = true;
|
obj["_object"] = true;
|
||||||
storeParams(
|
storeParams(
|
||||||
v,
|
v,
|
||||||
path.concat(k),
|
path + k + ".",
|
||||||
|
pathLength + 1,
|
||||||
obj["_timestamp"],
|
obj["_timestamp"],
|
||||||
descendantsFetched,
|
descendantsFetched,
|
||||||
projTree ? projTree[k] : null
|
projTree ? projTree[k] : null
|
||||||
@@ -315,14 +361,14 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
if (!descendantsFetched) {
|
if (!descendantsFetched) {
|
||||||
if (projTree) {
|
if (projTree) {
|
||||||
for (const k of Object.keys(projTree)) {
|
for (const k of Object.keys(projTree)) {
|
||||||
const p = path.concat(k);
|
const p = Path.parse(path + k);
|
||||||
loaded.push([p, ((1 << path.length) - 1) ^ ((1 << MAX_DEPTH) - 1)]);
|
loaded.push([p, ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1)]);
|
||||||
if ((obj["_object"] || !path.length) && obj["_timestamp"])
|
if ((obj["_object"] || !pathLength) && obj["_timestamp"])
|
||||||
res.push([p, obj["_timestamp"]]);
|
res.push([p, obj["_timestamp"]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ((obj["_object"] || !path.length) && obj["_timestamp"]) {
|
} else if ((obj["_object"] || !pathLength) && obj["_timestamp"]) {
|
||||||
res.push([path.concat("*"), obj["_timestamp"]]);
|
res.push([Path.parse(path + "*"), obj["_timestamp"]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +376,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
switch (k) {
|
switch (k) {
|
||||||
case "_lastInform":
|
case "_lastInform":
|
||||||
res.push([
|
res.push([
|
||||||
["Events", "Inform"],
|
Path.parse("Events.Inform"),
|
||||||
+v,
|
+v,
|
||||||
{
|
{
|
||||||
object: [+v, 0],
|
object: [+v, 0],
|
||||||
@@ -342,7 +388,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
break;
|
break;
|
||||||
case "_lastBoot":
|
case "_lastBoot":
|
||||||
res.push([
|
res.push([
|
||||||
["Events", "1_BOOT"],
|
Path.parse("Events.1_BOOT"),
|
||||||
+v,
|
+v,
|
||||||
{
|
{
|
||||||
object: [+v, 0],
|
object: [+v, 0],
|
||||||
@@ -354,7 +400,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
break;
|
break;
|
||||||
case "_lastBootstrap":
|
case "_lastBootstrap":
|
||||||
res.push([
|
res.push([
|
||||||
["Events", "0_BOOTSTRAP"],
|
Path.parse("Events.0_BOOTSTRAP"),
|
||||||
+v,
|
+v,
|
||||||
{
|
{
|
||||||
object: [+v, 0],
|
object: [+v, 0],
|
||||||
@@ -367,7 +413,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
case "_registered":
|
case "_registered":
|
||||||
// Use current timestamp for registered event attribute timestamps
|
// Use current timestamp for registered event attribute timestamps
|
||||||
res.push([
|
res.push([
|
||||||
["Events", "Registered"],
|
Path.parse("Events.Registered"),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -380,12 +426,12 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
case "_id":
|
case "_id":
|
||||||
if (projection[""] || projection["_id"]) {
|
if (projection[""] || projection["_id"]) {
|
||||||
res.push([
|
res.push([
|
||||||
["DeviceID", "ID"],
|
Path.parse("DeviceID.ID"),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
writable: [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];
|
delete device[k];
|
||||||
break;
|
break;
|
||||||
case "_tags":
|
case "_tags":
|
||||||
if (v.length) {
|
if ((v as string[]).length) {
|
||||||
res.push([
|
res.push([
|
||||||
["Tags"],
|
Path.parse("Tags"),
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1], writable: [timestamp, 0] }
|
{ 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, "_");
|
t = t.replace(/[^a-zA-Z0-9-]+/g, "_");
|
||||||
res.push([
|
res.push([
|
||||||
["Tags", t],
|
Path.parse("Tags." + t),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -418,7 +464,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
case "_deviceId":
|
case "_deviceId":
|
||||||
if (v["_Manufacturer"] != null) {
|
if (v["_Manufacturer"] != null) {
|
||||||
res.push([
|
res.push([
|
||||||
["DeviceID", "Manufacturer"],
|
Path.parse("DeviceID.Manufacturer"),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -430,7 +476,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
|
|
||||||
if (v["_OUI"] != null) {
|
if (v["_OUI"] != null) {
|
||||||
res.push([
|
res.push([
|
||||||
["DeviceID", "OUI"],
|
Path.parse("DeviceID.OUI"),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -442,7 +488,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
|
|
||||||
if (v["_ProductClass"] != null) {
|
if (v["_ProductClass"] != null) {
|
||||||
res.push([
|
res.push([
|
||||||
["DeviceID", "ProductClass"],
|
Path.parse("DeviceID.ProductClass"),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
@@ -454,7 +500,7 @@ function fetchDevice(id, timestamp, patterns, callback) {
|
|||||||
|
|
||||||
if (v["_SerialNumber"] != null) {
|
if (v["_SerialNumber"] != null) {
|
||||||
res.push([
|
res.push([
|
||||||
["DeviceID", "SerialNumber"],
|
Path.parse("DeviceID.SerialNumber"),
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
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);
|
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: {} };
|
const update = { $set: {}, $unset: {}, $addToSet: {}, $pull: {} };
|
||||||
|
|
||||||
for (const diff of deviceData.timestamps.diff()) {
|
for (const diff of deviceData.timestamps.diff()) {
|
||||||
if (diff[0].wildcard !== 1 << (diff[0].length - 1)) continue;
|
if (diff[0].wildcard !== 1 << (diff[0].length - 1)) continue;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
diff[0][0] === "Events" ||
|
diff[0].segments[0] === "Events" ||
|
||||||
diff[0][0] === "DeviceID" ||
|
diff[0].segments[0] === "DeviceID" ||
|
||||||
diff[0][0] === "Tags"
|
diff[0].segments[0] === "Tags"
|
||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
const parent = deviceData.paths.get(diff[0].slice(0, -1));
|
||||||
|
|
||||||
// Param timestamps may be greater than session timestamp to track revisions
|
// Param timestamps may be greater than session timestamp to track revisions
|
||||||
if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp;
|
if (diff[2] > sessionTimestamp) diff[2] = sessionTimestamp;
|
||||||
|
|
||||||
if (diff[2] == null && diff[1] != null) {
|
if (diff[2] == null && diff[1] != null) {
|
||||||
update["$unset"][
|
update["$unset"][
|
||||||
diff[0]
|
parent.length ? parent.toString() + "._timestamp" : "_timestamp"
|
||||||
.slice(0, -1)
|
|
||||||
.concat("_timestamp")
|
|
||||||
.join(".")
|
|
||||||
] = 1;
|
] = 1;
|
||||||
} else if (diff[2] !== diff[1]) {
|
} else {
|
||||||
const parent = deviceData.paths.get(diff[0].slice(0, -1));
|
|
||||||
if (parent && (!parent.length || deviceData.attributes.has(parent))) {
|
if (parent && (!parent.length || deviceData.attributes.has(parent))) {
|
||||||
update["$set"][
|
update["$set"][
|
||||||
diff[0]
|
parent.length ? parent.toString() + "._timestamp" : "_timestamp"
|
||||||
.slice(0, -1)
|
|
||||||
.concat("_timestamp")
|
|
||||||
.join(".")
|
|
||||||
] = new Date(diff[2]);
|
] = new Date(diff[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const diff of deviceData.attributes.diff()) {
|
for (const diff of deviceData.attributes.diff()) {
|
||||||
if (diff[1] === diff[2]) continue;
|
|
||||||
|
|
||||||
const path = diff[0];
|
const path = diff[0];
|
||||||
const value1 = (((diff[1] || {}).value || [])[1] || [])[0];
|
const value1 = (((diff[1] || {}).value || [])[1] || [])[0];
|
||||||
const value2 = (((diff[2] || {}).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 writable2 = ((diff[2] || {}).writable || [])[1];
|
||||||
const writable1 = ((diff[1] || {}).writable || [])[1];
|
const writable1 = ((diff[1] || {}).writable || [])[1];
|
||||||
|
|
||||||
switch (path[0]) {
|
switch (path.segments[0]) {
|
||||||
case "Events":
|
case "Events":
|
||||||
if (diff[0].length === 2 && value2 !== value1) {
|
if (path.length === 2 && value2 !== value1) {
|
||||||
if (!diff[2]) {
|
if (!diff[2]) {
|
||||||
switch (path[1]) {
|
switch (path.segments[1]) {
|
||||||
case "Inform":
|
case "Inform":
|
||||||
update["$unset"]["_lastInform"] = 1;
|
update["$unset"]["_lastInform"] = 1;
|
||||||
break;
|
break;
|
||||||
@@ -542,8 +587,8 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
|
|||||||
update["$unset"]["_registered"] = 1;
|
update["$unset"]["_registered"] = 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const t = new Date(diff[2].value[1][0]);
|
const t = new Date(diff[2].value[1][0] as number);
|
||||||
switch (path[1]) {
|
switch (path.segments[1]) {
|
||||||
case "Inform":
|
case "Inform":
|
||||||
update["$set"]["_lastInform"] = t;
|
update["$set"]["_lastInform"] = t;
|
||||||
break;
|
break;
|
||||||
@@ -563,7 +608,7 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
|
|||||||
case "DeviceID":
|
case "DeviceID":
|
||||||
if (value2 !== value1) {
|
if (value2 !== value1) {
|
||||||
const v = diff[2].value[1][0];
|
const v = diff[2].value[1][0];
|
||||||
switch (path[1]) {
|
switch (path.segments[1]) {
|
||||||
case "ID":
|
case "ID":
|
||||||
update["$set"]["_id"] = v;
|
update["$set"]["_id"] = v;
|
||||||
break;
|
break;
|
||||||
@@ -586,21 +631,21 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
|
|||||||
if (value2 != null) {
|
if (value2 != null) {
|
||||||
if (!update["$addToSet"]["_tags"])
|
if (!update["$addToSet"]["_tags"])
|
||||||
update["$addToSet"]["_tags"] = { $each: [] };
|
update["$addToSet"]["_tags"] = { $each: [] };
|
||||||
update["$addToSet"]["_tags"]["$each"].push(diff[0][1]);
|
update["$addToSet"]["_tags"]["$each"].push(path.segments[1]);
|
||||||
} else {
|
} else {
|
||||||
if (!update["$pull"]["_tags"]) {
|
if (!update["$pull"]["_tags"]) {
|
||||||
update["$pull"]["_tags"] = {
|
update["$pull"]["_tags"] = {
|
||||||
$in: []
|
$in: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
update["$pull"]["_tags"]["$in"].push(diff[0][1]);
|
update["$pull"]["_tags"]["$in"].push(path.segments[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (!diff[2]) {
|
if (!diff[2]) {
|
||||||
update["$unset"][diff[0].join(".")] = 1;
|
update["$unset"][path.toString()] = 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,35 +660,38 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
|
|||||||
if (value2 !== value1) {
|
if (value2 !== value1) {
|
||||||
if (
|
if (
|
||||||
valueType2 === "xsd:dateTime" &&
|
valueType2 === "xsd:dateTime" &&
|
||||||
Number.isInteger(value2)
|
Number.isInteger(value2 as number)
|
||||||
) {
|
) {
|
||||||
update["$set"][path.concat("_value").join(".")] = new Date(
|
update["$set"][path.toString() + "._value"] = new Date(
|
||||||
value2
|
value2 as number
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
update["$set"][path.concat("_value").join(".")] = value2;
|
update["$set"][path.toString() + "._value"] = value2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valueType2 !== valueType1)
|
if (valueType2 !== valueType1)
|
||||||
update["$set"][path.concat("_type").join(".")] = valueType2;
|
update["$set"][path.toString() + "._type"] = valueType2;
|
||||||
|
|
||||||
if (valueTimestamp2 !== valueTimestamp1) {
|
if (valueTimestamp2 !== valueTimestamp1) {
|
||||||
update["$set"][
|
update["$set"][path.toString() + "._timestamp"] = new Date(
|
||||||
path.concat("_timestamp").join(".")
|
valueTimestamp2
|
||||||
] = new Date(valueTimestamp2);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "object":
|
case "object":
|
||||||
if (!diff[1] || !diff[1].object || object2 !== object1)
|
if (!diff[1] || !diff[1].object || object2 !== object1) {
|
||||||
update["$set"][path.concat("_object").join(".")] = !!object2;
|
update["$set"][
|
||||||
|
path.length ? path.toString() + "._object" : "_object"
|
||||||
|
] = !!object2;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "writable":
|
case "writable":
|
||||||
if (!diff[1] || !diff[1].writable || writable2 !== writable1) {
|
if (!diff[1] || !diff[1].writable || writable2 !== writable1) {
|
||||||
update["$set"][
|
update["$set"][
|
||||||
path.concat("_writable").join(".")
|
path.length ? path.toString() + "._writable" : "_writable"
|
||||||
] = !!writable2;
|
] = !!writable2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -656,10 +704,11 @@ function saveDevice(deviceId, deviceData, isNew, sessionTimestamp, callback) {
|
|||||||
diff[1][attrName][1] != null &&
|
diff[1][attrName][1] != null &&
|
||||||
(!diff[2] || !diff[2][attrName] || diff[2][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") {
|
if (attrName === "value") {
|
||||||
update["$unset"][path.concat("_type").join(".")] = 1;
|
update["$unset"][p + "_type"] = 1;
|
||||||
update["$unset"][path.concat("_timestamp").join(".")] = 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
|
faultsCollection
|
||||||
.find({ _id: { $regex: `^${common.escapeRegExp(deviceId)}\\:` } })
|
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
|
||||||
.toArray((err, res) => {
|
.toArray((err, res) => {
|
||||||
if (err) return callback(err);
|
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}`;
|
const id = `${deviceId}:${channel}`;
|
||||||
fault = Object.assign({}, fault);
|
fault = Object.assign({}, fault);
|
||||||
fault._id = id;
|
fault._id = id;
|
||||||
@@ -753,11 +802,11 @@ function saveFault(deviceId, channel, fault, callback) {
|
|||||||
faultsCollection.replaceOne({ _id: id }, fault, { upsert: true }, 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);
|
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 cur = tasksCollection.find({ device: deviceId }).sort(["timestamp"]);
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
|
|
||||||
@@ -779,8 +828,8 @@ function getDueTasks(deviceId, timestamp, callback) {
|
|||||||
// For API compatibility
|
// For API compatibility
|
||||||
if (task.name === "download" && task.file) {
|
if (task.name === "download" && task.file) {
|
||||||
let q;
|
let q;
|
||||||
if (mongodb.ObjectID.isValid(task.file))
|
if (ObjectID.isValid(task.file))
|
||||||
q = { _id: { $in: [task.file, new mongodb.ObjectID(task.file)] } };
|
q = { _id: { $in: [task.file, new ObjectID(task.file)] } };
|
||||||
else q = { _id: task.file };
|
else q = { _id: task.file };
|
||||||
|
|
||||||
filesCollection.find(q).toArray((err, res) => {
|
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(
|
tasksCollection.deleteMany(
|
||||||
{ _id: { $in: taskIds.map(id => new mongodb.ObjectID(id)) } },
|
{ _id: { $in: taskIds.map(id => new ObjectID(id)) } },
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOperations(deviceId, callback) {
|
export function getOperations(deviceId, callback): void {
|
||||||
operationsCollection
|
operationsCollection
|
||||||
.find({ _id: { $regex: `^${common.escapeRegExp(deviceId)}\\:` } })
|
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
|
||||||
.toArray((err, res) => {
|
.toArray((err, res) => {
|
||||||
if (err) return void callback(err);
|
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}`;
|
const id = `${deviceId}:${commandKey}`;
|
||||||
operation = Object.assign({}, operation);
|
operation = Object.assign({}, operation);
|
||||||
operation._id = id;
|
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(
|
operationsCollection.deleteOne(
|
||||||
{ _id: `${deviceId}:${commandKey}` },
|
{ _id: `${deviceId}:${commandKey}` },
|
||||||
callback
|
callback
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPresets(callback) {
|
export function getPresets(callback): void {
|
||||||
presetsCollection.find().toArray(callback);
|
presetsCollection.find().toArray(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getObjects(callback) {
|
export function getObjects(callback): void {
|
||||||
objectsCollection.find().toArray(callback);
|
objectsCollection.find().toArray(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProvisions(callback) {
|
export function getProvisions(callback): void {
|
||||||
provisionsCollection.find().toArray(callback);
|
provisionsCollection.find().toArray(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVirtualParameters(callback) {
|
export function getVirtualParameters(callback): void {
|
||||||
virtualParametersCollection.find().toArray(callback);
|
virtualParametersCollection.find().toArray(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFiles(callback) {
|
export function getFiles(callback): void {
|
||||||
filesCollection.find().toArray(callback);
|
filesCollection.find().toArray(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.connect = connect;
|
export function getConfig(callback): void {
|
||||||
exports.disconnect = disconnect;
|
configCollection.find().toArray((err, res) => {
|
||||||
exports.fetchDevice = fetchDevice;
|
if (err) return void callback(err);
|
||||||
exports.saveDevice = saveDevice;
|
|
||||||
exports.getFaults = getFaults;
|
callback(
|
||||||
exports.saveFault = saveFault;
|
null,
|
||||||
exports.deleteFault = deleteFault;
|
res.map(c => ({
|
||||||
exports.clearTasks = clearTasks;
|
id: c["_id"],
|
||||||
exports.saveOperation = saveOperation;
|
value: parse(c["value"])
|
||||||
exports.deleteOperation = deleteOperation;
|
}))
|
||||||
exports.getPresets = getPresets;
|
);
|
||||||
exports.getObjects = getObjects;
|
});
|
||||||
exports.getProvisions = getProvisions;
|
}
|
||||||
exports.getVirtualParameters = getVirtualParameters;
|
|
||||||
exports.getFiles = getFiles;
|
export function getPermissions(
|
||||||
exports.getDueTasks = getDueTasks;
|
callback: (
|
||||||
exports.getOperations = getOperations;
|
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
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const config = require("./config");
|
import Path from "./common/path";
|
||||||
const common = require("./common");
|
import * as config from "./config";
|
||||||
const device = require("./device");
|
import * as device from "./device";
|
||||||
const scheduling = require("./scheduling");
|
import * as scheduling from "./scheduling";
|
||||||
|
|
||||||
const MAX_DEPTH = config.get("MAX_DEPTH");
|
const MAX_DEPTH = +config.get("MAX_DEPTH");
|
||||||
|
|
||||||
function refresh(sessionContext, provision, declarations) {
|
export function refresh(sessionContext, provision, declarations): boolean {
|
||||||
const path = common.parsePath(provision[1]).slice();
|
const segments = Path.parse(provision[1]).segments.slice();
|
||||||
const l = path.length;
|
const l = segments.length;
|
||||||
path.length = MAX_DEPTH;
|
segments.length = MAX_DEPTH;
|
||||||
path.fill("*", l);
|
segments.fill("*", l);
|
||||||
|
const path = Path.parse(segments.join("."));
|
||||||
const every = 1000 * (provision[2] || 1);
|
const every = 1000 * (provision[2] || 1);
|
||||||
const offset = scheduling.variance(sessionContext.deviceId, every);
|
const offset = scheduling.variance(sessionContext.deviceId, every);
|
||||||
const t = scheduling.interval(sessionContext.timestamp, every, offset);
|
const t = scheduling.interval(sessionContext.timestamp, every, offset);
|
||||||
@@ -48,10 +48,9 @@ function refresh(sessionContext, provision, declarations) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function value(sessionContext, provision, declarations) {
|
export function value(sessionContext, provision, declarations): boolean {
|
||||||
const path = common.parsePath(provision[1]);
|
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: path,
|
path: Path.parse(provision[1]),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
pathSet: null,
|
pathSet: null,
|
||||||
attrGet: { value: 1 },
|
attrGet: { value: 1 },
|
||||||
@@ -62,11 +61,9 @@ function value(sessionContext, provision, declarations) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tag(sessionContext, provision, declarations) {
|
export function tag(sessionContext, provision, declarations): boolean {
|
||||||
const path = ["Tags", provision[1]];
|
|
||||||
|
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: path,
|
path: Path.parse(`Tags.${provision[1]}`),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
pathSet: null,
|
pathSet: null,
|
||||||
attrGet: { value: 1 },
|
attrGet: { value: 1 },
|
||||||
@@ -77,9 +74,9 @@ function tag(sessionContext, provision, declarations) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reboot(sessionContext, provision, declarations) {
|
export function reboot(sessionContext, provision, declarations): boolean {
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: ["Reboot"],
|
path: Path.parse("Reboot"),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
pathSet: null,
|
pathSet: null,
|
||||||
attrGet: { value: 1 },
|
attrGet: { value: 1 },
|
||||||
@@ -90,9 +87,9 @@ function reboot(sessionContext, provision, declarations) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset(sessionContext, provision, declarations) {
|
export function reset(sessionContext, provision, declarations): boolean {
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: ["FactoryReset"],
|
path: Path.parse("FactoryReset"),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
pathSet: null,
|
pathSet: null,
|
||||||
attrGet: { value: 1 },
|
attrGet: { value: 1 },
|
||||||
@@ -103,18 +100,15 @@ function reset(sessionContext, provision, declarations) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function download(sessionContext, provision, declarations) {
|
export function download(sessionContext, provision, declarations): boolean {
|
||||||
const alias = [
|
const alias = [
|
||||||
["FileType"],
|
`FileType:${JSON.stringify(provision[1] || "")}`,
|
||||||
provision[1] || "",
|
`FileName:${JSON.stringify(provision[2] || "")}`,
|
||||||
["FileName"],
|
`TargetFileName:${JSON.stringify(provision[3] || "")}`
|
||||||
provision[2] || "",
|
].join(",");
|
||||||
["TargetFileName"],
|
|
||||||
provision[3] || ""
|
|
||||||
];
|
|
||||||
|
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: ["Downloads", alias],
|
path: Path.parse(`Downloads.[${alias}]`),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
pathSet: 1,
|
pathSet: 1,
|
||||||
attrGet: null,
|
attrGet: null,
|
||||||
@@ -123,7 +117,7 @@ function download(sessionContext, provision, declarations) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: ["Downloads", alias, "Download"],
|
path: Path.parse(`Downloads.[${alias}].Download`),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
pathSet: null,
|
pathSet: null,
|
||||||
attrGet: { value: 1 },
|
attrGet: { value: 1 },
|
||||||
@@ -134,18 +128,18 @@ function download(sessionContext, provision, declarations) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function instances(
|
export function instances(
|
||||||
sessionContext,
|
sessionContext,
|
||||||
provision,
|
provision,
|
||||||
declarations,
|
declarations,
|
||||||
startRevision,
|
startRevision,
|
||||||
endRevision
|
endRevision
|
||||||
) {
|
): boolean {
|
||||||
let count = Number(provision[2]);
|
let count = Number(provision[2]);
|
||||||
|
|
||||||
if (Number.isNaN(count)) return true;
|
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] === "-") {
|
if (provision[2][0] === "+" || provision[2][0] === "-") {
|
||||||
declarations.push({
|
declarations.push({
|
||||||
@@ -178,11 +172,3 @@ function instances(
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.refresh = refresh;
|
|
||||||
exports.value = value;
|
|
||||||
exports.tag = tag;
|
|
||||||
exports.reboot = reboot;
|
|
||||||
exports.reset = reset;
|
|
||||||
exports.download = download;
|
|
||||||
exports.instances = instances;
|
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* 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 = {
|
const CHANGE_FLAGS = {
|
||||||
object: 2,
|
object: 2,
|
||||||
@@ -24,7 +26,7 @@ const CHANGE_FLAGS = {
|
|||||||
value: 8
|
value: 8
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseBool(v) {
|
function parseBool(v): boolean {
|
||||||
v = "" + v;
|
v = "" + v;
|
||||||
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
|
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
|
||||||
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
|
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
|
||||||
@@ -32,14 +34,16 @@ function parseBool(v) {
|
|||||||
else return null;
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeParameterValue(parameterValue) {
|
export function sanitizeParameterValue(
|
||||||
|
parameterValue: [string | number | boolean, string]
|
||||||
|
): [string | number | boolean, string?] {
|
||||||
if (parameterValue[0] != null) {
|
if (parameterValue[0] != null) {
|
||||||
switch (parameterValue[1]) {
|
switch (parameterValue[1]) {
|
||||||
case "xsd:boolean":
|
case "xsd:boolean":
|
||||||
if (typeof parameterValue[0] !== "boolean") {
|
if (typeof parameterValue[0] !== "boolean") {
|
||||||
let v = parseBool(parameterValue[0]);
|
const b = parseBool(parameterValue[0]);
|
||||||
if (v == null) v = "" + parameterValue[0];
|
if (b == null) parameterValue[0] = "" + parameterValue[0];
|
||||||
parameterValue[0] = v;
|
else parameterValue[0] = b;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "xsd:string":
|
case "xsd:string":
|
||||||
@@ -52,20 +56,22 @@ function sanitizeParameterValue(parameterValue) {
|
|||||||
case "xsd:int":
|
case "xsd:int":
|
||||||
case "xsd:unsignedInt":
|
case "xsd:unsignedInt":
|
||||||
if (typeof parameterValue[0] !== "number") {
|
if (typeof parameterValue[0] !== "number") {
|
||||||
let v = parseInt(parameterValue[0]);
|
const i = parseInt(parameterValue[0] as string);
|
||||||
if (isNaN(v)) v = "" + parameterValue[0];
|
if (isNaN(i)) parameterValue[0] = "" + parameterValue[0];
|
||||||
parameterValue[0] = v;
|
else parameterValue[0] = i;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "xsd:dateTime":
|
case "xsd:dateTime":
|
||||||
if (typeof parameterValue[0] !== "number") {
|
if (typeof parameterValue[0] !== "number") {
|
||||||
// Don't use parseInt because it reads date string as a number
|
// Don't use parseInt because it reads date string as a number
|
||||||
let v = +parameterValue[0];
|
let i = +parameterValue[0];
|
||||||
if (isNaN(v)) {
|
if (isNaN(i)) {
|
||||||
v = Date.parse(parameterValue[0]);
|
i = Date.parse(parameterValue[0] as string);
|
||||||
if (isNaN(v)) v = "" + parameterValue[0];
|
if (isNaN(i)) parameterValue[0] = "" + parameterValue[0];
|
||||||
|
else parameterValue[0] = i;
|
||||||
|
} else {
|
||||||
|
parameterValue[0] = i;
|
||||||
}
|
}
|
||||||
parameterValue[0] = v;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -83,85 +89,79 @@ function sanitizeParameterValue(parameterValue) {
|
|||||||
return parameterValue;
|
return parameterValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAliasDeclarations(path, timestamp) {
|
export function getAliasDeclarations(
|
||||||
var decs = [];
|
path: Path,
|
||||||
|
timestamp: number,
|
||||||
function recursive(p, prefix) {
|
attrGet = null
|
||||||
const pattern = prefix.concat(p);
|
): Declaration[] {
|
||||||
|
const stripped = path.stripAlias();
|
||||||
for (let i = prefix.length; i < pattern.length; ++i) {
|
let decs: Declaration[] = [
|
||||||
if (Array.isArray(pattern[i])) {
|
{
|
||||||
const pat = pattern[i];
|
path: stripped,
|
||||||
pattern[i] = "*";
|
|
||||||
for (let j = 0; j < pat.length; j += 2)
|
|
||||||
recursive(pat[j], pattern.slice(0, i + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
decs.push({
|
|
||||||
path: pattern,
|
|
||||||
pathGet: timestamp,
|
pathGet: timestamp,
|
||||||
pathSet: null,
|
pathSet: null,
|
||||||
attrGet: prefix.length ? { value: timestamp } : null,
|
attrGet: attrGet,
|
||||||
attrSet: null,
|
attrSet: null,
|
||||||
defer: true
|
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;
|
return decs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unpack(deviceData, path, revision) {
|
export function unpack(
|
||||||
let allMatches = [];
|
deviceData: DeviceData,
|
||||||
|
path: Path,
|
||||||
|
revision?: number
|
||||||
|
): Path[] {
|
||||||
|
let allMatches = [] as Path[];
|
||||||
if (!path.alias) {
|
if (!path.alias) {
|
||||||
for (const p of deviceData.paths.find(path, false, true))
|
for (const p of deviceData.paths.find(path, false, true))
|
||||||
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
|
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
|
||||||
} else {
|
} else {
|
||||||
const wildcardPath = path.slice();
|
const wildcardPath = path.stripAlias();
|
||||||
wildcardPath.wildcard = path.wildcard;
|
|
||||||
for (let i = 0; i < wildcardPath.length; ++i) {
|
|
||||||
if (Array.isArray(wildcardPath[i])) {
|
|
||||||
wildcardPath[i] = "*";
|
|
||||||
wildcardPath.wildcard |= 1 << i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const p of deviceData.paths.find(wildcardPath, false, true))
|
for (const p of deviceData.paths.find(wildcardPath, false, true))
|
||||||
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
|
if (deviceData.attributes.has(p, revision)) allMatches.push(p);
|
||||||
|
|
||||||
for (let i = path.length - 1; i >= 0; --i) {
|
for (let i = path.length - 1; i >= 0; --i) {
|
||||||
if (Array.isArray(path[i])) {
|
if (path.alias & (i << i)) {
|
||||||
for (let j = 0; j < path[i].length; j += 2) {
|
for (const [param, val] of path.segments[i] as [Path, string][]) {
|
||||||
const p = wildcardPath.slice(0, i + 1).concat(path[i][j]);
|
const p = wildcardPath.slice(0, i + 1).concat(param);
|
||||||
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);
|
|
||||||
const unpacked = unpack(deviceData, p, revision);
|
const unpacked = unpack(deviceData, p, revision);
|
||||||
const filtered = [];
|
const filtered: Path[] = [];
|
||||||
for (const up of unpacked) {
|
for (const up of unpacked) {
|
||||||
const attributes = deviceData.attributes.get(up, revision);
|
const attributes = deviceData.attributes.get(up, revision);
|
||||||
if (
|
if (
|
||||||
attributes &&
|
attributes &&
|
||||||
attributes.value &&
|
attributes.value &&
|
||||||
attributes.value[1] &&
|
attributes.value[1] &&
|
||||||
sanitizeParameterValue([
|
sanitizeParameterValue([val, attributes.value[1][1]])[0] ===
|
||||||
path[i][j + 1],
|
attributes.value[1][0]
|
||||||
attributes.value[1][1]
|
|
||||||
])[0] === attributes.value[1][0]
|
|
||||||
) {
|
) {
|
||||||
for (let m = 0; m < allMatches.length; ++m) {
|
for (let m = 0; m < allMatches.length; ++m) {
|
||||||
let k;
|
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) {
|
if (k < 0) {
|
||||||
filtered.push(allMatches[m]);
|
filtered.push(allMatches[m]);
|
||||||
allMatches[m] = [];
|
allMatches[m] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,21 +172,19 @@ function unpack(deviceData, path, revision) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allMatches.sort((a, b) => {
|
allMatches.sort((p1, p2) => {
|
||||||
for (let i = 0; i < a.length; ++i) {
|
for (let i = 0; i < p1.length; ++i) {
|
||||||
if (a === b) continue;
|
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
|
if (ia === +a && ib === +b) return ia - ib;
|
||||||
const ia = parseInt(a);
|
else if (a < b) return -1;
|
||||||
const ib = parseInt(b);
|
else return 1;
|
||||||
|
|
||||||
if (ia.toString() === a && ib.toString() === b) {
|
|
||||||
a = ia;
|
|
||||||
b = ib;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a < b) return -1;
|
|
||||||
else return 1;
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
@@ -194,7 +192,13 @@ function unpack(deviceData, path, revision) {
|
|||||||
return allMatches;
|
return allMatches;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear(deviceData, path, timestamp, attributes, changeFlags = 0) {
|
export function clear(
|
||||||
|
deviceData,
|
||||||
|
path,
|
||||||
|
timestamp,
|
||||||
|
attributes,
|
||||||
|
changeFlags = 0
|
||||||
|
): void {
|
||||||
const changeTrackers = {};
|
const changeTrackers = {};
|
||||||
|
|
||||||
timestamp = timestamp || 0;
|
timestamp = timestamp || 0;
|
||||||
@@ -253,8 +257,8 @@ function clear(deviceData, path, timestamp, attributes, changeFlags = 0) {
|
|||||||
if (changeTrackers[k] & changeFlags) deviceData.changes.add(k);
|
if (changeTrackers[k] & changeFlags) deviceData.changes.add(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareEquality(a, b) {
|
function compareEquality(a, b): boolean {
|
||||||
var t = typeof a;
|
const t = typeof a;
|
||||||
if (
|
if (
|
||||||
a === null ||
|
a === null ||
|
||||||
a === undefined ||
|
a === undefined ||
|
||||||
@@ -268,7 +272,13 @@ function compareEquality(a, b) {
|
|||||||
return JSON.stringify(a) === JSON.stringify(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);
|
path = deviceData.paths.add(path);
|
||||||
|
|
||||||
const currentTimestamp = deviceData.timestamps.get(path);
|
const currentTimestamp = deviceData.timestamps.get(path);
|
||||||
@@ -366,7 +376,12 @@ function set(deviceData, path, timestamp, attributes, toClear) {
|
|||||||
return 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);
|
path = deviceData.paths.add(path);
|
||||||
let f = 1;
|
let f = 1;
|
||||||
|
|
||||||
@@ -382,7 +397,7 @@ function track(deviceData, path, marker, attributes) {
|
|||||||
cur[marker] |= f;
|
cur[marker] |= f;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTrackers(deviceData, tracker) {
|
export function clearTrackers(deviceData: DeviceData, tracker): void {
|
||||||
if (Array.isArray(tracker)) {
|
if (Array.isArray(tracker)) {
|
||||||
for (const v of deviceData.trackers.values())
|
for (const v of deviceData.trackers.values())
|
||||||
for (const t of tracker) delete v[t];
|
for (const t of tracker) delete v[t];
|
||||||
@@ -392,11 +407,3 @@ function clearTrackers(deviceData, tracker) {
|
|||||||
deviceData.changes.delete(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;
|
|
||||||
@@ -16,35 +16,41 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const childProcess = require("child_process");
|
import { spawn } from "child_process";
|
||||||
const crypto = require("crypto");
|
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 processes = {};
|
||||||
const jobs = {};
|
const jobs = new Map();
|
||||||
|
|
||||||
function messageHandler(message) {
|
function messageHandler(message): void {
|
||||||
const func = jobs[message[0]];
|
const func = jobs.get(message[0]);
|
||||||
if (func) {
|
if (func) {
|
||||||
delete jobs[message[0]];
|
jobs.delete(message[0]);
|
||||||
func(null, message[1], message[2]);
|
func(null, message[1], message[2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function run(args, callback) {
|
export function run(args, callback): boolean {
|
||||||
const scriptName = args[0];
|
const scriptName = args[0];
|
||||||
|
|
||||||
|
const id = crypto.randomBytes(8).toString("hex");
|
||||||
|
jobs.set(id, callback);
|
||||||
|
|
||||||
if (!processes[scriptName]) {
|
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;
|
processes[scriptName] = p;
|
||||||
|
|
||||||
p.on("error", () => {
|
p.on("error", err => {
|
||||||
if (processes[scriptName] === p) {
|
if (processes[scriptName] === p) {
|
||||||
|
if (jobs.delete(id))
|
||||||
|
callback(null, { code: err.name, message: err.message });
|
||||||
|
|
||||||
kill(processes[scriptName]);
|
kill(processes[scriptName]);
|
||||||
delete processes[scriptName];
|
delete processes[scriptName];
|
||||||
}
|
}
|
||||||
@@ -60,19 +66,17 @@ function run(args, callback) {
|
|||||||
p.on("message", messageHandler);
|
p.on("message", messageHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = crypto.randomBytes(8).toString("hex");
|
|
||||||
jobs[id] = callback;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (id in jobs) {
|
if (jobs.delete(id))
|
||||||
delete jobs[id];
|
|
||||||
callback(null, { code: "timeout", message: "Extension timed out" });
|
callback(null, { code: "timeout", message: "Extension timed out" });
|
||||||
}
|
|
||||||
}, TIMEOUT);
|
}, TIMEOUT);
|
||||||
|
|
||||||
|
if (!processes[scriptName].connected) return false;
|
||||||
|
|
||||||
return processes[scriptName].send([id, args.slice(1)]);
|
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;
|
if (process.signalCode !== null || process.exitCode !== null) return;
|
||||||
|
|
||||||
const timeToKill = Date.now() + 5000;
|
const timeToKill = Date.now() + 5000;
|
||||||
@@ -91,7 +95,7 @@ function kill(process, callback) {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function killAll(callback) {
|
export function killAll(callback?): void {
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
for (const n in processes) {
|
for (const n in processes) {
|
||||||
++counter;
|
++counter;
|
||||||
@@ -102,6 +106,3 @@ function killAll(callback) {
|
|||||||
}
|
}
|
||||||
if (--counter === 0 && callback) callback();
|
if (--counter === 0 && callback) callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.run = run;
|
|
||||||
exports.killAll = killAll;
|
|
||||||
+7
-10
@@ -16,14 +16,13 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const url = require("url");
|
import * as url from "url";
|
||||||
const querystring = require("querystring");
|
import * as querystring from "querystring";
|
||||||
const mongodb = require("mongodb");
|
import { GridFSBucket } from "mongodb";
|
||||||
const db = require("./db");
|
import * as db from "./db";
|
||||||
|
|
||||||
function listener(request, response) {
|
export function listener(request, response): void {
|
||||||
const urlParts = url.parse(request.url, true);
|
const urlParts = url.parse(request.url, true);
|
||||||
if (request.method === "GET") {
|
if (request.method === "GET") {
|
||||||
const filename = querystring.unescape(urlParts.pathname.substring(1));
|
const filename = querystring.unescape(urlParts.pathname.substring(1));
|
||||||
@@ -45,8 +44,8 @@ function listener(request, response) {
|
|||||||
"Content-Length": file.length
|
"Content-Length": file.length
|
||||||
});
|
});
|
||||||
|
|
||||||
const bucket = new mongodb.GridFSBucket(db.client.db());
|
const bucket = new GridFSBucket(db.client.db());
|
||||||
const downloadStream = bucket.openDownloadStream(filename);
|
const downloadStream = bucket.openDownloadStreamByName(filename);
|
||||||
downloadStream.pipe(response);
|
downloadStream.pipe(response);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -54,5 +53,3 @@ function listener(request, response) {
|
|||||||
response.end("405 Method Not Allowed");
|
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
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const WILDCARD_MULTIPLIER = 2;
|
const WILDCARD_MULTIPLIER = 2;
|
||||||
const UNDISCOVERED_DEPTH = 7;
|
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
|
// 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
|
// need refreshing. Leading 0s indicate that the pattern up to that
|
||||||
// point has been discovered.
|
// point has been discovered.
|
||||||
function estimateGpnCount(gpnPatterns, depth = 0) {
|
export function estimateGpnCount(gpnPatterns, depth = 0): number {
|
||||||
const children = {};
|
const children = {};
|
||||||
const wildcardChildren = [];
|
const wildcardChildren = [];
|
||||||
let wildcardDiscovered = false;
|
let wildcardDiscovered = false;
|
||||||
@@ -77,5 +76,3 @@ function estimateGpnCount(gpnPatterns, depth = 0) {
|
|||||||
|
|
||||||
return gpnCount;
|
return gpnCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.estimateGpnCount = estimateGpnCount;
|
|
||||||
@@ -16,22 +16,27 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
class InstanceSet {
|
interface Instance {
|
||||||
constructor() {
|
[name: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class InstanceSet {
|
||||||
|
private set: Set<Instance>;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
this.set = new Set();
|
this.set = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
add(instance) {
|
public add(instance: Instance): void {
|
||||||
this.set.add(instance);
|
this.set.add(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(instance) {
|
public delete(instance: Instance): void {
|
||||||
this.set.delete(instance);
|
this.set.delete(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
superset(instance) {
|
public superset(instance: Instance): Instance[] {
|
||||||
const res = [];
|
const res = [];
|
||||||
for (const inst of this.set) {
|
for (const inst of this.set) {
|
||||||
let match = true;
|
let match = true;
|
||||||
@@ -67,7 +72,7 @@ class InstanceSet {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
subset(instance) {
|
public subset(instance: Instance): Instance[] {
|
||||||
const res = [];
|
const res = [];
|
||||||
|
|
||||||
for (const inst of this.set) {
|
for (const inst of this.set) {
|
||||||
@@ -104,25 +109,23 @@ class InstanceSet {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
public [Symbol.iterator](): IterableIterator<Instance> {
|
||||||
return this.set.values();
|
return this.set.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
forEach(callback) {
|
public forEach(callback: (instance: Instance) => void): void {
|
||||||
return this.set.forEach(callback);
|
this.set.forEach(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
values() {
|
public values(): IterableIterator<Instance> {
|
||||||
return this.set.values();
|
return this.set.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
public clear(): void {
|
||||||
this.set.clear();
|
this.set.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
public get size(): number {
|
||||||
return this.set.size;
|
return this.set.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = InstanceSet;
|
|
||||||
@@ -16,16 +16,30 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const vm = require("vm");
|
import * as vm from "vm";
|
||||||
const crypto = require("crypto");
|
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");
|
interface Permissions {
|
||||||
const cache = require("./cache");
|
[role: string]: {
|
||||||
const query = require("./query");
|
[access: number]: {
|
||||||
const logger = require("./logger");
|
[resource: string]: {
|
||||||
const scheduling = require("./scheduling");
|
access: number;
|
||||||
|
filter: Expression;
|
||||||
|
validate: { [validator: string]: boolean };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const REFRESH = 3000;
|
const REFRESH = 3000;
|
||||||
const EVICT_TIMEOUT = 60000;
|
const EVICT_TIMEOUT = 60000;
|
||||||
@@ -34,7 +48,7 @@ const snapshots = new Map();
|
|||||||
let currentSnapshot = null;
|
let currentSnapshot = null;
|
||||||
let nextRefresh = 1;
|
let nextRefresh = 1;
|
||||||
|
|
||||||
function computeHash(snapshot) {
|
function computeHash(snapshot): string {
|
||||||
// MD5 hash for presets, provisions, virtual parameters for detecting changes
|
// MD5 hash for presets, provisions, virtual parameters for detecting changes
|
||||||
const h = crypto.createHash("md5");
|
const h = crypto.createHash("md5");
|
||||||
for (const p of snapshot.presets) {
|
for (const p of snapshot.presets) {
|
||||||
@@ -56,10 +70,35 @@ function computeHash(snapshot) {
|
|||||||
h.update(JSON.stringify(keys));
|
h.update(JSON.stringify(keys));
|
||||||
for (const k of keys) h.update(snapshot.virtualParameters[k].md5);
|
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");
|
return h.digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenObject(src, prefix = "", dst = {}) {
|
function flattenObject(src, prefix = "", dst = {}): {} {
|
||||||
for (const k of Object.keys(src)) {
|
for (const k of Object.keys(src)) {
|
||||||
const v = src[k];
|
const v = src[k];
|
||||||
if (typeof v === "object" && !Array.isArray(v))
|
if (typeof v === "object" && !Array.isArray(v))
|
||||||
@@ -69,7 +108,7 @@ function flattenObject(src, prefix = "", dst = {}) {
|
|||||||
return dst;
|
return dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh(callback) {
|
function refresh(callback): void {
|
||||||
if (!nextRefresh) {
|
if (!nextRefresh) {
|
||||||
return void setTimeout(() => {
|
return void setTimeout(() => {
|
||||||
refresh(callback);
|
refresh(callback);
|
||||||
@@ -120,7 +159,7 @@ function refresh(callback) {
|
|||||||
else return a.weight - b.weight;
|
else return a.weight - b.weight;
|
||||||
});
|
});
|
||||||
|
|
||||||
const presets = [];
|
const presets = [] as Preset[];
|
||||||
for (const preset of res) {
|
for (const preset of res) {
|
||||||
let schedule = null;
|
let schedule = null;
|
||||||
if (preset.schedule) {
|
if (preset.schedule) {
|
||||||
@@ -146,9 +185,15 @@ function refresh(callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const events = preset.events || {};
|
const events = preset.events || {};
|
||||||
const precondition = query.convertMongoQueryToFilters(
|
let precondition;
|
||||||
JSON.parse(preset.precondition)
|
try {
|
||||||
);
|
precondition = parse(preset.precondition);
|
||||||
|
} catch (error) {
|
||||||
|
precondition = mongoQueryToFilter(
|
||||||
|
JSON.parse(preset.precondition)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const _provisions = preset.provisions || [];
|
const _provisions = preset.provisions || [];
|
||||||
|
|
||||||
// Generate provisions from the old configuration format
|
// 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)
|
Promise.all(promises)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const snapshot = {
|
const snapshot = {
|
||||||
presets: res[0],
|
presets: res[0],
|
||||||
provisions: res[1],
|
provisions: res[1],
|
||||||
virtualParameters: res[2],
|
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) {
|
if (currentSnapshot) {
|
||||||
@@ -327,44 +466,128 @@ function refresh(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentSnapshot(callback) {
|
interface GetCurrentSnapshotCallback {
|
||||||
if (Date.now() < nextRefresh) return void callback(null, currentSnapshot);
|
(err: null, currentSnapshot: string): void;
|
||||||
refresh(err => {
|
(err: Error, currentSnapshot?: null): void;
|
||||||
callback(err, currentSnapshot);
|
}
|
||||||
|
|
||||||
|
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);
|
return snapshots.has(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPresets(snapshotKey) {
|
export function getPresets(snapshotKey): Preset[] {
|
||||||
const snapshot = snapshots.get(snapshotKey);
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
return snapshot.presets;
|
return snapshot.presets;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProvisions(snapshotKey) {
|
export function getProvisions(
|
||||||
|
snapshotKey
|
||||||
|
): { [name: string]: { md5: string; script: vm.Script } } {
|
||||||
const snapshot = snapshots.get(snapshotKey);
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
return snapshot.provisions;
|
return snapshot.provisions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVirtualParameters(snapshotKey) {
|
export function getVirtualParameters(
|
||||||
|
snapshotKey
|
||||||
|
): { [name: string]: { md5: string; script: vm.Script } } {
|
||||||
const snapshot = snapshots.get(snapshotKey);
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
return snapshot.virtualParameters;
|
return snapshot.virtualParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFiles(snapshotKey) {
|
export function getFiles(
|
||||||
|
snapshotKey
|
||||||
|
): { [name: string]: { length: number; md5: string; contentType: string } } {
|
||||||
const snapshot = snapshots.get(snapshotKey);
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
return snapshot.files;
|
return snapshot.files;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getCurrentSnapshot = getCurrentSnapshot;
|
export function getConfig(
|
||||||
exports.hasSnapshot = hasSnapshot;
|
snapshotKey,
|
||||||
exports.getPresets = getPresets;
|
key,
|
||||||
exports.getProvisions = getProvisions;
|
context?,
|
||||||
exports.getVirtualParameters = getVirtualParameters;
|
now?
|
||||||
exports.getFiles = getFiles;
|
): 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;
|
||||||
|
}
|
||||||
@@ -16,32 +16,46 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const fs = require("fs");
|
import * as fs from "fs";
|
||||||
const os = require("os");
|
import * as os from "os";
|
||||||
|
|
||||||
const config = require("./config");
|
import * as config from "./config";
|
||||||
|
|
||||||
const REOPEN_EVERY = 60000;
|
const REOPEN_EVERY = 60000;
|
||||||
|
|
||||||
const LOG_FORMAT = config.get("LOG_FORMAT");
|
const LOG_FORMAT = config.get("LOG_FORMAT");
|
||||||
const ACCESS_LOG_FORMAT = config.get("ACCESS_LOG_FORMAT") || 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 LOG_SYSTEMD = false;
|
||||||
let ACCESS_LOG_SYSTEMD = false;
|
let ACCESS_LOG_SYSTEMD = false;
|
||||||
|
|
||||||
let LOG_FILE, ACCESS_LOG_FILE;
|
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 logStream = fs.createWriteStream(null, { fd: process.stderr.fd });
|
||||||
let logStat = fs.fstatSync(logStream.fd);
|
let logStat = fs.fstatSync(logStream.fd);
|
||||||
let accessLogStream = fs.createWriteStream(null, { fd: process.stdout.fd });
|
let accessLogStream = fs.createWriteStream(null, { fd: process.stdout.fd });
|
||||||
let accessLogStat = fs.fstatSync(accessLogStream.fd);
|
let accessLogStat = fs.fstatSync(accessLogStream.fd);
|
||||||
|
|
||||||
// Reopen if original files have been moved (e.g. logrotate)
|
// Reopen if original files have been moved (e.g. logrotate)
|
||||||
function reopen() {
|
function reopen(): void {
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
|
|
||||||
if (LOG_FILE) {
|
if (LOG_FILE) {
|
||||||
@@ -90,7 +104,7 @@ function reopen() {
|
|||||||
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
|
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
function init(service, version) {
|
export function init(service, version): void {
|
||||||
defaultMeta.hostname = os.hostname();
|
defaultMeta.hostname = os.hostname();
|
||||||
defaultMeta.pid = process.pid;
|
defaultMeta.pid = process.pid;
|
||||||
defaultMeta.name = `genieacs-${service}`;
|
defaultMeta.name = `genieacs-${service}`;
|
||||||
@@ -115,9 +129,7 @@ function init(service, version) {
|
|||||||
const JOURNAL_STREAM = process.env["JOURNAL_STREAM"];
|
const JOURNAL_STREAM = process.env["JOURNAL_STREAM"];
|
||||||
|
|
||||||
if (JOURNAL_STREAM) {
|
if (JOURNAL_STREAM) {
|
||||||
let [dev, inode] = JOURNAL_STREAM.split(":");
|
const [dev, inode] = JOURNAL_STREAM.split(":").map(parseInt);
|
||||||
dev = parseInt(dev);
|
|
||||||
inode = parseInt(inode);
|
|
||||||
|
|
||||||
LOG_SYSTEMD = logStat.dev === dev && logStat.ino === inode;
|
LOG_SYSTEMD = logStat.dev === dev && logStat.ino === inode;
|
||||||
ACCESS_LOG_SYSTEMD =
|
ACCESS_LOG_SYSTEMD =
|
||||||
@@ -129,12 +141,12 @@ function init(service, version) {
|
|||||||
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
|
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
export function close(): void {
|
||||||
accessLogStream.end();
|
accessLogStream.end();
|
||||||
logStream.end();
|
logStream.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
function flatten(details) {
|
export function flatten(details): {} {
|
||||||
if (details.sessionContext) {
|
if (details.sessionContext) {
|
||||||
details.deviceId = details.sessionContext.deviceId;
|
details.deviceId = details.sessionContext.deviceId;
|
||||||
details.remoteAddress =
|
details.remoteAddress =
|
||||||
@@ -184,10 +196,21 @@ function flatten(details) {
|
|||||||
delete details.fault;
|
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;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatJson(details, systemd) {
|
function formatJson(details, systemd): string {
|
||||||
if (systemd) {
|
if (systemd) {
|
||||||
let severity = "";
|
let severity = "";
|
||||||
if (details.severity === "info") severity = "<6>";
|
if (details.severity === "info") severity = "<6>";
|
||||||
@@ -200,19 +223,22 @@ function formatJson(details, systemd) {
|
|||||||
return `${JSON.stringify(flatten(details))}${os.EOL}`;
|
return `${JSON.stringify(flatten(details))}${os.EOL}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSimple(details, systemd) {
|
function formatSimple(details, systemd): string {
|
||||||
flatten(details);
|
flatten(details);
|
||||||
|
|
||||||
let remote = "";
|
let remote = "";
|
||||||
if (details.remoteAddress) {
|
if (details.remoteAddress) {
|
||||||
if (details.deviceId)
|
if (details.deviceId)
|
||||||
remote = `${details.remoteAddress} ${details.deviceId}: `;
|
remote = `${details.remoteAddress} ${details.deviceId}: `;
|
||||||
|
else if (details.user)
|
||||||
|
remote = `${details.user}@${details.remoteAddress}: `;
|
||||||
else remote = `${details.remoteAddress}: `;
|
else remote = `${details.remoteAddress}: `;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = Object.keys(details);
|
const keys = Object.keys(details);
|
||||||
const skip = {
|
const skip = {
|
||||||
deviceId: 1,
|
deviceId: 1,
|
||||||
|
user: 1,
|
||||||
remoteAddress: 1,
|
remoteAddress: 1,
|
||||||
severity: 1,
|
severity: 1,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
@@ -241,7 +267,7 @@ function formatSimple(details, systemd) {
|
|||||||
}${meta}${os.EOL}`;
|
}${meta}${os.EOL}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(details) {
|
function log(details): void {
|
||||||
details.timestamp = new Date().toISOString();
|
details.timestamp = new Date().toISOString();
|
||||||
if (LOG_FORMAT === "json") {
|
if (LOG_FORMAT === "json") {
|
||||||
details = Object.assign({}, defaultMeta, details);
|
details = Object.assign({}, defaultMeta, details);
|
||||||
@@ -251,22 +277,22 @@ function log(details) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function info(details) {
|
export function info(details): void {
|
||||||
details.severity = "info";
|
details.severity = "info";
|
||||||
log(details);
|
log(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
function warn(details) {
|
export function warn(details): void {
|
||||||
details.severity = "warn";
|
details.severity = "warn";
|
||||||
log(details);
|
log(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
function error(details) {
|
export function error(details): void {
|
||||||
details.severity = "error";
|
details.severity = "error";
|
||||||
log(details);
|
log(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
function accessLog(details) {
|
export function accessLog(details): void {
|
||||||
details.timestamp = new Date().toISOString();
|
details.timestamp = new Date().toISOString();
|
||||||
if (ACCESS_LOG_FORMAT === "json") {
|
if (ACCESS_LOG_FORMAT === "json") {
|
||||||
Object.assign(details, defaultMeta);
|
Object.assign(details, defaultMeta);
|
||||||
@@ -276,27 +302,17 @@ function accessLog(details) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function accessInfo(details) {
|
export function accessInfo(details): void {
|
||||||
details.severity = "info";
|
details.severity = "info";
|
||||||
accessLog(details);
|
accessLog(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
function accessWarn(details) {
|
export function accessWarn(details): void {
|
||||||
details.severity = "warn";
|
details.severity = "warn";
|
||||||
accessLog(details);
|
accessLog(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
function accessError(details) {
|
export function accessError(details): void {
|
||||||
details.severity = "error";
|
details.severity = "error";
|
||||||
accessLog(details);
|
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;
|
|
||||||
@@ -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
@@ -16,20 +16,18 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const url = require("url");
|
import * as url from "url";
|
||||||
const mongodb = require("mongodb");
|
import * as mongodb from "mongodb";
|
||||||
const querystring = require("querystring");
|
import * as querystring from "querystring";
|
||||||
const vm = require("vm");
|
import * as vm from "vm";
|
||||||
const childProcess = require("child_process");
|
import { exec } from "child_process";
|
||||||
const config = require("./config");
|
import * as config from "./config";
|
||||||
const db = require("./db");
|
import * as db from "./db";
|
||||||
const query = require("./query");
|
import * as query from "./query";
|
||||||
const apiFunctions = require("./api-functions");
|
import * as apiFunctions from "./api-functions";
|
||||||
const cache = require("./cache");
|
import * as cache from "./cache";
|
||||||
|
import { version as VERSION } from "../package.json";
|
||||||
const VERSION = require("../package.json").version;
|
|
||||||
|
|
||||||
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
||||||
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
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 VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-_%]+)\/?$/;
|
||||||
const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/;
|
const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/;
|
||||||
|
|
||||||
function throwError(err, httpResponse) {
|
function throwError(err, httpResponse?): never {
|
||||||
if (httpResponse) {
|
if (httpResponse) {
|
||||||
httpResponse.writeHead(500, { Connection: "close" });
|
httpResponse.writeHead(500, { Connection: "close" });
|
||||||
httpResponse.end(`${err.name}: ${err.message}`);
|
httpResponse.end(`${err.name}: ${err.message}`);
|
||||||
@@ -52,17 +50,20 @@ function throwError(err, httpResponse) {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
function listener(request, response) {
|
export function listener(request, response): void {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
let bytes = 0;
|
let bytes = 0;
|
||||||
response.setHeader("GenieACS-Version", VERSION);
|
response.setHeader("GenieACS-Version", VERSION);
|
||||||
|
|
||||||
request.addListener("data", chunk => {
|
request.addListener(
|
||||||
chunks.push(chunk);
|
"data",
|
||||||
bytes += chunk.length;
|
(chunk): void => {
|
||||||
});
|
chunks.push(chunk);
|
||||||
|
bytes += chunk.length;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function getBody() {
|
function getBody(): Buffer {
|
||||||
// Write all chunks into a Buffer
|
// Write all chunks into a Buffer
|
||||||
const body = Buffer.allocUnsafe(bytes);
|
const body = Buffer.allocUnsafe(bytes);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
@@ -81,7 +82,7 @@ function listener(request, response) {
|
|||||||
PRESETS_REGEX.exec(urlParts.pathname)[1]
|
PRESETS_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
if (request.method === "PUT") {
|
if (request.method === "PUT") {
|
||||||
const preset = JSON.parse(body);
|
const preset = JSON.parse(body.toString());
|
||||||
preset._id = presetName;
|
preset._id = presetName;
|
||||||
db.presetsCollection.replaceOne(
|
db.presetsCollection.replaceOne(
|
||||||
{ _id: presetName },
|
{ _id: presetName },
|
||||||
@@ -118,7 +119,7 @@ function listener(request, response) {
|
|||||||
OBJECTS_REGEX.exec(urlParts.pathname)[1]
|
OBJECTS_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
if (request.method === "PUT") {
|
if (request.method === "PUT") {
|
||||||
const object = JSON.parse(body);
|
const object = JSON.parse(body.toString());
|
||||||
object._id = objectName;
|
object._id = objectName;
|
||||||
db.objectsCollection.replaceOne(
|
db.objectsCollection.replaceOne(
|
||||||
{ _id: objectName },
|
{ _id: objectName },
|
||||||
@@ -321,7 +322,7 @@ function listener(request, response) {
|
|||||||
DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1]
|
DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
if (body.length) {
|
if (body.length) {
|
||||||
const task = JSON.parse(body);
|
const task = JSON.parse(body.toString());
|
||||||
task.device = deviceId;
|
task.device = deviceId;
|
||||||
apiFunctions.insertTasks(task, err => {
|
apiFunctions.insertTasks(task, err => {
|
||||||
if (err) return void throwError(err, response);
|
if (err) return void throwError(err, response);
|
||||||
@@ -341,7 +342,7 @@ function listener(request, response) {
|
|||||||
|
|
||||||
const taskTimeout =
|
const taskTimeout =
|
||||||
(urlParts.query.timeout &&
|
(urlParts.query.timeout &&
|
||||||
parseInt(urlParts.query.timeout)) ||
|
parseInt(urlParts.query.timeout as string)) ||
|
||||||
config.get("DEVICE_ONLINE_THRESHOLD", deviceId);
|
config.get("DEVICE_ONLINE_THRESHOLD", deviceId);
|
||||||
|
|
||||||
apiFunctions.watchTask(
|
apiFunctions.watchTask(
|
||||||
@@ -500,15 +501,18 @@ function listener(request, response) {
|
|||||||
const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
|
const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
|
||||||
metadata: metadata
|
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.writeHead(201);
|
||||||
response.end();
|
response.end();
|
||||||
});
|
});
|
||||||
} else if (request.method === "DELETE") {
|
} else if (request.method === "DELETE") {
|
||||||
const bucket = new mongodb.GridFSBucket(db.client.db());
|
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);
|
if (err) return void throwError(err, response);
|
||||||
|
|
||||||
response.writeHead(200);
|
response.writeHead(200);
|
||||||
@@ -520,7 +524,7 @@ function listener(request, response) {
|
|||||||
}
|
}
|
||||||
} else if (PING_REGEX.test(urlParts.pathname)) {
|
} else if (PING_REGEX.test(urlParts.pathname)) {
|
||||||
const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]);
|
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) {
|
if (err) {
|
||||||
response.writeHead(404, { "Cache-Control": "no-cache" });
|
response.writeHead(404, { "Cache-Control": "no-cache" });
|
||||||
response.end(`${err.name}: ${err.message}`);
|
response.end(`${err.name}: ${err.message}`);
|
||||||
@@ -578,7 +582,7 @@ function listener(request, response) {
|
|||||||
let q = {};
|
let q = {};
|
||||||
if (urlParts.query.query) {
|
if (urlParts.query.query) {
|
||||||
try {
|
try {
|
||||||
q = JSON.parse(urlParts.query.query);
|
q = JSON.parse(urlParts.query.query as string);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
response.writeHead(400);
|
response.writeHead(400);
|
||||||
response.end(`${err.name}: ${err.message}`);
|
response.end(`${err.name}: ${err.message}`);
|
||||||
@@ -607,14 +611,14 @@ function listener(request, response) {
|
|||||||
let projection = null;
|
let projection = null;
|
||||||
if (urlParts.query.projection) {
|
if (urlParts.query.projection) {
|
||||||
projection = {};
|
projection = {};
|
||||||
for (const p of urlParts.query.projection.split(","))
|
for (const p of (urlParts.query.projection as string).split(","))
|
||||||
projection[p.trim()] = 1;
|
projection[p.trim()] = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cur = collection.find(q, { projection: projection });
|
const cur = collection.find(q, { projection: projection });
|
||||||
|
|
||||||
if (urlParts.query.sort) {
|
if (urlParts.query.sort) {
|
||||||
const s = JSON.parse(urlParts.query.sort);
|
const s = JSON.parse(urlParts.query.sort as string);
|
||||||
const sort = {};
|
const sort = {};
|
||||||
for (const [k, v] of Object.entries(s)) {
|
for (const [k, v] of Object.entries(s)) {
|
||||||
if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices")
|
if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices")
|
||||||
@@ -625,9 +629,11 @@ function listener(request, response) {
|
|||||||
cur.sort(sort);
|
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) => {
|
cur.count(false, (err, total) => {
|
||||||
if (err) return void throwError(err);
|
if (err) return void throwError(err);
|
||||||
@@ -661,5 +667,3 @@ function listener(request, response) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.listener = listener;
|
|
||||||
+13
-149
@@ -16,11 +16,10 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* 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;
|
if (input.indexOf("*") === -1) return false;
|
||||||
|
|
||||||
let output = input.replace(/[[\]\\^$.|?+()]/, "\\$&");
|
let output = input.replace(/[[\]\\^$.|?+()]/, "\\$&");
|
||||||
@@ -34,7 +33,7 @@ function stringToRegexp(input, flags) {
|
|||||||
return new RegExp(output, flags);
|
return new RegExp(output, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalize(input) {
|
function normalize(input): any {
|
||||||
if (common.typeOf(input) === common.STRING_TYPE) {
|
if (common.typeOf(input) === common.STRING_TYPE) {
|
||||||
const vals = [input];
|
const vals = [input];
|
||||||
const m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input);
|
const m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input);
|
||||||
@@ -53,7 +52,7 @@ function normalize(input) {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandValue(value) {
|
export function expandValue(value): any[] {
|
||||||
if (common.typeOf(value) === common.ARRAY_TYPE) {
|
if (common.typeOf(value) === common.ARRAY_TYPE) {
|
||||||
let a = [];
|
let a = [];
|
||||||
for (const j of value) a = a.concat(expandValue(j));
|
for (const j of value) a = a.concat(expandValue(j));
|
||||||
@@ -89,7 +88,7 @@ function expandValue(value) {
|
|||||||
return objs;
|
return objs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function permute(param, val) {
|
function permute(param, val): any[] {
|
||||||
const conditions = [];
|
const conditions = [];
|
||||||
const values = expandValue(val);
|
const values = expandValue(val);
|
||||||
|
|
||||||
@@ -104,12 +103,12 @@ function permute(param, val) {
|
|||||||
return conditions;
|
return conditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function expand(query) {
|
export function expand(query): {} {
|
||||||
const newQuery = {};
|
const newQuery = {};
|
||||||
for (const [k, v] of Object.entries(query)) {
|
for (const [k, v] of Object.entries(query)) {
|
||||||
if (k[0] === "$") {
|
if (k[0] === "$") {
|
||||||
// Operator
|
// Operator
|
||||||
newQuery[k] = v.map(e => expand(e));
|
newQuery[k] = (v as any[]).map(e => expand(e));
|
||||||
} else {
|
} else {
|
||||||
const conditions = permute(k, v);
|
const conditions = permute(k, v);
|
||||||
if (conditions.length > 1) {
|
if (conditions.length > 1) {
|
||||||
@@ -130,7 +129,7 @@ function expand(query) {
|
|||||||
return newQuery;
|
return newQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testExpressions(params, expressions, lop) {
|
function testExpressions(params, expressions, lop): boolean {
|
||||||
for (const f of expressions) {
|
for (const f of expressions) {
|
||||||
const res = test(params, f);
|
const res = test(params, f);
|
||||||
switch (lop) {
|
switch (lop) {
|
||||||
@@ -157,7 +156,7 @@ function testExpressions(params, expressions, lop) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function test(params, query) {
|
export function test(params, query): boolean {
|
||||||
let res;
|
let res;
|
||||||
for (const [k, v] of Object.entries(query)) {
|
for (const [k, v] of Object.entries(query)) {
|
||||||
if (k.charAt(0) === "$") {
|
if (k.charAt(0) === "$") {
|
||||||
@@ -170,7 +169,8 @@ function test(params, query) {
|
|||||||
// TODO comparing array to regex, array to array, and object to object
|
// TODO comparing array to regex, array to array, and object to object
|
||||||
res = value.indexOf(v) !== -1;
|
res = value.indexOf(v) !== -1;
|
||||||
} else {
|
} 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 res = v === value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -216,141 +216,11 @@ function test(params, query) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchType(src, dst) {
|
export function sanitizeQueryTypes(query, types): {} {
|
||||||
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) {
|
|
||||||
for (const [k, v] of Object.entries(query)) {
|
for (const [k, v] of Object.entries(query)) {
|
||||||
if (k[0] === "$") {
|
if (k[0] === "$") {
|
||||||
// Logical operator
|
// 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) {
|
} else if (k in types) {
|
||||||
if (common.typeOf(v) === common.OBJECT_TYPE) {
|
if (common.typeOf(v) === common.OBJECT_TYPE) {
|
||||||
for (const [kk, vv] of Object.entries(v)) {
|
for (const [kk, vv] of Object.entries(v)) {
|
||||||
@@ -383,9 +253,3 @@ function sanitizeQueryTypes(query, types) {
|
|||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.expand = expand;
|
|
||||||
exports.test = test;
|
|
||||||
exports.sanitizeQueryTypes = sanitizeQueryTypes;
|
|
||||||
exports.convertMongoQueryToFilters = convertMongoQueryToFilters;
|
|
||||||
exports.testFilter = testFilter;
|
|
||||||
@@ -16,17 +16,15 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const vm = require("vm");
|
import * as vm from "vm";
|
||||||
|
import seedrandom from "seedrandom";
|
||||||
const seedrandom = require("seedrandom");
|
import * as device from "./device";
|
||||||
|
import * as extensions from "./extensions";
|
||||||
const common = require("./common");
|
import * as logger from "./logger";
|
||||||
const device = require("./device");
|
import * as scheduling from "./scheduling";
|
||||||
const extensions = require("./extensions");
|
import Path from "./common/path";
|
||||||
const logger = require("./logger");
|
import { Fault, SessionContext } from "./types";
|
||||||
const scheduling = require("./scheduling");
|
|
||||||
|
|
||||||
// Used for throwing to exit user script and commit
|
// Used for throwing to exit user script and commit
|
||||||
const COMMIT = Symbol();
|
const COMMIT = Symbol();
|
||||||
@@ -41,7 +39,7 @@ const context = vm.createContext();
|
|||||||
let state;
|
let state;
|
||||||
|
|
||||||
const runningExtensions = new WeakMap();
|
const runningExtensions = new WeakMap();
|
||||||
function runExtension(sessionContext, key, extCall, callback) {
|
function runExtension(sessionContext, key, extCall, callback): void {
|
||||||
let re = runningExtensions.get(sessionContext);
|
let re = runningExtensions.get(sessionContext);
|
||||||
if (!re) {
|
if (!re) {
|
||||||
re = {};
|
re = {};
|
||||||
@@ -62,13 +60,23 @@ function runExtension(sessionContext, key, extCall, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SandboxDate {
|
class SandboxDate {
|
||||||
constructor() {
|
public constructor(
|
||||||
if (arguments.length) return new Date(...arguments);
|
...argumentList: [
|
||||||
|
number?,
|
||||||
|
number?,
|
||||||
|
number?,
|
||||||
|
number?,
|
||||||
|
number?,
|
||||||
|
number?,
|
||||||
|
number?
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
if (argumentList.length) return new Date(...argumentList);
|
||||||
|
|
||||||
return new Date(state.sessionContext.timestamp);
|
return new Date(state.sessionContext.timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
static now(intervalOrCron, variance) {
|
public static now(intervalOrCron, variance): number {
|
||||||
let t = state.sessionContext.timestamp;
|
let t = state.sessionContext.timestamp;
|
||||||
|
|
||||||
if (typeof intervalOrCron === "number") {
|
if (typeof intervalOrCron === "number") {
|
||||||
@@ -93,7 +101,7 @@ class SandboxDate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function random() {
|
function random(): number {
|
||||||
if (!state.rng) state.rng = seedrandom(state.sessionContext.deviceId);
|
if (!state.rng) state.rng = seedrandom(state.sessionContext.deviceId);
|
||||||
|
|
||||||
return state.rng();
|
return state.rng();
|
||||||
@@ -104,7 +112,7 @@ random.seed = function(s) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class ParameterWrapper {
|
class ParameterWrapper {
|
||||||
constructor(path, attributes, unpacked, unpackedRevision) {
|
public constructor(path: Path, attributes, unpacked?, unpackedRevision?) {
|
||||||
for (const attrName of attributes) {
|
for (const attrName of attributes) {
|
||||||
Object.defineProperty(this, attrName, {
|
Object.defineProperty(this, attrName, {
|
||||||
get: function() {
|
get: function() {
|
||||||
@@ -148,7 +156,7 @@ class ParameterWrapper {
|
|||||||
|
|
||||||
if (!unpacked.length) return UNDEFINED;
|
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;
|
state.uncommitted = true;
|
||||||
if (!timestamps) timestamps = {};
|
if (!timestamps) timestamps = {};
|
||||||
|
|
||||||
if (!values) values = {};
|
if (!values) values = {};
|
||||||
|
|
||||||
const parsedPath = common.parsePath(path);
|
const parsedPath = Path.parse(path);
|
||||||
|
|
||||||
const declaration = {
|
const declaration = {
|
||||||
path: parsedPath,
|
path: parsedPath,
|
||||||
@@ -237,14 +249,14 @@ function declare(path, timestamps, values) {
|
|||||||
return new ParameterWrapper(parsedPath, attrs);
|
return new ParameterWrapper(parsedPath, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear(path, timestamp, attributes) {
|
function clear(path: string, timestamp: number, attributes?): void {
|
||||||
state.uncommitted = true;
|
state.uncommitted = true;
|
||||||
|
|
||||||
if (state.revision === state.maxRevision)
|
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.revision;
|
||||||
state.uncommitted = false;
|
state.uncommitted = false;
|
||||||
|
|
||||||
@@ -258,7 +270,7 @@ function commit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ext() {
|
function ext(): any {
|
||||||
++state.extCounter;
|
++state.extCounter;
|
||||||
const extCall = Array.from(arguments).map(String);
|
const extCall = Array.from(arguments).map(String);
|
||||||
const key = `${state.revision}: ${JSON.stringify(extCall)}`;
|
const key = `${state.revision}: ${JSON.stringify(extCall)}`;
|
||||||
@@ -270,19 +282,19 @@ function ext() {
|
|||||||
throw EXT;
|
throw EXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(msg, meta) {
|
function log(msg: string, meta: {}): void {
|
||||||
if (state.revision === state.maxRevision && state.extCounter >= 0) {
|
if (state.revision === state.maxRevision && state.extCounter >= 0) {
|
||||||
const details = Object.assign({}, meta, {
|
const details = Object.assign({}, meta, {
|
||||||
sessionContext: state.sessionContext,
|
sessionContext: state.sessionContext,
|
||||||
message: `Script: ${msg}`
|
message: `Script: ${msg}`
|
||||||
});
|
});
|
||||||
|
|
||||||
delete details.hostname;
|
delete details["hostname"];
|
||||||
delete details.pid;
|
delete details["pid"];
|
||||||
delete details.name;
|
delete details["name"];
|
||||||
delete details.version;
|
delete details["version"];
|
||||||
delete details.deviceId;
|
delete details["deviceId"];
|
||||||
delete details.remoteAddress;
|
delete details["remoteAddress"];
|
||||||
|
|
||||||
logger.accessInfo(details);
|
logger.accessInfo(details);
|
||||||
}
|
}
|
||||||
@@ -300,12 +312,12 @@ context.random = random;
|
|||||||
vm.runInContext("Math.random = random;", context);
|
vm.runInContext("Math.random = random;", context);
|
||||||
delete context.random;
|
delete context.random;
|
||||||
|
|
||||||
function errorToFault(err) {
|
function errorToFault(err: Error): Fault {
|
||||||
if (!err) return null;
|
if (!err) return null;
|
||||||
|
|
||||||
if (!err.name) return { code: "script", message: `${err}` };
|
if (!err.name) return { code: "script", message: `${err}` };
|
||||||
|
|
||||||
const fault = {
|
const fault: Fault = {
|
||||||
code: `script.${err.name}`,
|
code: `script.${err.name}`,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
detail: {
|
detail: {
|
||||||
@@ -315,27 +327,31 @@ function errorToFault(err) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (err.stack) {
|
if (err.stack) {
|
||||||
fault.detail.stack = err.stack;
|
fault.detail["stack"] = err.stack;
|
||||||
// Trim the stack trace at the self-executing anonymous wrapper function
|
// 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.+\)/
|
/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/
|
||||||
);
|
);
|
||||||
if (stackTrimIndex)
|
if (stackTrimIndex) {
|
||||||
fault.detail.stack = fault.detail.stack.slice(0, stackTrimIndex.index);
|
fault.detail["stack"] = fault.detail["stack"].slice(
|
||||||
|
0,
|
||||||
|
stackTrimIndex.index
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fault;
|
return fault;
|
||||||
}
|
}
|
||||||
|
|
||||||
function run(
|
export function run(
|
||||||
script,
|
script: vm.Script,
|
||||||
globals,
|
globals,
|
||||||
sessionContext,
|
sessionContext: SessionContext,
|
||||||
startRevision,
|
startRevision: number,
|
||||||
maxRevision,
|
maxRevision: number,
|
||||||
callback,
|
callback,
|
||||||
extCounter = 0
|
extCounter = 0
|
||||||
) {
|
): void {
|
||||||
state = {
|
state = {
|
||||||
sessionContext: sessionContext,
|
sessionContext: sessionContext,
|
||||||
revision: startRevision,
|
revision: startRevision,
|
||||||
@@ -416,5 +432,3 @@ function run(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.run = run;
|
|
||||||
@@ -16,13 +16,11 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* 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): number {
|
||||||
|
|
||||||
function md532(str) {
|
|
||||||
const digest = crypto
|
const digest = crypto
|
||||||
.createHash("md5")
|
.createHash("md5")
|
||||||
.update(str)
|
.update(str)
|
||||||
@@ -35,22 +33,22 @@ function md532(str) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function variance(deviceId, vrnc) {
|
export function variance(deviceId, vrnc): number {
|
||||||
return (md532(deviceId) >>> 0) % vrnc;
|
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;
|
return Math.trunc((timestamp + offset) / intrvl) * intrvl - offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCron(cronExp) {
|
export function parseCron(cronExp): any {
|
||||||
const parts = cronExp.trim().split(/\s+/);
|
const parts = cronExp.trim().split(/\s+/);
|
||||||
if (parts.length === 5) parts.unshift("*");
|
if (parts.length === 5) parts.unshift("*");
|
||||||
|
|
||||||
return later.schedule(later.parse.cron(parts.join(" "), true));
|
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!
|
// TODO later.js doesn't throw erorr if expression is invalid!
|
||||||
const ret = [0, 0];
|
const ret = [0, 0];
|
||||||
|
|
||||||
@@ -62,8 +60,3 @@ function cron(timestamp, schedule, offset = 0) {
|
|||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.variance = variance;
|
|
||||||
exports.interval = interval;
|
|
||||||
exports.parseCron = parseCron;
|
|
||||||
exports.cron = cron;
|
|
||||||
-132
@@ -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);
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
+448
-305
File diff suppressed because it is too large.
Load diff
+96
-60
@@ -16,13 +16,23 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* 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/${VERSION}`;
|
||||||
|
|
||||||
const SERVER_NAME = `GenieACS/${require("../package.json").version}`;
|
|
||||||
|
|
||||||
const NAMESPACES = {
|
const NAMESPACES = {
|
||||||
"1.0": {
|
"1.0": {
|
||||||
@@ -62,21 +72,36 @@ const NAMESPACES = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIBXMLJS_PARSE_OPTIONS = {
|
const LIBXMLJS_PARSE_OPTIONS: ParserOptions = {
|
||||||
nocdata: true,
|
nocdata: true,
|
||||||
recover: config.get("XML_RECOVER"),
|
recover: !!config.get("XML_RECOVER"),
|
||||||
ignore_enc: config.get("XML_IGNORE_ENC")
|
/* eslint-disable-next-line @typescript-eslint/camelcase */
|
||||||
|
ignore_enc: !!config.get("XML_IGNORE_ENC")
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIBXMLJS_SAVE_OPTIONS = {
|
interface WriterOptions {
|
||||||
format: config.get("XML_FORMAT"),
|
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"),
|
declaration: !config.get("XML_NO_DECL"),
|
||||||
selfCloseEmpty: !config.get("XML_NO_EMPTY")
|
selfCloseEmpty: !config.get("XML_NO_EMPTY")
|
||||||
};
|
};
|
||||||
|
|
||||||
let warnings;
|
let warnings;
|
||||||
|
|
||||||
function parseBool(v) {
|
function parseBool(v): boolean {
|
||||||
v = "" + v;
|
v = "" + v;
|
||||||
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
|
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
|
||||||
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
|
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
|
||||||
@@ -84,7 +109,7 @@ function parseBool(v) {
|
|||||||
else return null;
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function event(xml) {
|
function event(xml): string[] {
|
||||||
return xml
|
return xml
|
||||||
.childNodes()
|
.childNodes()
|
||||||
.filter(n => n.name() === "EventStruct")
|
.filter(n => n.name() === "EventStruct")
|
||||||
@@ -97,7 +122,7 @@ function event(xml) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parameterInfoList(xml) {
|
function parameterInfoList(xml): [string, boolean][] {
|
||||||
return xml
|
return xml
|
||||||
.childNodes()
|
.childNodes()
|
||||||
.filter(e => e.name() === "ParameterInfoStruct")
|
.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
|
return xml
|
||||||
.childNodes()
|
.childNodes()
|
||||||
.filter(e => e.name() === "ParameterValueStruct")
|
.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");
|
const el = xml.node("cwmp:GetParameterNames");
|
||||||
el.node("ParameterPath").text(methodRequest.parameterPath);
|
el.node("ParameterPath").text(methodRequest.parameterPath);
|
||||||
el.node("NextLevel").text(+methodRequest.nextLevel);
|
el.node("NextLevel").text(+methodRequest.nextLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GetParameterNamesResponse(xml) {
|
function GetParameterNamesResponse(xml): CpeGetResponse {
|
||||||
return {
|
return {
|
||||||
name: "GetParameterNamesResponse",
|
name: "GetParameterNamesResponse",
|
||||||
parameterList: parameterInfoList(
|
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");
|
const el = xml.node("cwmp:GetParameterValues").node("ParameterNames");
|
||||||
el.attr({
|
el.attr({
|
||||||
"soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]`
|
"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);
|
for (const p of methodRequest.parameterNames) el.node("string").text(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GetParameterValuesResponse(xml) {
|
function GetParameterValuesResponse(xml): CpeGetResponse {
|
||||||
return {
|
return {
|
||||||
name: "GetParameterValuesResponse",
|
name: "GetParameterValuesResponse",
|
||||||
parameterList: parameterValueList(
|
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 el = xml.node("cwmp:SetParameterValues");
|
||||||
const paramList = el.node("ParameterList");
|
const paramList = el.node("ParameterList");
|
||||||
paramList.attr({
|
paramList.attr({
|
||||||
@@ -244,7 +271,7 @@ function SetParameterValues(xml, methodRequest) {
|
|||||||
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function SetParameterValuesResponse(xml) {
|
function SetParameterValuesResponse(xml): CpeSetResponse {
|
||||||
return {
|
return {
|
||||||
name: "SetParameterValuesResponse",
|
name: "SetParameterValuesResponse",
|
||||||
status: parseInt(
|
status: parseInt(
|
||||||
@@ -256,13 +283,13 @@ function SetParameterValuesResponse(xml) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddObject(xml, methodRequest) {
|
function AddObject(xml, methodRequest): void {
|
||||||
const el = xml.node("cwmp:AddObject");
|
const el = xml.node("cwmp:AddObject");
|
||||||
el.node("ObjectName").text(methodRequest.objectName);
|
el.node("ObjectName").text(methodRequest.objectName);
|
||||||
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddObjectResponse(xml) {
|
function AddObjectResponse(xml): CpeSetResponse {
|
||||||
let instanceNumber, status;
|
let instanceNumber, status;
|
||||||
for (const c of xml.childNodes()) {
|
for (const c of xml.childNodes()) {
|
||||||
switch (c.name()) {
|
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");
|
const el = xml.node("cwmp:DeleteObject");
|
||||||
el.node("ObjectName").text(methodRequest.objectName);
|
el.node("ObjectName").text(methodRequest.objectName);
|
||||||
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeleteObjectResponse(xml) {
|
function DeleteObjectResponse(xml): CpeSetResponse {
|
||||||
return {
|
return {
|
||||||
name: "DeleteObjectResponse",
|
name: "DeleteObjectResponse",
|
||||||
status: parseInt(
|
status: parseInt(
|
||||||
@@ -300,28 +327,28 @@ function DeleteObjectResponse(xml) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function Reboot(xml, methodRequest) {
|
function Reboot(xml, methodRequest): void {
|
||||||
const el = xml.node("cwmp:Reboot");
|
const el = xml.node("cwmp:Reboot");
|
||||||
el.node("CommandKey").text(methodRequest.commandKey || "");
|
el.node("CommandKey").text(methodRequest.commandKey || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function RebootResponse() {
|
function RebootResponse(): CpeSetResponse {
|
||||||
return {
|
return {
|
||||||
name: "RebootResponse"
|
name: "RebootResponse"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function FactoryReset(xml) {
|
function FactoryReset(xml): void {
|
||||||
xml.node("cwmp:FactoryReset");
|
xml.node("cwmp:FactoryReset");
|
||||||
}
|
}
|
||||||
|
|
||||||
function FactoryResetResponse() {
|
function FactoryResetResponse(): CpeSetResponse {
|
||||||
return {
|
return {
|
||||||
name: "FactoryResetResponse"
|
name: "FactoryResetResponse"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function Download(xml, methodRequest) {
|
function Download(xml, methodRequest): void {
|
||||||
const el = xml.node("cwmp:Download");
|
const el = xml.node("cwmp:Download");
|
||||||
el.node("CommandKey").text(methodRequest.commandKey || "");
|
el.node("CommandKey").text(methodRequest.commandKey || "");
|
||||||
el.node("FileType").text(methodRequest.fileType);
|
el.node("FileType").text(methodRequest.fileType);
|
||||||
@@ -335,7 +362,7 @@ function Download(xml, methodRequest) {
|
|||||||
el.node("FailureURL").text(methodRequest.failureUrl || "");
|
el.node("FailureURL").text(methodRequest.failureUrl || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function DownloadResponse(xml) {
|
function DownloadResponse(xml): CpeSetResponse {
|
||||||
let status, startTime, completeTime;
|
let status, startTime, completeTime;
|
||||||
for (const c of xml.childNodes()) {
|
for (const c of xml.childNodes()) {
|
||||||
switch (c.name()) {
|
switch (c.name()) {
|
||||||
@@ -359,7 +386,7 @@ function DownloadResponse(xml) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function Inform(xml) {
|
function Inform(xml): InformRequest {
|
||||||
let retryCount, evnt, parameterList;
|
let retryCount, evnt, parameterList;
|
||||||
const deviceId = {
|
const deviceId = {
|
||||||
Manufacturer: null,
|
Manufacturer: null,
|
||||||
@@ -397,18 +424,18 @@ function Inform(xml) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function InformResponse(xml) {
|
function InformResponse(xml): void {
|
||||||
xml
|
xml
|
||||||
.node("cwmp:InformResponse")
|
.node("cwmp:InformResponse")
|
||||||
.node("MaxEnvelopes")
|
.node("MaxEnvelopes")
|
||||||
.text(1);
|
.text(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GetRPCMethods() {
|
function GetRPCMethods(): AcsResponse {
|
||||||
return { name: "GetRPCMethods" };
|
return { name: "GetRPCMethods" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function GetRPCMethodsResponse(xml, methodResponse) {
|
function GetRPCMethodsResponse(xml, methodResponse): void {
|
||||||
const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList");
|
const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList");
|
||||||
el.attr({
|
el.attr({
|
||||||
"soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]`
|
"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);
|
for (const m of methodResponse.methodList) el.node("string").text(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TransferComplete(xml) {
|
function TransferComplete(xml): AcsResponse {
|
||||||
let commandKey, _faultStruct, startTime, completeTime;
|
let commandKey, _faultStruct, startTime, completeTime;
|
||||||
for (const c of xml.childNodes()) {
|
for (const c of xml.childNodes()) {
|
||||||
switch (c.name()) {
|
switch (c.name()) {
|
||||||
@@ -445,11 +472,11 @@ function TransferComplete(xml) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function TransferCompleteResponse(xml) {
|
function TransferCompleteResponse(xml): void {
|
||||||
xml.node("cwmp:TransferCompleteResponse").text("");
|
xml.node("cwmp:TransferCompleteResponse").text("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestDownload(xml) {
|
function RequestDownload(xml): CpeRequest {
|
||||||
return {
|
return {
|
||||||
name: "RequestDownload",
|
name: "RequestDownload",
|
||||||
fileType: xml
|
fileType: xml
|
||||||
@@ -459,12 +486,12 @@ function RequestDownload(xml) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestDownloadResponse(xml) {
|
function RequestDownloadResponse(xml): void {
|
||||||
xml.node("cwmp:RequestDownloadResponse").text("");
|
xml.node("cwmp:RequestDownloadResponse").text("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function faultStruct(xml) {
|
function faultStruct(xml): FaultStruct {
|
||||||
let faultCode, faultString, setParameterValuesFault, pn, fc, fs;
|
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
|
||||||
for (const c of xml.childNodes()) {
|
for (const c of xml.childNodes()) {
|
||||||
switch (c.name()) {
|
switch (c.name()) {
|
||||||
case "FaultCode":
|
case "FaultCode":
|
||||||
@@ -499,7 +526,7 @@ function faultStruct(xml) {
|
|||||||
return { faultCode, faultString, setParameterValuesFault };
|
return { faultCode, faultString, setParameterValuesFault };
|
||||||
}
|
}
|
||||||
|
|
||||||
function fault(xml) {
|
function fault(xml): CpeFault {
|
||||||
let faultCode, faultString, detail;
|
let faultCode, faultString, detail;
|
||||||
for (const c of xml.childNodes()) {
|
for (const c of xml.childNodes()) {
|
||||||
switch (c.name()) {
|
switch (c.name()) {
|
||||||
@@ -518,7 +545,18 @@ function fault(xml) {
|
|||||||
return { faultCode, faultString, detail };
|
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;
|
warnings = warn;
|
||||||
|
|
||||||
const rpc = {
|
const rpc = {
|
||||||
@@ -526,12 +564,13 @@ function request(data, cwmpVersion, warn) {
|
|||||||
cwmpVersion: cwmpVersion,
|
cwmpVersion: cwmpVersion,
|
||||||
sessionTimeout: null,
|
sessionTimeout: null,
|
||||||
cpeRequest: null,
|
cpeRequest: null,
|
||||||
cpeFault: null
|
cpeFault: null,
|
||||||
|
cpeResponse: null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data.length) return rpc;
|
if (!data.length) return rpc;
|
||||||
|
|
||||||
const xml = libxmljs.parseXml(data, LIBXMLJS_PARSE_OPTIONS);
|
const xml = parseXml(data, LIBXMLJS_PARSE_OPTIONS);
|
||||||
|
|
||||||
let headerElement, bodyElement;
|
let headerElement, bodyElement;
|
||||||
|
|
||||||
@@ -589,7 +628,7 @@ function request(data, cwmpVersion, warn) {
|
|||||||
rpc.cpeRequest = Inform(methodElement);
|
rpc.cpeRequest = Inform(methodElement);
|
||||||
break;
|
break;
|
||||||
case "GetRPCMethods":
|
case "GetRPCMethods":
|
||||||
rpc.cpeRequest = GetRPCMethods(methodElement);
|
rpc.cpeRequest = GetRPCMethods();
|
||||||
break;
|
break;
|
||||||
case "TransferComplete":
|
case "TransferComplete":
|
||||||
rpc.cpeRequest = TransferComplete(methodElement);
|
rpc.cpeRequest = TransferComplete(methodElement);
|
||||||
@@ -613,16 +652,16 @@ function request(data, cwmpVersion, warn) {
|
|||||||
rpc.cpeResponse = DeleteObjectResponse(methodElement);
|
rpc.cpeResponse = DeleteObjectResponse(methodElement);
|
||||||
break;
|
break;
|
||||||
case "RebootResponse":
|
case "RebootResponse":
|
||||||
rpc.cpeResponse = RebootResponse(methodElement);
|
rpc.cpeResponse = RebootResponse();
|
||||||
break;
|
break;
|
||||||
case "FactoryResetResponse":
|
case "FactoryResetResponse":
|
||||||
rpc.cpeResponse = FactoryResetResponse(methodElement);
|
rpc.cpeResponse = FactoryResetResponse();
|
||||||
break;
|
break;
|
||||||
case "DownloadResponse":
|
case "DownloadResponse":
|
||||||
rpc.cpeResponse = DownloadResponse(methodElement);
|
rpc.cpeResponse = DownloadResponse(methodElement);
|
||||||
break;
|
break;
|
||||||
case "Fault":
|
case "Fault":
|
||||||
rpc.cpeFault = fault(methodElement, rpc.cwmpVersion);
|
rpc.cpeFault = fault(methodElement);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`8000 Method not supported ${methodName}`);
|
throw new Error(`8000 Method not supported ${methodName}`);
|
||||||
@@ -632,8 +671,8 @@ function request(data, cwmpVersion, warn) {
|
|||||||
return rpc;
|
return rpc;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSoapEnv(cwmpVersion) {
|
function createSoapEnv(cwmpVersion: string): Element {
|
||||||
const xml = libxmljs.Document();
|
const xml = new Document();
|
||||||
const env = xml.node("soap-env:Envelope");
|
const env = xml.node("soap-env:Envelope");
|
||||||
|
|
||||||
for (const prefix of Object.keys(NAMESPACES[cwmpVersion]))
|
for (const prefix of Object.keys(NAMESPACES[cwmpVersion]))
|
||||||
@@ -642,20 +681,20 @@ function createSoapEnv(cwmpVersion) {
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
function response(rpc) {
|
export function response(rpc): { code: number; headers: {}; data: string } {
|
||||||
const headers = {
|
const headers = {
|
||||||
Server: SERVER_NAME,
|
Server: SERVER_NAME,
|
||||||
SOAPServer: 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 env = createSoapEnv(rpc.cwmpVersion);
|
||||||
const header = env.node("soap-env:Header");
|
const header = env.node("soap-env:Header");
|
||||||
header
|
header
|
||||||
.node("cwmp:ID")
|
.node("cwmp:ID")
|
||||||
.attr({
|
.attr({
|
||||||
"soap-env:mustUnderstand": 1
|
"soap-env:mustUnderstand": "1"
|
||||||
})
|
})
|
||||||
.text(rpc.id);
|
.text(rpc.id);
|
||||||
|
|
||||||
@@ -664,16 +703,16 @@ function response(rpc) {
|
|||||||
if (rpc.acsResponse) {
|
if (rpc.acsResponse) {
|
||||||
switch (rpc.acsResponse.name) {
|
switch (rpc.acsResponse.name) {
|
||||||
case "InformResponse":
|
case "InformResponse":
|
||||||
InformResponse(body, rpc.acsResponse);
|
InformResponse(body);
|
||||||
break;
|
break;
|
||||||
case "GetRPCMethodsResponse":
|
case "GetRPCMethodsResponse":
|
||||||
GetRPCMethodsResponse(body, rpc.acsResponse);
|
GetRPCMethodsResponse(body, rpc.acsResponse);
|
||||||
break;
|
break;
|
||||||
case "TransferCompleteResponse":
|
case "TransferCompleteResponse":
|
||||||
TransferCompleteResponse(body, rpc.acsResponse);
|
TransferCompleteResponse(body);
|
||||||
break;
|
break;
|
||||||
case "RequestDownloadResponse":
|
case "RequestDownloadResponse":
|
||||||
RequestDownloadResponse(body, rpc.acsResponse);
|
RequestDownloadResponse(body);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
|
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
|
||||||
@@ -699,7 +738,7 @@ function response(rpc) {
|
|||||||
Reboot(body, rpc.acsRequest);
|
Reboot(body, rpc.acsRequest);
|
||||||
break;
|
break;
|
||||||
case "FactoryReset":
|
case "FactoryReset":
|
||||||
FactoryReset(body, rpc.acsRequest);
|
FactoryReset(body);
|
||||||
break;
|
break;
|
||||||
case "Download":
|
case "Download":
|
||||||
Download(body, rpc.acsRequest);
|
Download(body, rpc.acsRequest);
|
||||||
@@ -716,6 +755,3 @@ function response(rpc) {
|
|||||||
data: env.doc().toString(LIBXMLJS_SAVE_OPTIONS)
|
data: env.doc().toString(LIBXMLJS_SAVE_OPTIONS)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.request = request;
|
|
||||||
exports.response = response;
|
|
||||||
+311
@@ -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;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
@@ -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();
|
||||||
@@ -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
@@ -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
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,44 +16,53 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const NONEXISTENT = new Object();
|
const NONEXISTENT = Symbol();
|
||||||
const UNDEFINED = undefined;
|
const UNDEFINED = undefined;
|
||||||
|
|
||||||
class VersionedMap {
|
interface Revisions<V> {
|
||||||
constructor() {
|
[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._sizeDiff = [0];
|
||||||
this._revision = 0;
|
this._revision = 0;
|
||||||
this.map = new Map();
|
this.map = new Map();
|
||||||
this.dirty = 0;
|
this.dirty = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
public get size(): number {
|
||||||
return this.map.size + this._sizeDiff[this.revision];
|
return this.map.size + this._sizeDiff[this.revision];
|
||||||
}
|
}
|
||||||
|
|
||||||
get revision() {
|
public get revision(): number {
|
||||||
return this._revision;
|
return this._revision;
|
||||||
}
|
}
|
||||||
|
|
||||||
set revision(rev) {
|
public set revision(rev) {
|
||||||
for (let i = this._sizeDiff.length; i <= rev; ++i)
|
for (let i = this._sizeDiff.length; i <= rev; ++i)
|
||||||
this._sizeDiff[i] = this._sizeDiff[i - 1];
|
this._sizeDiff[i] = this._sizeDiff[i - 1];
|
||||||
|
|
||||||
this._revision = rev;
|
this._revision = rev;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key, rev = this._revision) {
|
public get(key: K, rev = this._revision): V {
|
||||||
const revisions = this.map.get(key);
|
const revisions = this.map.get(key);
|
||||||
if (!revisions) return UNDEFINED;
|
if (!revisions) return UNDEFINED;
|
||||||
|
|
||||||
const v = revisions[Math.min(revisions.length - 1, rev)];
|
const v = revisions[Math.min(revisions.length - 1, rev)];
|
||||||
if (v === NONEXISTENT) return UNDEFINED;
|
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);
|
const revisions = this.map.get(key);
|
||||||
if (!revisions) return false;
|
if (!revisions) return false;
|
||||||
|
|
||||||
@@ -62,7 +71,7 @@ class VersionedMap {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key, value, rev = this._revision) {
|
public set(key: K, value: V, rev = this._revision): this {
|
||||||
let revisions = this.map.get(key);
|
let revisions = this.map.get(key);
|
||||||
if (!revisions) {
|
if (!revisions) {
|
||||||
this.dirty |= 1 << rev;
|
this.dirty |= 1 << rev;
|
||||||
@@ -72,7 +81,7 @@ class VersionedMap {
|
|||||||
for (let i = 0; i < rev; ++i) revisions[i] = NONEXISTENT;
|
for (let i = 0; i < rev; ++i) revisions[i] = NONEXISTENT;
|
||||||
revisions[rev] = value;
|
revisions[rev] = value;
|
||||||
this.map.set(key, revisions);
|
this.map.set(key, revisions);
|
||||||
return true;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't modify old revisions
|
// Can't modify old revisions
|
||||||
@@ -86,10 +95,10 @@ class VersionedMap {
|
|||||||
for (let i = revisions.length; i < rev; ++i) revisions[i] = old;
|
for (let i = revisions.length; i < rev; ++i) revisions[i] = old;
|
||||||
revisions[rev] = value;
|
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);
|
const revisions = this.map.get(key);
|
||||||
if (!revisions) return false;
|
if (!revisions) return false;
|
||||||
|
|
||||||
@@ -108,24 +117,24 @@ class VersionedMap {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRevisions(key) {
|
public getRevisions(key: K): Revisions<V> {
|
||||||
const revisions = this.map.get(key);
|
const revisions = this.map.get(key);
|
||||||
if (!revisions) return null;
|
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()) {
|
for (const [i, v] of revisions.entries()) {
|
||||||
if (v === prev) continue;
|
if (v === prev) continue;
|
||||||
if (v === NONEXISTENT) res.delete |= 1 << i;
|
if (v === NONEXISTENT) res.delete |= 1 << i;
|
||||||
else res[i] = v;
|
else res[i] = v as V;
|
||||||
prev = v;
|
prev = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRevisions(key, revisionsObj) {
|
public setRevisions(key: K, revisionsObj: Revisions<V>): void {
|
||||||
const del = revisionsObj.delete || 0;
|
const del = revisionsObj.delete || 0;
|
||||||
const mutations = Object.keys(revisionsObj).reduce(
|
const mutations = Object.keys(revisionsObj).reduce(
|
||||||
(acc, cur) => (cur === "delete" ? acc : acc | (1 << +cur)),
|
(acc, cur) => (cur === "delete" ? acc : acc | (1 << +cur)),
|
||||||
@@ -134,7 +143,7 @@ class VersionedMap {
|
|||||||
|
|
||||||
const revisions = [];
|
const revisions = [];
|
||||||
|
|
||||||
let prev = NONEXISTENT;
|
let prev: V | symbol = NONEXISTENT;
|
||||||
for (let i = 0; mutations >> i; ++i) {
|
for (let i = 0; mutations >> i; ++i) {
|
||||||
let v = prev;
|
let v = prev;
|
||||||
if (del & (1 << i)) v = NONEXISTENT;
|
if (del & (1 << i)) v = NONEXISTENT;
|
||||||
@@ -147,28 +156,28 @@ class VersionedMap {
|
|||||||
this.map.set(key, revisions);
|
this.map.set(key, revisions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDiff(key) {
|
public getDiff(key: K): [V, V] {
|
||||||
const revisions = this.map.get(key);
|
const revisions = this.map.get(key);
|
||||||
if (!revisions) return [UNDEFINED, UNDEFINED];
|
if (!revisions) return [UNDEFINED, UNDEFINED];
|
||||||
let first = revisions[0];
|
let first = revisions[0];
|
||||||
if (first === NONEXISTENT) first = UNDEFINED;
|
if (first === NONEXISTENT) first = UNDEFINED;
|
||||||
let last = revisions[revisions.length - 1];
|
let last = revisions[revisions.length - 1];
|
||||||
if (last === NONEXISTENT) last = UNDEFINED;
|
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) {
|
for (const [key, revisions] of this.map) {
|
||||||
let first = revisions[0];
|
let first = revisions[0];
|
||||||
let last = revisions[revisions.length - 1];
|
let last = revisions[revisions.length - 1];
|
||||||
if ((first === last) === NONEXISTENT) continue;
|
if (first === NONEXISTENT && last === NONEXISTENT) continue;
|
||||||
if (first === NONEXISTENT) first = UNDEFINED;
|
if (first === NONEXISTENT) first = UNDEFINED;
|
||||||
if (last === NONEXISTENT) last = 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;
|
if (this._sizeDiff.length <= revision) return;
|
||||||
|
|
||||||
this._sizeDiff[revision] = this._sizeDiff[this._sizeDiff.length - 1];
|
this._sizeDiff[revision] = this._sizeDiff[this._sizeDiff.length - 1];
|
||||||
@@ -189,5 +198,3 @@ class VersionedMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = VersionedMap;
|
|
||||||
Generated
+9507
-271
File diff suppressed because it is too large.
Load diff
+48
-9
@@ -16,20 +16,54 @@
|
|||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
"directories": {
|
"directories": {
|
||||||
"bin": "bin"
|
"bin": "bin",
|
||||||
|
"tools": "tools"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jsonwebtoken": "^8.4.0",
|
||||||
|
"koa": "^2.7.0",
|
||||||
|
"koa-bodyparser": "^4.2.1",
|
||||||
|
"koa-compress": "^3.0.0",
|
||||||
|
"koa-jwt": "^3.5.1",
|
||||||
|
"koa-router": "^7.4.0",
|
||||||
|
"koa-static": "^5.0.0",
|
||||||
"later": "^1.2.0",
|
"later": "^1.2.0",
|
||||||
"libxmljs": "^0.19.5",
|
"libxmljs": "^0.19.5",
|
||||||
"mongodb": "^3.1.10",
|
"mongodb": "^3.1.13",
|
||||||
|
"parsimmon": "^1.12.0",
|
||||||
"seedrandom": "^2.4.4"
|
"seedrandom": "^2.4.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
|
||||||
"redis": "^2.6.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^5.10.0",
|
"@types/jsonwebtoken": "^8.3.0",
|
||||||
"prettier": "^1.15.3"
|
"@types/koa": "^2.0.48",
|
||||||
|
"@types/koa-compress": "^2.0.8",
|
||||||
|
"@types/koa-router": "^7.0.39",
|
||||||
|
"@types/koa-static": "^4.0.1",
|
||||||
|
"@types/libxmljs": "^0.18.3",
|
||||||
|
"@types/mithril": "^1.1.16",
|
||||||
|
"@types/mongodb": "^3.1.19",
|
||||||
|
"@types/parsimmon": "^1.10.0",
|
||||||
|
"@types/seedrandom": "^2.4.27",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^1.3.0",
|
||||||
|
"ava": "^1.2.1",
|
||||||
|
"codemirror": "^5.43.0",
|
||||||
|
"cssnano": "^4.1.10",
|
||||||
|
"eslint": "^5.14.1",
|
||||||
|
"eslint-config-prettier": "^4.0.0",
|
||||||
|
"esm": "^3.2.5",
|
||||||
|
"mithril": "^1.1.6",
|
||||||
|
"postcss-cssnext": "^3.1.0",
|
||||||
|
"postcss-import": "^12.0.1",
|
||||||
|
"prettier": "^1.16.4",
|
||||||
|
"rollup": "^1.2.1",
|
||||||
|
"rollup-plugin-json": "^3.1.0",
|
||||||
|
"rollup-plugin-replace": "^2.1.0",
|
||||||
|
"rollup-plugin-terser": "^4.0.4",
|
||||||
|
"rollup-plugin-typescript": "^1.0.0",
|
||||||
|
"ts-node": "^8.0.2",
|
||||||
|
"typescript": "^3.3.3",
|
||||||
|
"webpack": "^4.29.5",
|
||||||
|
"yaml": "^1.3.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.14.0"
|
"node": ">=8.14.0"
|
||||||
@@ -38,7 +72,12 @@
|
|||||||
"compile": "echo 'Compile step no longer required, skipping.'",
|
"compile": "echo 'Compile step no longer required, skipping.'",
|
||||||
"configure": "if [ ! -f ./config/config.json ]; then cp ./config/config-sample.json ./config/config.json; fi",
|
"configure": "if [ ! -f ./config/config.json ]; then cp ./config/config-sample.json ./config/config.json; fi",
|
||||||
"install": "npm run configure",
|
"install": "npm run configure",
|
||||||
"test": "node test",
|
"test": "ava --verbose --color",
|
||||||
"lint": "eslint --fix lib/*.js; prettier --write lib/*.js"
|
"lint-ln": "find bin -type f -exec sh -c 'ln -fsr \"$1\" \"bin/.${1#bin/}.ts\"' _ {} \\;; find tools -type f -exec sh -c 'ln -fsr \"$1\" \"tools/.${1#tools/}.ts\"' _ {} \\;",
|
||||||
|
"lint-eslint": "eslint --fix \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"",
|
||||||
|
"lint-prettier": "prettier --parser typescript --write \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"",
|
||||||
|
"lint-tsc": "tsc --noEmit",
|
||||||
|
"lint": "npm run lint-ln && npm run lint-eslint && npm run lint-prettier && npm run lint-tsc",
|
||||||
|
"build": "node -r esm -r ts-node/register/transpile-only build/build"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="310" height="64" viewBox="0 0 82.020832 16.933334"><defs><linearGradient id="b"><stop offset="0" stop-color="#737373"/><stop offset="1" stop-color="#4b4b4b"/></linearGradient><linearGradient id="a"><stop offset="0" stop-color="#b72f5f"/><stop offset="1" stop-color="#872346"/></linearGradient><linearGradient xlink:href="#a" id="c" gradientUnits="userSpaceOnUse" gradientTransform="translate(173.233 -461.003) scale(.21343)" x1="16.045" y1="132.803" x2="16.045" y2="-81.164"/><linearGradient xlink:href="#a" id="d" gradientUnits="userSpaceOnUse" x1="46.459" y1="281.163" x2="46.459" y2="296.66" gradientTransform="translate(1.09 16.9) scale(.94143)"/><linearGradient xlink:href="#b" id="e" gradientUnits="userSpaceOnUse" gradientTransform="translate(11.105 -994.648) scale(3.55816)" x1="46.459" y1="281.163" x2="46.459" y2="296.66"/></defs><path d="M173.233-461.003l1.953-3.714c-.868-1.509-2.042-2.814-3.522-3.917-2.419-1.43-4.386-1.199-5.901.694 2.931 1.323 5.421 3.635 7.47 6.937m-7.577 11.151a6.095 6.095 0 0 0 .673-3.49 23.76 23.76 0 0 0-1.473-6.285 20.998 20.998 0 0 1-1.206-4.855 6.766 6.766 0 0 1 .48-3.426c.463-1.117 1.298-2.074 2.508-2.87-1.302-.392-2.44-.499-3.415-.32-1.707.326-2.82 1.354-3.34 3.083a10.152 10.152 0 0 0-.267 5.08 33.38 33.38 0 0 0 1.238 4.674 32.993 32.993 0 0 1 1.227 4.856c.107.626.118 1.277.032 1.953a4.72 4.72 0 0 1-.779 2.006 4.498 4.498 0 0 1-1.707 1.44c.697.129 1.387.235 2.07.32a4.124 4.124 0 0 0 2.07-.351c.84-.37 1.47-.975 1.89-1.815m22.463 5.123l.63-.62-5.25-4.77 1.579-1.579 4.77 5.25.619-.629-5.24-4.77 1.483-1.483 4.77 5.24.63-.62-5.25-4.77 1.58-1.58 5.015 5.016 1.067-1.045-4.503-4.504h.01l-5.87-5.859-3.563-1.462-5.102 3.831-.02.022-.022-.022.01-.02 3.394-4.526-1.131-.458-4.973 4.973 1.26 3.073 4.652-3.49.01-.01.033.02-.01.012-3.95 5.26.64 1.57 4.814 4.802h-.01l5.559 5.549 1.057-1.046-5.027-5.026 1.569-1.569 4.77 5.24m4.14-28.173c4.405 4.403 6.607 9.722 6.607 15.954 0 6.218-2.202 11.536-6.606 15.954-4.418 4.404-9.736 6.606-15.954 6.606-6.233 0-11.55-2.202-15.955-6.606-2.703-2.71-4.574-5.756-5.613-9.135l.448.245c2.241 1.181 4.226.577 5.955-1.814-1.565-.32-2.82-.658-3.767-1.014a5.467 5.467 0 0 1-1.43-.821c-.96-.762-1.565-1.73-1.814-2.903a11.315 11.315 0 0 1 .032-5.005c.81-4.34 2.874-8.16 6.19-11.461 4.403-4.411 9.721-6.617 15.954-6.617 6.225 0 11.543 2.206 15.954 6.617" fill="url(#c)" transform="matrix(.35176 0 0 -.35176 -53.595 -152.27)"/><path style="line-height:6.61458302px;-inkscape-font-specification:Norwegian" d="M47.104 281.834c-.456 0-.816.388-.816.849 0 .46.355.816.816.816a.82.82 0 0 0 .833-.816.85.85 0 0 0-.833-.849zm-21.502 1.404c-.205 0-.382.046-.5.13-.118.085-.187.201-.187.4v.981l-.151-.031a9.89 9.89 0 0 0-1.872-.173c-1.288 0-2.148.066-2.657.374a1.264 1.264 0 0 0-.548.676c-.119.313-.177.73-.177 1.28v6.123c0 .982.25 1.508.787 1.824.539.315 1.407.41 2.628.41 1.308 0 2.178-.13 2.701-.484.524-.354.745-.932.745-1.942 0-.821-.147-1.37-.543-1.722-.395-.352-1.075-.528-2.186-.528h-2.787v-1.234l.148.03c.478.092 1.094.11 1.89.11 1.292 0 2.148-.112 2.658-.453.51-.34.724-.915.724-1.958v-3.284c0-.198-.07-.314-.186-.399a.843.843 0 0 0-.487-.13zm6.112 1.29c-1.226 0-2.038.137-2.526.495-.488.357-.697.945-.697 1.947v2.84c0 .935.23 1.54.743 1.932.512.39 1.337.571 2.544.571 1.392 0 2.17-.058 2.567-.196.199-.069.298-.15.355-.246.058-.097.078-.227.078-.405 0-.208-.04-.378-.117-.485-.076-.108-.186-.171-.396-.171-.794 0-1.057.19-2.408.19-.593 0-1.06-.078-1.395-.255-.335-.177-.53-.473-.53-.84v-.89h4.301a.717.717 0 0 0 .539-.213c.13-.138.213-.342.213-.62v-1.212c0-1.037-.179-1.623-.654-1.97-.476-.348-1.3-.472-2.617-.472zm8.83 0c-1.303 0-2.117.086-2.59.49-.472.403-.665 1.168-.665 2.623v4.002c0 .215.074.338.197.426.123.088.31.134.524.134.223 0 .412-.046.537-.134.125-.088.198-.211.198-.426v-3.81c0-.424.006-.754.04-1.019.036-.265.102-.47.236-.623.135-.152.328-.236.574-.28.246-.044.552-.053.949-.053.4 0 .712.01.963.053.25.044.447.126.587.278.139.151.211.358.25.623.038.265.046.597.046 1.02v3.81c0 .217.07.34.19.427.122.088.308.134.53.134.215 0 .401-.046.525-.134.123-.088.196-.211.196-.426v-4.002c0-1.455-.201-2.219-.682-2.623-.48-.404-1.303-.49-2.605-.49zm12.996 0c-1.227 0-2.038.137-2.527.495-.488.357-.696.945-.696 1.947v2.84c0 .935.23 1.54.742 1.932.513.39 1.338.571 2.545.571 1.392 0 2.17-.058 2.567-.196.2-.069.298-.15.355-.246.058-.097.079-.227.079-.405 0-.208-.04-.378-.117-.485-.077-.108-.188-.171-.398-.171-.793 0-1.056.19-2.406.19-.594 0-1.062-.078-1.396-.255-.335-.177-.53-.473-.53-.84v-.89h4.301a.716.716 0 0 0 .539-.213c.13-.138.213-.342.213-.62v-1.212c0-1.037-.18-1.623-.655-1.97-.475-.348-1.3-.472-2.616-.472zm-6.436.13c-.212 0-.398.047-.522.14-.124.09-.198.22-.198.435v6.41c0 .215.073.338.196.426.123.088.31.134.524.134.223 0 .414-.046.54-.134.124-.088.198-.211.198-.426v-6.41c0-.215-.075-.342-.201-.434-.126-.092-.316-.142-.537-.142zm-24.212 1.135c.662 0 1.4.065 1.918.146l.105.016v.92c0 .246-.01.453-.058.632a.757.757 0 0 1-Line truncated
|
||||||
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,70 @@
|
|||||||
|
import ava from "ava";
|
||||||
|
import * as common from "../lib/common";
|
||||||
|
|
||||||
|
ava("typeOf", t => {
|
||||||
|
const cases = [
|
||||||
|
[undefined, common.UNDEFINED_TYPE],
|
||||||
|
[null, common.NULL_TYPE],
|
||||||
|
[true, common.BOOLEAN_TYPE],
|
||||||
|
[false, common.BOOLEAN_TYPE],
|
||||||
|
[0, common.NUMBER_TYPE],
|
||||||
|
[1, common.NUMBER_TYPE],
|
||||||
|
[-1, common.NUMBER_TYPE],
|
||||||
|
[0.0, common.NUMBER_TYPE],
|
||||||
|
[1.0, common.NUMBER_TYPE],
|
||||||
|
[1.1, common.NUMBER_TYPE],
|
||||||
|
[-1.0, common.NUMBER_TYPE],
|
||||||
|
[-1.1, common.NUMBER_TYPE],
|
||||||
|
["", common.STRING_TYPE],
|
||||||
|
["TEST", common.STRING_TYPE],
|
||||||
|
[[], common.ARRAY_TYPE],
|
||||||
|
[[1, 2, 3], common.ARRAY_TYPE],
|
||||||
|
[{}, common.OBJECT_TYPE],
|
||||||
|
[{ test: 1 }, common.OBJECT_TYPE],
|
||||||
|
[/[^A-Za-z0-9_]/g, common.REGEXP_TYPE],
|
||||||
|
[new RegExp("//"), common.REGEXP_TYPE],
|
||||||
|
[new Date(), common.DATE_TYPE]
|
||||||
|
];
|
||||||
|
t.plan(cases.length);
|
||||||
|
for (const c of cases) t.is(common.typeOf(c[0]), c[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
ava("generateDeviceId", t => {
|
||||||
|
const space = [" ", "%20"];
|
||||||
|
const special = [";", "%3B"];
|
||||||
|
const cases = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ProductClass: "TestProductClass",
|
||||||
|
OUI: "TestOUI",
|
||||||
|
SerialNumber: "TestSerialNumber"
|
||||||
|
},
|
||||||
|
"TestOUI-TestProductClass-TestSerialNumber"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
OUI: "TestOUI",
|
||||||
|
SerialNumber: "TestSerialNumber"
|
||||||
|
},
|
||||||
|
"TestOUI-TestSerialNumber"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
OUI: `TestOUIWith${space[0]}_${special[0]}2912`,
|
||||||
|
SerialNumber: `TestSerialNumberWith${space[0]}_${special[0]}2912`
|
||||||
|
},
|
||||||
|
`TestOUIWith${space[1]}_${special[1]}2912-TestSerialNumberWith${
|
||||||
|
space[1]
|
||||||
|
}_${special[1]}2912`
|
||||||
|
]
|
||||||
|
];
|
||||||
|
t.plan(cases.length);
|
||||||
|
for (const c of cases) t.is(common.generateDeviceId(c[0]), c[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
ava("escapeRegExp", t => {
|
||||||
|
t.is(
|
||||||
|
common.escapeRegExp("\\ ^ $ * + ? . ( ) | { } [ ]"),
|
||||||
|
"\\\\ \\^ \\$ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]"
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import ava from "ava";
|
||||||
|
import Path from "../lib/common/path";
|
||||||
|
import * as device from "../lib/device";
|
||||||
|
|
||||||
|
ava("getAliasDeclarations", t => {
|
||||||
|
const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b");
|
||||||
|
const decs = device.getAliasDeclarations(path, 99);
|
||||||
|
|
||||||
|
const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"];
|
||||||
|
|
||||||
|
t.plan(24);
|
||||||
|
for (const [i, d] of decs.entries()) {
|
||||||
|
t.is(d.path.toString(), expected[i]);
|
||||||
|
t.is(d.pathGet, 99);
|
||||||
|
t.is(d.pathSet, null);
|
||||||
|
t.deepEqual(d.attrGet, i ? { value: 99 } : null);
|
||||||
|
t.is(d.attrSet, null);
|
||||||
|
t.is(d.defer, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import ava from "ava";
|
||||||
|
import * as mongodbFunctions from "../lib/mongodb-functions";
|
||||||
|
import { stringify } from "../lib/common/expression-parser";
|
||||||
|
|
||||||
|
ava("mongoQueryToFilter", t => {
|
||||||
|
const tests = [
|
||||||
|
[{}, "true"],
|
||||||
|
[{ test: "test" }, 'test = "test"'],
|
||||||
|
[{ test: { $eq: "test" } }, 'test = "test"'],
|
||||||
|
[{ test: { $ne: "test" } }, 'test <> "test"'],
|
||||||
|
[{ test: { $gte: "test" } }, 'test >= "test"'],
|
||||||
|
[{ "Tags.test": true }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ "Tags.test": false }, "Tags.test IS NULL"],
|
||||||
|
[{ "Tags.test": { $exists: true } }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ "Tags.test": { $exists: false } }, "Tags.test IS NULL"],
|
||||||
|
[{ "Tags.test": { $ne: true } }, "Tags.test IS NULL"],
|
||||||
|
[{ "Tags.test": { $ne: false } }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ "Tags.test": { $eq: true } }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ "Tags.test": { $eq: false } }, "Tags.test IS NULL"],
|
||||||
|
[{ _tags: "test" }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ _tags: { $ne: "test" } }, "Tags.test IS NULL"],
|
||||||
|
[{ _tags: { $eq: "test" } }, "Tags.test IS NOT NULL"],
|
||||||
|
[
|
||||||
|
{ $and: [{ test: "test" }, { test: { $ne: "test" } }] },
|
||||||
|
'test = "test" AND test <> "test"'
|
||||||
|
],
|
||||||
|
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
|
||||||
|
[
|
||||||
|
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
|
||||||
|
'test = "test" OR test <> "test"'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ test: { $gte: "test1", $ne: "test2" } },
|
||||||
|
'test >= "test1" AND test <> "test2"'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
const shouldFailTests = [
|
||||||
|
[{ test: { $gee: "test" } }, "Operator $gee not supported"],
|
||||||
|
[{ test: [] }, "Invalid type"],
|
||||||
|
[{ "Tags.test": { $gt: true } }, "Invalid tag query"],
|
||||||
|
[{ _tags: [] }, "Invalid type"],
|
||||||
|
[{ _tags: { $gt: "test" } }, "Invalid tag query"],
|
||||||
|
[{ $nor: [] }, "Operator $nor not supported"]
|
||||||
|
];
|
||||||
|
|
||||||
|
t.plan(tests.length + 2 * shouldFailTests.length);
|
||||||
|
for (const test of tests)
|
||||||
|
t.is(stringify(mongodbFunctions.mongoQueryToFilter(test[0])), test[1]);
|
||||||
|
|
||||||
|
for (const test of shouldFailTests) {
|
||||||
|
const func = (): void => {
|
||||||
|
mongodbFunctions.mongoQueryToFilter(test[0]);
|
||||||
|
};
|
||||||
|
const error = t.throws(func, Error);
|
||||||
|
t.is(error.message, test[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import ava from "ava";
|
||||||
|
import Path from "../lib/common/path";
|
||||||
|
|
||||||
|
ava("toString", t => {
|
||||||
|
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
|
||||||
|
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
|
||||||
|
t.is(path1.toString(), path2.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
ava("slice", t => {
|
||||||
|
const path = Path.parse("a.*.b.[x:y].c");
|
||||||
|
const sliced = path.slice(1, -1);
|
||||||
|
t.is(sliced.toString(), '*.b.[x:"y"]');
|
||||||
|
t.is(sliced.alias, 0b100);
|
||||||
|
t.is(sliced.wildcard, 0b1);
|
||||||
|
});
|
||||||
|
|
||||||
|
ava("concat", t => {
|
||||||
|
const path1 = Path.parse("a");
|
||||||
|
const path2 = Path.parse("*.[a:b]");
|
||||||
|
const concat = path1.concat(path2);
|
||||||
|
t.is(concat.toString(), 'a.*.[a:"b"]');
|
||||||
|
t.is(concat.alias, 0b100);
|
||||||
|
t.is(concat.wildcard, 0b10);
|
||||||
|
});
|
||||||
Executable
+232
@@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||||
|
import { MongoClient } from "mongodb";
|
||||||
|
import { get } from "../lib/config";
|
||||||
|
import { hashPassword, generateSalt } from "../lib/auth";
|
||||||
|
|
||||||
|
const permissionsData = [
|
||||||
|
{ role: "admin", resource: "devices", access: 3, validate: ["pass"] },
|
||||||
|
{ role: "admin", resource: "faults", access: 3, validate: ["pass"] },
|
||||||
|
{ role: "admin", resource: "files", access: 3, validate: ["pass"] },
|
||||||
|
{ role: "admin", resource: "presets", access: 3, validate: ["pass"] },
|
||||||
|
{ role: "admin", resource: "provisions", access: 3, validate: ["pass"] },
|
||||||
|
{ role: "admin", resource: "config", access: 3, validate: ["pass"] },
|
||||||
|
{ role: "admin", resource: "permissions", access: 3, validate: ["pass"] },
|
||||||
|
{ role: "admin", resource: "users", access: 3, validate: ["pass"] },
|
||||||
|
{
|
||||||
|
role: "admin",
|
||||||
|
resource: "virtualParameters",
|
||||||
|
access: 3,
|
||||||
|
validate: ["pass"]
|
||||||
|
},
|
||||||
|
{ role: "view", resource: "devices", access: 2, validate: [] },
|
||||||
|
{ role: "view", resource: "faults", access: 2, validate: [] },
|
||||||
|
{ role: "view", resource: "files", access: 2, validate: [] },
|
||||||
|
{ role: "view", resource: "presets", access: 2, validate: [] },
|
||||||
|
{ role: "view", resource: "provisions", access: 2, validate: [] },
|
||||||
|
{ role: "view", resource: "config", access: 2, validate: [] },
|
||||||
|
{ role: "view", resource: "users", access: 2, validate: [] },
|
||||||
|
{ role: "view", resource: "permissions", access: 2, validate: [] },
|
||||||
|
{ role: "view", resource: "virtualParameters", access: 2, validate: [] }
|
||||||
|
];
|
||||||
|
|
||||||
|
const usersData = [
|
||||||
|
{ username: "admin", password: "admin", roles: ["admin"] },
|
||||||
|
{ username: "view", password: "view", roles: ["view"] }
|
||||||
|
];
|
||||||
|
|
||||||
|
const configData = {
|
||||||
|
"ui.device.0.type": "'tags'",
|
||||||
|
"ui.device.1.type": "'ping'",
|
||||||
|
"ui.device.2.parameters.0.components.0.type": "'parameter'",
|
||||||
|
"ui.device.2.parameters.0.components.1.chart": "'online'",
|
||||||
|
"ui.device.2.parameters.0.components.1.type": "'overview-dot'",
|
||||||
|
"ui.device.2.parameters.0.components.2.parameters.0":
|
||||||
|
"InternetGatewayDevice.DeviceInfo.HardwareVersion",
|
||||||
|
"ui.device.2.parameters.0.components.2.parameters.1":
|
||||||
|
"InternetGatewayDevice.DeviceInfo.SoftwareVersion",
|
||||||
|
"ui.device.2.parameters.0.components.2.parameters.2":
|
||||||
|
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress",
|
||||||
|
"ui.device.2.parameters.0.components.2.parameters.3":
|
||||||
|
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress",
|
||||||
|
"ui.device.2.parameters.0.components.2.parameters.4":
|
||||||
|
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
|
||||||
|
"ui.device.2.parameters.0.components.2.parameters.5":
|
||||||
|
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase",
|
||||||
|
"ui.device.2.parameters.0.components.2.parameters.6":
|
||||||
|
"InternetGatewayDevice.LANDevice.1.Hosts.Host",
|
||||||
|
"ui.device.2.parameters.0.components.2.type": "'summon-button'",
|
||||||
|
"ui.device.2.parameters.0.element": "'span.inform'",
|
||||||
|
"ui.device.2.parameters.0.label": "'Last inform'",
|
||||||
|
"ui.device.2.parameters.0.parameter": "DATE_STRING(Events.Inform)",
|
||||||
|
"ui.device.2.parameters.0.type": "'container'",
|
||||||
|
"ui.device.2.parameters.1.label": "'Serial number'",
|
||||||
|
"ui.device.2.parameters.1.parameter":
|
||||||
|
"InternetGatewayDevice.DeviceInfo.SerialNumber",
|
||||||
|
"ui.device.2.parameters.10.label": "'WLAN passphrase'",
|
||||||
|
"ui.device.2.parameters.10.parameter":
|
||||||
|
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase",
|
||||||
|
"ui.device.2.parameters.2.label": "'Product class'",
|
||||||
|
"ui.device.2.parameters.2.parameter":
|
||||||
|
"InternetGatewayDevice.DeviceInfo.ProductClass",
|
||||||
|
"ui.device.2.parameters.3.label": "'OUI'",
|
||||||
|
"ui.device.2.parameters.3.parameter":
|
||||||
|
"InternetGatewayDevice.DeviceInfo.ManufacturerOUI",
|
||||||
|
"ui.device.2.parameters.4.label": "'Manufacturer'",
|
||||||
|
"ui.device.2.parameters.4.parameter":
|
||||||
|
"InternetGatewayDevice.DeviceInfo.Manufacturer",
|
||||||
|
"ui.device.2.parameters.5.label": "'Hardware version'",
|
||||||
|
"ui.device.2.parameters.5.parameter":
|
||||||
|
"InternetGatewayDevice.DeviceInfo.HardwareVersion",
|
||||||
|
"ui.device.2.parameters.6.label": "'Software version'",
|
||||||
|
"ui.device.2.parameters.6.parameter":
|
||||||
|
"InternetGatewayDevice.DeviceInfo.SoftwareVersion",
|
||||||
|
"ui.device.2.parameters.7.label": "'MAC'",
|
||||||
|
"ui.device.2.parameters.7.parameter":
|
||||||
|
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress",
|
||||||
|
"ui.device.2.parameters.8.label": "'IP'",
|
||||||
|
"ui.device.2.parameters.8.parameter":
|
||||||
|
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress",
|
||||||
|
"ui.device.2.parameters.9.label": "'WLAN SSID'",
|
||||||
|
"ui.device.2.parameters.9.parameter":
|
||||||
|
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
|
||||||
|
"ui.device.2.type": "'parameter-list'",
|
||||||
|
"ui.device.3.childParameters.0.label": "'Host name'",
|
||||||
|
"ui.device.3.childParameters.0.parameter": "HostName",
|
||||||
|
"ui.device.3.childParameters.1.label": "'IP address'",
|
||||||
|
"ui.device.3.childParameters.1.parameter": "IPAddress",
|
||||||
|
"ui.device.3.childParameters.2.label": "'MAC address'",
|
||||||
|
"ui.device.3.childParameters.2.parameter": "MACAddress",
|
||||||
|
"ui.device.3.label": "'WLAN hosts'",
|
||||||
|
"ui.device.3.parameter": "InternetGatewayDevice.LANDevice.1.Hosts.Host",
|
||||||
|
"ui.device.3.type": "'parameter-table'",
|
||||||
|
"ui.device.4.components.0.components.0": "'Faults'",
|
||||||
|
"ui.device.4.components.0.element": "'h3'",
|
||||||
|
"ui.device.4.components.0.type": "'container'",
|
||||||
|
"ui.device.4.components.1.type": "'device-faults'",
|
||||||
|
"ui.device.4.element": "'div'",
|
||||||
|
"ui.device.4.type": "'container'",
|
||||||
|
"ui.device.5.components.0.components.0": "'All parameters'",
|
||||||
|
"ui.device.5.components.0.element": "'h3'",
|
||||||
|
"ui.device.5.components.0.type": "'container'",
|
||||||
|
"ui.device.5.components.1.type": "'all-parameters'",
|
||||||
|
"ui.device.5.element": "'div.container-full-width'",
|
||||||
|
"ui.device.5.type": "'container'",
|
||||||
|
"ui.device.6.type": "'device-actions'",
|
||||||
|
"ui.filters.0.label": "'Serial number'",
|
||||||
|
"ui.filters.0.parameter": "DeviceID.SerialNumber",
|
||||||
|
"ui.filters.0.type": "'string'",
|
||||||
|
"ui.filters.1.label": "'Product class'",
|
||||||
|
"ui.filters.1.parameter": "DeviceID.ProductClass",
|
||||||
|
"ui.filters.1.type": "'string'",
|
||||||
|
"ui.filters.2.label": "'Tag'",
|
||||||
|
"ui.filters.2.parameter": "tag",
|
||||||
|
"ui.filters.2.type": "'string'",
|
||||||
|
"ui.index.0.components.0.type": "'parameter'",
|
||||||
|
"ui.index.0.label": "'Serial number'",
|
||||||
|
"ui.index.0.parameter": "DeviceID.SerialNumber",
|
||||||
|
"ui.index.0.type": "'device-link'",
|
||||||
|
"ui.index.1.label": "'Product class'",
|
||||||
|
"ui.index.1.parameter": "DeviceID.ProductClass",
|
||||||
|
"ui.index.2.label": "'Software version'",
|
||||||
|
"ui.index.2.parameter": "InternetGatewayDevice.DeviceInfo.SoftwareVersion",
|
||||||
|
"ui.index.3.label": "'IP'",
|
||||||
|
"ui.index.3.parameter":
|
||||||
|
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress",
|
||||||
|
"ui.index.4.label": "'SSID'",
|
||||||
|
"ui.index.4.parameter":
|
||||||
|
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
|
||||||
|
"ui.index.5.components.0.type": "'parameter'",
|
||||||
|
"ui.index.5.components.1.chart": "'online'",
|
||||||
|
"ui.index.5.components.1.type": "'overview-dot'",
|
||||||
|
"ui.index.5.element": "'span.inform'",
|
||||||
|
"ui.index.5.label": "'Last inform'",
|
||||||
|
"ui.index.5.parameter": "DATE_STRING(Events.Inform)",
|
||||||
|
"ui.index.5.type": "'container'",
|
||||||
|
"ui.index.6.label": "'Tags'",
|
||||||
|
"ui.index.6.parameter": "Tags",
|
||||||
|
"ui.index.6.type": "'tags'",
|
||||||
|
"ui.index.6.unsortable": "true",
|
||||||
|
"ui.index.6.writable": "false",
|
||||||
|
"ui.overview.charts.online.label": "'Online status'",
|
||||||
|
"ui.overview.charts.online.slices.1_onlineNow.color": "'#31a354'",
|
||||||
|
"ui.overview.charts.online.slices.1_onlineNow.filter":
|
||||||
|
"Events.Inform > NOW() - 5 * 60 * 1000",
|
||||||
|
"ui.overview.charts.online.slices.1_onlineNow.label": "'Online now'",
|
||||||
|
"ui.overview.charts.online.slices.2_past24.color": "'#addd8e'",
|
||||||
|
"ui.overview.charts.online.slices.2_past24.filter":
|
||||||
|
"Events.Inform > (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000) AND Events.Inform < (NOW() - 5 * 60 * 1000)",
|
||||||
|
"ui.overview.charts.online.slices.2_past24.label": "'Past 24 hours'",
|
||||||
|
"ui.overview.charts.online.slices.3_others.color": "'#f7fcb9'",
|
||||||
|
"ui.overview.charts.online.slices.3_others.filter":
|
||||||
|
"Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)",
|
||||||
|
"ui.overview.charts.online.slices.3_others.label": "'Others'",
|
||||||
|
"ui.overview.groups.online.charts.0": "'online'",
|
||||||
|
"ui.overview.groups.online.label": "''",
|
||||||
|
"ui.pageSize": "10"
|
||||||
|
};
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
const client = await MongoClient.connect(get("MONGODB_CONNECTION_URL"), {
|
||||||
|
useNewUrlParser: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletes = [];
|
||||||
|
for (const col of ["permissions", "users", "config"]) {
|
||||||
|
deletes.push(
|
||||||
|
client
|
||||||
|
.db()
|
||||||
|
.collection(col)
|
||||||
|
.deleteMany({})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(deletes);
|
||||||
|
|
||||||
|
const inserts = [];
|
||||||
|
const objs = Object.entries(configData).map(c => ({
|
||||||
|
_id: c[0],
|
||||||
|
value: c[1]
|
||||||
|
}));
|
||||||
|
inserts.push(
|
||||||
|
client
|
||||||
|
.db()
|
||||||
|
.collection("config")
|
||||||
|
.insertMany(objs)
|
||||||
|
);
|
||||||
|
const perms = permissionsData.map(p => {
|
||||||
|
p.validate = (p.validate || []).join(",");
|
||||||
|
return Object.assign({ _id: `${p.role}:${p.resource}:${p.access}` }, p);
|
||||||
|
});
|
||||||
|
inserts.push(
|
||||||
|
client
|
||||||
|
.db()
|
||||||
|
.collection("permissions")
|
||||||
|
.insertMany(perms)
|
||||||
|
);
|
||||||
|
const saltingProms = [];
|
||||||
|
for (const u of usersData) {
|
||||||
|
saltingProms.push(
|
||||||
|
new Promise(async resolve => {
|
||||||
|
u.salt = await generateSalt(64);
|
||||||
|
u.password = await hashPassword(u.password, u.salt);
|
||||||
|
u.roles = (u.roles || []).join(",");
|
||||||
|
u._id = u.username;
|
||||||
|
delete u.username;
|
||||||
|
resolve(u);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await Promise.all(saltingProms);
|
||||||
|
inserts.push(
|
||||||
|
client
|
||||||
|
.db()
|
||||||
|
.collection("users")
|
||||||
|
.insertMany(users)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(inserts);
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
+35
-43
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||||
/**
|
/**
|
||||||
* Copyright 2013-2017 Zaid Abdulla
|
* Copyright 2013-2017 Zaid Abdulla
|
||||||
*
|
*
|
||||||
@@ -17,75 +17,67 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const fs = require("fs");
|
import * as fs from "fs";
|
||||||
const path = require("path");
|
import * as path from "path";
|
||||||
|
import { connect, devicesCollection } from "../lib/db";
|
||||||
|
|
||||||
const outputFilename = path.resolve(`data_model_${process.argv[2]}.json`);
|
const outputFilename = path.resolve(`data_model_${process.argv[2]}.json`);
|
||||||
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
|
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
|
||||||
|
|
||||||
const db = require("../lib/db");
|
|
||||||
|
|
||||||
const dataModel = {};
|
const dataModel = {};
|
||||||
|
|
||||||
|
function recursive(object, p): void {
|
||||||
|
if (!p) p = "";
|
||||||
|
|
||||||
function recursive(object, path) {
|
for (const key in object) {
|
||||||
if (!path)
|
if (key[0] === "_" || !object[key]) continue;
|
||||||
path = "";
|
|
||||||
|
|
||||||
for (let key in object) {
|
const isObject = object[key]["_object"];
|
||||||
if (key[0] === "_" || !object[key])
|
const isValue = "_value" in object[key];
|
||||||
continue;
|
|
||||||
|
|
||||||
let isObject = object[key]["_object"];
|
|
||||||
let isValue = "_value" in object[key];
|
|
||||||
|
|
||||||
if ((isObject && isValue) || (!isObject && !isValue))
|
if ((isObject && isValue) || (!isObject && !isValue))
|
||||||
throw new Error("Unexpected object");
|
throw new Error("Unexpected object");
|
||||||
|
|
||||||
if (isValue) {
|
if (isValue) {
|
||||||
let value = object[key]["_value"];
|
let value = object[key]["_value"];
|
||||||
if (typeof value === "boolean")
|
if (typeof value === "boolean") value = +value;
|
||||||
value = +value;
|
|
||||||
|
|
||||||
let values = [object[key]["_writable"] || false, value.toString()];
|
const values = [object[key]["_writable"] || false, value.toString()];
|
||||||
if (object[key]["_type"])
|
if (object[key]["_type"]) values.push(object[key]["_type"]);
|
||||||
values.push(object[key]["_type"]);
|
|
||||||
|
|
||||||
dataModel[`${path}${key}`] = values;
|
dataModel[`${p}${key}`] = values;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isObject) {
|
if (isObject) {
|
||||||
let values = [object[key]["_writable"] || false];
|
const values = [object[key]["_writable"] || false];
|
||||||
dataModel[`${path}${key}.`] = values;
|
dataModel[`${p}${key}.`] = values;
|
||||||
recursive(object[key], `${path}${key}.`);
|
recursive(object[key], `${p}${key}.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connect(err => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
db.connect(function(err) {
|
return devicesCollection.findOne(
|
||||||
if (err)
|
{
|
||||||
throw err;
|
_id: process.argv[2]
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
(err, device) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
return db.devicesCollection.findOne({
|
if (!device) throw new Error("Device not found");
|
||||||
_id: process.argv[2]
|
|
||||||
}, {}, function(err, device) {
|
|
||||||
if (err)
|
|
||||||
throw err;
|
|
||||||
|
|
||||||
if (!device)
|
recursive(device);
|
||||||
throw new Error("Device not found");
|
|
||||||
|
|
||||||
recursive(device);
|
fs.writeFile(outputFilename, JSON.stringify(dataModel, null, 2), () => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
fs.writeFile(outputFilename, JSON.stringify(dataModel, null, 2), function() {
|
process.stdout.write(`${outputFilename} saved\n`);
|
||||||
if (err)
|
process.exit(0);
|
||||||
throw err;
|
});
|
||||||
|
}
|
||||||
console.log(`${outputFilename} saved`);
|
);
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"esnext", "dom"
|
||||||
|
],
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "esnext",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./bin/.*.ts",
|
||||||
|
"./lib/**/*",
|
||||||
|
"./ui/**/*",
|
||||||
|
"./test/**/*",
|
||||||
|
"./build/**/*",
|
||||||
|
"./toos/.*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import m from "mithril";
|
||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const active = { [vnode.attrs["page"]]: "active" };
|
||||||
|
const tabs = [];
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("presets", 1)) {
|
||||||
|
tabs.push(
|
||||||
|
m(
|
||||||
|
"li",
|
||||||
|
{ class: active["presets"] },
|
||||||
|
m("a", { href: "#!/admin/presets" }, "Presets")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("provisions", 1)) {
|
||||||
|
tabs.push(
|
||||||
|
m(
|
||||||
|
"li",
|
||||||
|
{ class: active["provisions"] },
|
||||||
|
m("a", { href: "#!/admin/provisions" }, "Provisions")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("virtualParameters", 1)) {
|
||||||
|
tabs.push(
|
||||||
|
m(
|
||||||
|
"li",
|
||||||
|
{ class: active["virtualParameters"] },
|
||||||
|
m("a", { href: "#!/admin/virtualParameters" }, "Virtual Parameters")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("files", 1)) {
|
||||||
|
tabs.push(
|
||||||
|
m(
|
||||||
|
"li",
|
||||||
|
{ class: active["files"] },
|
||||||
|
m("a", { href: "#!/admin/files" }, "Files")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("config", 1)) {
|
||||||
|
tabs.push(
|
||||||
|
m(
|
||||||
|
"li",
|
||||||
|
{ class: active["config"] },
|
||||||
|
m("a", { href: "#!/admin/config" }, "Config")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("permissions", 1)) {
|
||||||
|
tabs.push(
|
||||||
|
m(
|
||||||
|
"li",
|
||||||
|
{ class: active["permissions"] },
|
||||||
|
m("a", { href: "#!/admin/permissions" }, "Permissions")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("users", 1)) {
|
||||||
|
tabs.push(
|
||||||
|
m(
|
||||||
|
"li",
|
||||||
|
{ class: active["users"] },
|
||||||
|
m("a", { href: "#!/admin/users" }, "Users")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m("nav#side-menu", m("ul", tabs));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import m, { RouteResolver } from "mithril";
|
||||||
|
import layout from "./layout";
|
||||||
|
import * as store from "./store";
|
||||||
|
import * as loginPage from "./login-page";
|
||||||
|
import * as overviewPage from "./overview-page";
|
||||||
|
import * as devicesPage from "./devices-page";
|
||||||
|
import * as devicePage from "./device-page";
|
||||||
|
import * as errorPage from "./error-page";
|
||||||
|
import * as faultsPage from "./faults-page";
|
||||||
|
import * as presetsPage from "./presets-page";
|
||||||
|
import * as provisionsPage from "./provisions-page";
|
||||||
|
import * as virtualParametersPage from "./virtual-parameters-page";
|
||||||
|
import * as filesPage from "./files-page";
|
||||||
|
import * as configPage from "./config-page";
|
||||||
|
import * as permissionsPage from "./permissions-page";
|
||||||
|
import * as usersPage from "./users-page";
|
||||||
|
import Authorizer from "../lib//common/authorizer";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
import { contextifyComponent } from "./components";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
authorizer: Authorizer;
|
||||||
|
permissionSets: any;
|
||||||
|
username: string;
|
||||||
|
clientConfig: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.authorizer = new Authorizer(window.permissionSets);
|
||||||
|
|
||||||
|
const adminPages = [
|
||||||
|
"presets",
|
||||||
|
"provisions",
|
||||||
|
"virtualParameters",
|
||||||
|
"files",
|
||||||
|
"config",
|
||||||
|
"users",
|
||||||
|
"permissions"
|
||||||
|
];
|
||||||
|
|
||||||
|
let state;
|
||||||
|
|
||||||
|
let lastRenderTimestamp = 0;
|
||||||
|
let pageVisitTimestamp = 0;
|
||||||
|
let fulfillTimeout = null;
|
||||||
|
|
||||||
|
function fulfill(): void {
|
||||||
|
clearTimeout(fulfillTimeout);
|
||||||
|
fulfillTimeout = setTimeout(() => {
|
||||||
|
store.fulfill(lastRenderTimestamp, pageVisitTimestamp).then(updated => {
|
||||||
|
if (updated) m.redraw();
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pagify(pageName, page): RouteResolver {
|
||||||
|
const component: RouteResolver = {
|
||||||
|
render: () => {
|
||||||
|
lastRenderTimestamp = Date.now();
|
||||||
|
let p;
|
||||||
|
if (state && state.error) p = m(errorPage.component, state);
|
||||||
|
else p = m(contextifyComponent(page.component), state);
|
||||||
|
fulfill();
|
||||||
|
const attrs = {};
|
||||||
|
attrs["page"] = pageName;
|
||||||
|
return m(layout, attrs, p);
|
||||||
|
},
|
||||||
|
onmatch: null
|
||||||
|
};
|
||||||
|
|
||||||
|
component.onmatch = (args, requestedPath) => {
|
||||||
|
pageVisitTimestamp = Date.now();
|
||||||
|
if (!page.init) {
|
||||||
|
state = null;
|
||||||
|
fulfill();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
page
|
||||||
|
.init(args)
|
||||||
|
.then(st => {
|
||||||
|
if (!st) return void m.route.set("/");
|
||||||
|
state = st;
|
||||||
|
resolve();
|
||||||
|
fulfill();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (!window.username && err.message.indexOf("authorized") >= 0) {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
m.route.set("/login", { continue: requestedPath });
|
||||||
|
}
|
||||||
|
state = { error: err.message };
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectAdminPage(): RouteResolver {
|
||||||
|
const component: RouteResolver = {
|
||||||
|
onmatch: () => {
|
||||||
|
for (const page of adminPages) {
|
||||||
|
if (window.authorizer.hasAccess(page, 2)) {
|
||||||
|
m.route.set(`/admin/${page}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
m.route(document.body, "/overview", {
|
||||||
|
"/login": pagify("login", loginPage),
|
||||||
|
"/overview": pagify("overview", overviewPage),
|
||||||
|
"/devices": pagify("devices", devicesPage),
|
||||||
|
"/devices/:id": pagify("devices", devicePage),
|
||||||
|
"/faults": pagify("faults", faultsPage),
|
||||||
|
"/admin": redirectAdminPage(),
|
||||||
|
"/admin/presets": pagify("presets", presetsPage),
|
||||||
|
"/admin/provisions": pagify("provisions", provisionsPage),
|
||||||
|
"/admin/virtualParameters": pagify(
|
||||||
|
"virtualParameters",
|
||||||
|
virtualParametersPage
|
||||||
|
),
|
||||||
|
"/admin/files": pagify("files", filesPage),
|
||||||
|
"/admin/config": pagify("config", configPage),
|
||||||
|
"/admin/users": pagify("users", usersPage),
|
||||||
|
"/admin/permissions": pagify("permissions", permissionsPage)
|
||||||
|
});
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
export default class Autocomplete {
|
||||||
|
private callback: (
|
||||||
|
value: string,
|
||||||
|
callback: (suggestions: string[]) => void
|
||||||
|
) => void;
|
||||||
|
private element: HTMLInputElement;
|
||||||
|
private hideTimeout: NodeJS.Timeout;
|
||||||
|
private visible: boolean;
|
||||||
|
private default: string;
|
||||||
|
private selection: number;
|
||||||
|
private container: HTMLElement;
|
||||||
|
|
||||||
|
public constructor(className, callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.element = null;
|
||||||
|
this.hideTimeout = null;
|
||||||
|
this.visible = false;
|
||||||
|
this.default = null;
|
||||||
|
this.selection = null;
|
||||||
|
|
||||||
|
this.container = document.createElement("div");
|
||||||
|
this.container.style.position = "absolute";
|
||||||
|
this.container.style.display = "block";
|
||||||
|
this.container.style.opacity = "0";
|
||||||
|
this.container.className = className;
|
||||||
|
}
|
||||||
|
|
||||||
|
public attach(el): void {
|
||||||
|
el.setAttribute("autocomplete", "off");
|
||||||
|
|
||||||
|
el.addEventListener("focus", () => {
|
||||||
|
this.element = el;
|
||||||
|
const domRect = el.getBoundingClientRect();
|
||||||
|
this.container.style.left = `${domRect.left + window.pageXOffset}px`;
|
||||||
|
this.container.style.width = `${domRect.width}px`;
|
||||||
|
this.container.style.top = `${domRect.bottom + window.pageYOffset}px`;
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener("blur", () => {
|
||||||
|
if (this.element !== el) return;
|
||||||
|
if (!this.visible) return;
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener("keydown", e => {
|
||||||
|
if (this.element !== el) return;
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
if (this.visible) this.hide();
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
if (this.default != null) {
|
||||||
|
el.value = this.default;
|
||||||
|
e.preventDefault();
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
} else if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.selection == null) this.selection = 0;
|
||||||
|
else ++this.selection;
|
||||||
|
this.update();
|
||||||
|
} else if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
--this.selection;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener("input", () => {
|
||||||
|
if (this.element !== el) return;
|
||||||
|
this.selection = null;
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private hide(): void {
|
||||||
|
this.container.style.opacity = "0";
|
||||||
|
this.visible = false;
|
||||||
|
this.default = null;
|
||||||
|
this.selection = null;
|
||||||
|
clearTimeout(this.hideTimeout);
|
||||||
|
this.hideTimeout = setTimeout(() => {
|
||||||
|
this.hideTimeout = null;
|
||||||
|
while (this.container.firstChild)
|
||||||
|
this.container.removeChild(this.container.firstChild);
|
||||||
|
document.body.removeChild(this.container);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private update(): void {
|
||||||
|
const el = this.element;
|
||||||
|
|
||||||
|
this.callback(el.value, suggestions => {
|
||||||
|
if (this.element !== el) return;
|
||||||
|
this.default = null;
|
||||||
|
|
||||||
|
if (!suggestions.length) {
|
||||||
|
if (this.visible) this.hide();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (this.container.firstChild)
|
||||||
|
this.container.removeChild(this.container.firstChild);
|
||||||
|
|
||||||
|
if (!this.visible) {
|
||||||
|
if (!this.hideTimeout) {
|
||||||
|
document.body.appendChild(this.container);
|
||||||
|
window.getComputedStyle(this.container).opacity;
|
||||||
|
} else {
|
||||||
|
clearTimeout(this.hideTimeout);
|
||||||
|
this.hideTimeout = null;
|
||||||
|
}
|
||||||
|
this.container.style.opacity = "1";
|
||||||
|
this.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selection != null) {
|
||||||
|
this.selection =
|
||||||
|
((this.selection % suggestions.length) + suggestions.length) %
|
||||||
|
suggestions.length;
|
||||||
|
this.default = suggestions[this.selection];
|
||||||
|
} else {
|
||||||
|
this.default = suggestions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedElement;
|
||||||
|
for (const [idx, suggestion] of suggestions.entries()) {
|
||||||
|
const e = document.createElement("div");
|
||||||
|
e.classList.add("suggestion");
|
||||||
|
if (idx === this.selection) {
|
||||||
|
e.classList.add("selected");
|
||||||
|
selectedElement = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = document.createTextNode(suggestion);
|
||||||
|
e.appendChild(t);
|
||||||
|
e.addEventListener("mousedown", ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
el.value = suggestion;
|
||||||
|
if (this.element === el) this.update();
|
||||||
|
});
|
||||||
|
this.container.appendChild(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure selected element is in view
|
||||||
|
if (selectedElement) {
|
||||||
|
this.container.scrollTop = Math.min(
|
||||||
|
this.container.scrollTop,
|
||||||
|
selectedElement.offsetTop
|
||||||
|
);
|
||||||
|
|
||||||
|
this.container.scrollTop = Math.max(
|
||||||
|
this.container.scrollTop,
|
||||||
|
selectedElement.offsetTop +
|
||||||
|
selectedElement.scrollHeight -
|
||||||
|
this.container.clientHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { VnodeDOM, ClosureComponent, Component } from "mithril";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
import { changePassword } from "./store";
|
||||||
|
import { m } from "./components";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const onPasswordChange = vnode.attrs["onPasswordChange"];
|
||||||
|
const enforceAuth = !vnode.attrs["noAuth"];
|
||||||
|
const username = vnode.attrs["username"];
|
||||||
|
|
||||||
|
if (username) vnode.state["username"] = username;
|
||||||
|
|
||||||
|
const form = [
|
||||||
|
m(
|
||||||
|
"p",
|
||||||
|
m("label", { for: "username" }, "Username"),
|
||||||
|
m("input", {
|
||||||
|
name: "username",
|
||||||
|
type: "text",
|
||||||
|
value: vnode.state["username"],
|
||||||
|
disabled: !!username,
|
||||||
|
oninput: m.withAttr("value", v => (vnode.state["username"] = v)),
|
||||||
|
oncreate: _vnode => {
|
||||||
|
(_vnode.dom as HTMLSelectElement).focus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
let fields = {
|
||||||
|
newPassword: "New password",
|
||||||
|
confirmPassword: "Confirm password"
|
||||||
|
};
|
||||||
|
if (enforceAuth)
|
||||||
|
fields = Object.assign({ authPassword: "Your password" }, fields);
|
||||||
|
|
||||||
|
for (const [f, l] of Object.entries(fields)) {
|
||||||
|
form.push(
|
||||||
|
m(
|
||||||
|
"p",
|
||||||
|
m("label", { for: f }, l),
|
||||||
|
m("input", {
|
||||||
|
name: f,
|
||||||
|
type: "password",
|
||||||
|
value: vnode.state[f],
|
||||||
|
oninput: m.withAttr("value", v => (vnode.state[f] = v))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
type: "submit"
|
||||||
|
},
|
||||||
|
"Change password"
|
||||||
|
) as VnodeDOM;
|
||||||
|
|
||||||
|
form.push(m(".actions-bar", submit));
|
||||||
|
|
||||||
|
const children = [
|
||||||
|
m("h1", "Change password"),
|
||||||
|
m(
|
||||||
|
"form",
|
||||||
|
{
|
||||||
|
onsubmit: e => {
|
||||||
|
e.redraw = false;
|
||||||
|
e.preventDefault();
|
||||||
|
if (
|
||||||
|
!vnode.state["username"] ||
|
||||||
|
!vnode.state["newPassword"] ||
|
||||||
|
(enforceAuth && !vnode.state["authPassword"])
|
||||||
|
) {
|
||||||
|
notifications.push("error", "Please fill all fields");
|
||||||
|
} else if (
|
||||||
|
vnode.state["newPassword"] !== vnode.state["confirmPassword"]
|
||||||
|
) {
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
"Password confirm doesn't match new password"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
(submit.dom as HTMLFormElement).disabled = true;
|
||||||
|
changePassword(
|
||||||
|
vnode.state["username"],
|
||||||
|
vnode.state["newPassword"],
|
||||||
|
vnode.state["authPassword"]
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
notifications.push(
|
||||||
|
"success",
|
||||||
|
"Password updated successfully"
|
||||||
|
);
|
||||||
|
if (onPasswordChange) onPasswordChange();
|
||||||
|
(submit.dom as HTMLFormElement).disabled = false;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
(submit.dom as HTMLFormElement).disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
form
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
return m("div.put-form", children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "./components";
|
||||||
|
import { codeMirror } from "./dynamic-loader";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
return m("textarea", {
|
||||||
|
name: vnode.attrs["id"],
|
||||||
|
value: vnode.attrs["value"],
|
||||||
|
oncreate: _vnode => {
|
||||||
|
const editor = codeMirror.fromTextArea(_vnode.dom, {
|
||||||
|
mode: vnode.attrs["mode"],
|
||||||
|
lineNumbers: true,
|
||||||
|
readOnly: vnode.attrs["readOnly"],
|
||||||
|
extraKeys: {
|
||||||
|
"Ctrl-Enter": () => {
|
||||||
|
if (vnode.attrs["onSubmit"])
|
||||||
|
vnode.attrs["onSubmit"](_vnode.dom);
|
||||||
|
},
|
||||||
|
"Cmd-Enter": () => {
|
||||||
|
if (vnode.attrs["onSubmit"])
|
||||||
|
vnode.attrs["onSubmit"](_vnode.dom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (vnode.attrs["onChange"]) {
|
||||||
|
editor.on("change", e => {
|
||||||
|
vnode.attrs["onChange"](e.getValue());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vnode.attrs["focus"]) editor.focus();
|
||||||
|
if (vnode.attrs["onReady"]) vnode.attrs["onReady"](editor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import m, {
|
||||||
|
Static,
|
||||||
|
Attributes,
|
||||||
|
Children,
|
||||||
|
ComponentTypes,
|
||||||
|
Lifecycle,
|
||||||
|
ClosureComponent,
|
||||||
|
Vnode
|
||||||
|
} from "mithril";
|
||||||
|
import parameter from "./components/parameter";
|
||||||
|
import parameterList from "./components/parameter-list";
|
||||||
|
import parameterTable from "./components/parameter-table";
|
||||||
|
import overviewDot from "./components/overview-dot";
|
||||||
|
import container from "./components/container";
|
||||||
|
import summonButton from "./components/summon-button";
|
||||||
|
import deviceFaults from "./components/device-faults";
|
||||||
|
import allParameters from "./components/all-parameters";
|
||||||
|
import deviceActions from "./components/device-actions";
|
||||||
|
import tags from "./components/tags";
|
||||||
|
import ping from "./components/ping";
|
||||||
|
import deviceLink from "./components/device-link";
|
||||||
|
import longTextComponent from "./long-text-component";
|
||||||
|
|
||||||
|
const comps = {
|
||||||
|
parameter,
|
||||||
|
"parameter-list": parameterList,
|
||||||
|
"parameter-table": parameterTable,
|
||||||
|
"overview-dot": overviewDot,
|
||||||
|
container,
|
||||||
|
"summon-button": summonButton,
|
||||||
|
"device-faults": deviceFaults,
|
||||||
|
"all-parameters": allParameters,
|
||||||
|
"device-actions": deviceActions,
|
||||||
|
tags,
|
||||||
|
ping,
|
||||||
|
"device-link": deviceLink,
|
||||||
|
"long-text": longTextComponent
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
|
||||||
|
const vnodeContext = new WeakMap<Vnode, Attributes>();
|
||||||
|
|
||||||
|
interface MC extends Static {
|
||||||
|
context: {
|
||||||
|
(ctx: Attributes, selector: string, ...children: Children[]): Vnode<
|
||||||
|
any,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
(
|
||||||
|
ctx: Attributes,
|
||||||
|
selector: string,
|
||||||
|
attributes: Attributes,
|
||||||
|
...children: Children[]
|
||||||
|
): Vnode<any, any>;
|
||||||
|
<Attrs, State>(
|
||||||
|
ctx: Attributes,
|
||||||
|
component: ComponentTypes<Attrs, State>,
|
||||||
|
...args: Children[]
|
||||||
|
): Vnode<Attrs, State>;
|
||||||
|
<Attrs, State>(
|
||||||
|
ctx: Attributes,
|
||||||
|
component: ComponentTypes<Attrs, State>,
|
||||||
|
attributes: Attrs & Lifecycle<Attrs, State> & { key?: string | number },
|
||||||
|
...args: Children[]
|
||||||
|
): Vnode<Attrs, State>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const M = new Proxy(m, {
|
||||||
|
apply: (target, thisArg, argumentsList) => {
|
||||||
|
const c = argumentsList[0];
|
||||||
|
if (typeof c !== "string") argumentsList[0] = contextifyComponent(c);
|
||||||
|
else if (comps[c]) argumentsList[0] = contextifyComponent(comps[c]);
|
||||||
|
|
||||||
|
return Reflect.apply(target, undefined, argumentsList);
|
||||||
|
},
|
||||||
|
get: (target, prop) => {
|
||||||
|
if (prop === "context") return contextFn;
|
||||||
|
else return Reflect.get(target, prop);
|
||||||
|
}
|
||||||
|
}) as MC;
|
||||||
|
|
||||||
|
function contextFn(context, ...argumentsList): Vnode {
|
||||||
|
const vnode = Reflect.apply(M, undefined, argumentsList);
|
||||||
|
vnodeContext.set(vnode, context);
|
||||||
|
return vnode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyContext(vnode, parentContext): void {
|
||||||
|
if (Array.isArray(vnode)) {
|
||||||
|
for (const c of vnode) applyContext(c, parentContext);
|
||||||
|
} else if (vnode && typeof vnode === "object" && vnode.tag) {
|
||||||
|
const vc = Object.assign({}, parentContext, vnodeContext.get(vnode));
|
||||||
|
vnodeContext.set(vnode, vc);
|
||||||
|
vnode.attrs = Object.assign({}, vc, vnode.attrs);
|
||||||
|
if (vnode.children && vnode.children.length)
|
||||||
|
for (const c of vnode.children) applyContext(c, vc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contextifyComponent(component: ComponentTypes): ComponentTypes {
|
||||||
|
let c = contextifiedComponents.get(component);
|
||||||
|
if (!c) {
|
||||||
|
if (typeof component !== "function") {
|
||||||
|
c = Object.assign({}, component);
|
||||||
|
const view = component.view;
|
||||||
|
c.view = function(vnode) {
|
||||||
|
const context = vnodeContext.get(vnode) || {};
|
||||||
|
const res = Reflect.apply(view, this, [vnode]);
|
||||||
|
applyContext(res, context);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
} else if (!component.prototype || !component.prototype.view) {
|
||||||
|
c = initialNode => {
|
||||||
|
const state = (component as ClosureComponent)(initialNode);
|
||||||
|
const view = state.view;
|
||||||
|
state.view = function(vnode) {
|
||||||
|
const context = vnodeContext.get(vnode) || {};
|
||||||
|
const res = Reflect.apply(view, this, [vnode]);
|
||||||
|
applyContext(res, context);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// TODO support class components
|
||||||
|
throw new Error("Class components not supported");
|
||||||
|
}
|
||||||
|
contextifiedComponents.set(component, c);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { M as m };
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as taskQueue from "../task-queue";
|
||||||
|
import { parse } from "../../lib/common/expression-parser";
|
||||||
|
import memoize from "../../lib/common/memoize";
|
||||||
|
|
||||||
|
const memoizedParse = memoize(parse);
|
||||||
|
|
||||||
|
function escapeRegExp(str): string {
|
||||||
|
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
|
||||||
|
const search = m("input", {
|
||||||
|
type: "text",
|
||||||
|
placeholder: "Search parameters",
|
||||||
|
oninput: e => {
|
||||||
|
vnode.state["searchString"] = e.target.value;
|
||||||
|
e.redraw = false;
|
||||||
|
clearTimeout(vnode.state["timeout"]);
|
||||||
|
vnode.state["timeout"] = setTimeout(m.redraw, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const instanceRegex = /\.[0-9]+$/;
|
||||||
|
let re;
|
||||||
|
if (vnode.state["searchString"]) {
|
||||||
|
const keywords = vnode.state["searchString"].split(" ").filter(s => s);
|
||||||
|
if (keywords.length)
|
||||||
|
re = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = Object.keys(device)
|
||||||
|
.sort()
|
||||||
|
.map(k => {
|
||||||
|
const p = device[k];
|
||||||
|
const val = [];
|
||||||
|
const attrs = {};
|
||||||
|
if (re) {
|
||||||
|
const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k;
|
||||||
|
if (!re.test(str)) attrs["style"] = "display: none;";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.object === false) {
|
||||||
|
val.push(
|
||||||
|
m(
|
||||||
|
"parameter",
|
||||||
|
Object.assign({ device: device, parameter: memoizedParse(k) })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (p.object && p.writable) {
|
||||||
|
if (instanceRegex.test(k)) {
|
||||||
|
val.push(
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Delete this instance",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.queueTask({
|
||||||
|
name: "deleteObject",
|
||||||
|
device: device["DeviceID.ID"].value[0],
|
||||||
|
objectName: k
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"✕"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
val.push(
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Create a new instance",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.queueTask({
|
||||||
|
name: "addObject",
|
||||||
|
device: device["DeviceID.ID"].value[0],
|
||||||
|
objectName: k
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"🞢"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val.push(
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Refresh tree",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.queueTask({
|
||||||
|
name: "getParameterValues",
|
||||||
|
device: device["DeviceID.ID"].value[0],
|
||||||
|
parameterNames: [k]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"↺"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return m(
|
||||||
|
"tr",
|
||||||
|
attrs,
|
||||||
|
m("td.left", m("long-text", { text: k })),
|
||||||
|
m("td.right", val)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return m(
|
||||||
|
".all-parameters",
|
||||||
|
m(
|
||||||
|
"a.download-csv",
|
||||||
|
{
|
||||||
|
href: `/api/devices/${device["DeviceID.ID"].value[0]}.csv`,
|
||||||
|
download: "",
|
||||||
|
style: "float: right;"
|
||||||
|
},
|
||||||
|
"Download"
|
||||||
|
),
|
||||||
|
search,
|
||||||
|
m(".parameter-list", m("table", m("tbody", rows)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as store from "../store";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
if (vnode.attrs["filter"]) {
|
||||||
|
if (
|
||||||
|
!store.evaluateExpression(
|
||||||
|
vnode.attrs["filter"],
|
||||||
|
vnode.attrs["device"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = Object.values(vnode.attrs["components"]).map(c => {
|
||||||
|
if (typeof c !== "object") return `${c}`;
|
||||||
|
return m(c["type"], c);
|
||||||
|
});
|
||||||
|
if (vnode.attrs["element"]) return m(vnode.attrs["element"], children);
|
||||||
|
else return children;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as taskQueue from "../task-queue";
|
||||||
|
import * as notifications from "../notifications";
|
||||||
|
import * as store from "../store";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Reboot device",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.queueTask({
|
||||||
|
name: "reboot",
|
||||||
|
device: device["DeviceID.ID"].value[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Reboot"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.critical",
|
||||||
|
{
|
||||||
|
title: "Factory reset device",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.queueTask({
|
||||||
|
name: "factoryReset",
|
||||||
|
device: device["DeviceID.ID"].value[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Reset"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.critical",
|
||||||
|
{
|
||||||
|
title: "Push a firmware or a config file",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.stageDownload({
|
||||||
|
name: "download",
|
||||||
|
device: device["DeviceID.ID"].value[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Push file"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Delete device",
|
||||||
|
onclick: () => {
|
||||||
|
if (!confirm("Deleting this device. Are you sure?")) return;
|
||||||
|
const deviceId = device["DeviceID.ID"].value[0];
|
||||||
|
|
||||||
|
store
|
||||||
|
.deleteResource("devices", deviceId)
|
||||||
|
.then(() => {
|
||||||
|
notifications.push("success", `${deviceId}: Device deleted`);
|
||||||
|
m.route.set("/devices");
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Delete"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return m(".actions-bar", buttons);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as store from "../store";
|
||||||
|
import * as notifications from "../notifications";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
const deviceId = device["DeviceID.ID"].value[0];
|
||||||
|
const faults = store.fetch("faults", [
|
||||||
|
"AND",
|
||||||
|
[">", ["PARAM", "_id"], `${deviceId}:`],
|
||||||
|
["<", ["PARAM", "_id"], `${deviceId}:zzzz`]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
"Channel",
|
||||||
|
"Code",
|
||||||
|
"Message",
|
||||||
|
"Retries",
|
||||||
|
"Timestamp"
|
||||||
|
].map(l => m("th", l));
|
||||||
|
const thead = m("thead", m("tr", headers));
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const f of faults.value) {
|
||||||
|
rows.push([
|
||||||
|
m("td", f["channel"]),
|
||||||
|
m("td", f["code"]),
|
||||||
|
m("td", f["message"]),
|
||||||
|
m("td", f["retries"]),
|
||||||
|
m("td", new Date(f["timestamp"]).toLocaleString()),
|
||||||
|
m(
|
||||||
|
"td",
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Delete fault",
|
||||||
|
onclick: e => {
|
||||||
|
e.redraw = false;
|
||||||
|
store
|
||||||
|
.deleteResource("faults", f["_id"])
|
||||||
|
.then(() => {
|
||||||
|
notifications.push("success", "Fault deleted");
|
||||||
|
store.fulfill(Date.now(), Date.now());
|
||||||
|
m.redraw();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
store.fulfill(Date.now(), Date.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"✕"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tbody;
|
||||||
|
if (rows.length) {
|
||||||
|
tbody = m("tbody", rows.map(r => m("tr", r)));
|
||||||
|
} else {
|
||||||
|
tbody = m(
|
||||||
|
"tbody",
|
||||||
|
m("tr.empty", m("td", { colspan: headers.length }, "No faults"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m("table.table", thead, tbody);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
let deviceId;
|
||||||
|
if (vnode.attrs["device"])
|
||||||
|
deviceId = vnode.attrs["device"]["DeviceID.ID"].value[0];
|
||||||
|
|
||||||
|
const children = Object.values(vnode.attrs["components"]).map(c => {
|
||||||
|
if (typeof c !== "object") return `${c}`;
|
||||||
|
const attrs = Object.assign({}, vnode.attrs, c);
|
||||||
|
return m(attrs["type"], attrs);
|
||||||
|
});
|
||||||
|
if (deviceId) {
|
||||||
|
return m(
|
||||||
|
"a",
|
||||||
|
{ href: `#!/devices/${encodeURIComponent(deviceId)}` },
|
||||||
|
children
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import config from "../config";
|
||||||
|
import * as store from "../store";
|
||||||
|
|
||||||
|
const CHARTS = config.ui.overview.charts;
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
const chart = CHARTS[vnode.attrs["chart"]];
|
||||||
|
if (!chart) return null;
|
||||||
|
for (const slice of Object.values(chart.slices)) {
|
||||||
|
const filter = slice["filter"];
|
||||||
|
if (store.evaluateExpression(filter, device)) {
|
||||||
|
const dot = m(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
width: "1em",
|
||||||
|
height: "1em",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
"xmlns:xlink": "http://www.w3.org/1999/xlink"
|
||||||
|
},
|
||||||
|
m("circle", {
|
||||||
|
cx: "0.5em",
|
||||||
|
cy: "0.5em",
|
||||||
|
r: "0.4em",
|
||||||
|
fill: slice["color"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return m("span.overview-dot", dot, `${slice["label"]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ClosureComponent, Component, VnodeDOM } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
|
||||||
|
const rows = Object.values(vnode.attrs["parameters"]).map(parameter => {
|
||||||
|
const p = m.context(
|
||||||
|
{
|
||||||
|
device: device,
|
||||||
|
parameter: parameter["parameter"]
|
||||||
|
},
|
||||||
|
parameter["type"] || "parameter",
|
||||||
|
parameter
|
||||||
|
);
|
||||||
|
|
||||||
|
return m(
|
||||||
|
"tr",
|
||||||
|
{
|
||||||
|
onupdate: vn => {
|
||||||
|
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
||||||
|
? ""
|
||||||
|
: "none";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
m("th", parameter["label"]),
|
||||||
|
m("td", p)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return m("table.parameter-list", rows);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as taskQueue from "../task-queue";
|
||||||
|
import * as store from "../store";
|
||||||
|
import * as expressionParser from "../../lib/common/expression-parser";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
oninit: vnode => {
|
||||||
|
const obj = vnode.attrs["parameter"];
|
||||||
|
if (!Array.isArray(obj) || obj[0] !== "PARAM")
|
||||||
|
throw new Error("Object must be a parameter path");
|
||||||
|
vnode.state["object"] = obj[1];
|
||||||
|
vnode.state["parameters"] = Object.values(vnode.attrs["childParameters"]);
|
||||||
|
},
|
||||||
|
view: vnode => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
const object = store.evaluateExpression(vnode.state["object"], device);
|
||||||
|
const parameters = vnode.state["parameters"];
|
||||||
|
|
||||||
|
if (!device[object]) return null;
|
||||||
|
|
||||||
|
const instances = new Set();
|
||||||
|
const prefix = `${object}.`;
|
||||||
|
for (const p in device) {
|
||||||
|
if (p.startsWith(prefix)) {
|
||||||
|
const i = p.indexOf(".", prefix.length);
|
||||||
|
if (i === -1) instances.add(p);
|
||||||
|
else instances.add(p.slice(0, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = Object.values(parameters).map(p => m("th", p["label"]));
|
||||||
|
|
||||||
|
const thead = m("thead", m("tr", headers));
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const i of instances) {
|
||||||
|
let filter =
|
||||||
|
vnode.attrs["filter"] != null ? vnode.attrs["filter"] : true;
|
||||||
|
|
||||||
|
filter = expressionParser.map(filter, e => {
|
||||||
|
if (Array.isArray(e) && e[0] === "PARAM")
|
||||||
|
return ["PARAM", ["||", i, ".", e[1]]];
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!store.evaluateExpression(filter, device)) continue;
|
||||||
|
|
||||||
|
const row = parameters.map(p => {
|
||||||
|
const param = expressionParser.map(p.parameter, e => {
|
||||||
|
if (Array.isArray(e) && e[0] === "PARAM")
|
||||||
|
return ["PARAM", ["||", i, ".", e[1]]];
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
return m(
|
||||||
|
"td",
|
||||||
|
m.context(
|
||||||
|
{
|
||||||
|
device: device,
|
||||||
|
parameter: param
|
||||||
|
},
|
||||||
|
p.type || "parameter",
|
||||||
|
Object.assign({}, p, {
|
||||||
|
device: device,
|
||||||
|
parameter: param
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (device[i].writable === true) {
|
||||||
|
row.push(
|
||||||
|
m(
|
||||||
|
"td",
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Delete this instance",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.queueTask({
|
||||||
|
name: "deleteObject",
|
||||||
|
device: device["DeviceID.ID"].value[0],
|
||||||
|
objectName: i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"✕"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rows.push(m("tr", row));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
rows.push(
|
||||||
|
m("tr.empty", m("td", { colspan: headers.length }, "No instances"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device[object].writable === true) {
|
||||||
|
rows.push(
|
||||||
|
m(
|
||||||
|
"tr",
|
||||||
|
m("td", { colspan: headers.length }),
|
||||||
|
m(
|
||||||
|
"td",
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Create a new instance",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.queueTask({
|
||||||
|
name: "addObject",
|
||||||
|
device: device["DeviceID.ID"].value[0],
|
||||||
|
objectName: object
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"🞢"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label;
|
||||||
|
if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
|
||||||
|
|
||||||
|
return [label, m("table.table", thead, m("tbody", rows))];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { ClosureComponent, Component, VnodeDOM } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as taskQueue from "../task-queue";
|
||||||
|
import * as store from "../store";
|
||||||
|
import * as expression from "../../lib/common/expression";
|
||||||
|
import memoize from "../../lib/common/memoize";
|
||||||
|
import timeAgo from "../timeago";
|
||||||
|
|
||||||
|
const evaluateParam = memoize((exp, obj, now) => {
|
||||||
|
let timestamp = now;
|
||||||
|
const params = new Set();
|
||||||
|
const value = expression.evaluate(exp, obj, now, e => {
|
||||||
|
if (Array.isArray(e)) {
|
||||||
|
if (e[0] === "PARAM") {
|
||||||
|
params.add(e[1]);
|
||||||
|
if (!Array.isArray(e[1])) {
|
||||||
|
const p = obj[e[1]];
|
||||||
|
if (p && p.valueTimestamp)
|
||||||
|
timestamp = Math.min(timestamp, p.valueTimestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e[0] === "FUNC" && e[1] === "DATE_STRING" && !Array.isArray(e[2]))
|
||||||
|
return new Date(e[2]).toLocaleString();
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
|
||||||
|
let parameter = params.size === 1 ? params.values().next().value : null;
|
||||||
|
if (Array.isArray(parameter)) parameter = null;
|
||||||
|
return { value, timestamp, parameter };
|
||||||
|
});
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
|
||||||
|
const { value, timestamp, parameter } = evaluateParam(
|
||||||
|
vnode.attrs["parameter"],
|
||||||
|
vnode.attrs["device"],
|
||||||
|
store.getTimestamp()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (value == null) return null;
|
||||||
|
|
||||||
|
let edit;
|
||||||
|
if (device[parameter] && device[parameter].writable) {
|
||||||
|
edit = m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Edit parameter value",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.stageSpv({
|
||||||
|
name: "setParameterValues",
|
||||||
|
device: device["DeviceID.ID"].value[0],
|
||||||
|
parameterValues: [
|
||||||
|
[
|
||||||
|
parameter,
|
||||||
|
device[parameter].value[0],
|
||||||
|
device[parameter].value[1]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"✎"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = m("long-text", { text: `${value}` });
|
||||||
|
|
||||||
|
return m(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
class: "parameter-value",
|
||||||
|
onmouseover: e => {
|
||||||
|
e.redraw = false;
|
||||||
|
// Don't update any child element
|
||||||
|
if (e.target === (el as VnodeDOM).dom) {
|
||||||
|
const now = Date.now();
|
||||||
|
const localeString = new Date(timestamp).toLocaleString();
|
||||||
|
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
el,
|
||||||
|
edit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as store from "../store";
|
||||||
|
|
||||||
|
const REFRESH_INTERVAL = 3000;
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
oninit: vnode => {
|
||||||
|
vnode.state["timestamp"] = 0;
|
||||||
|
vnode.state["ping"] = null;
|
||||||
|
vnode.state["host"] = null;
|
||||||
|
vnode.state["timeout"] = null;
|
||||||
|
},
|
||||||
|
view: vnode => {
|
||||||
|
const refresh = (): void => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
let param =
|
||||||
|
device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"];
|
||||||
|
if (!param)
|
||||||
|
param = device["Device.ManagementServer.ConnectionRequestURL"];
|
||||||
|
if (!param || !param.value) return;
|
||||||
|
|
||||||
|
const url = new URL(param.value[0]);
|
||||||
|
vnode.state["host"] = url.hostname;
|
||||||
|
vnode.state["timestamp"] = Date.now();
|
||||||
|
store
|
||||||
|
.ping(vnode.state["host"])
|
||||||
|
.then(res => {
|
||||||
|
vnode.state["ping"] = res;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Do nothing
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = vnode.state["timestamp"] + REFRESH_INTERVAL - Date.now();
|
||||||
|
clearTimeout(vnode.state["timeout"]);
|
||||||
|
if (t <= 0) refresh();
|
||||||
|
else vnode.state["timeout"] = setTimeout(refresh, t);
|
||||||
|
|
||||||
|
if (vnode.state["host"]) {
|
||||||
|
let status = "";
|
||||||
|
if (vnode.state["ping"]) {
|
||||||
|
if (vnode.state["ping"].avg != null)
|
||||||
|
status = `${Math.trunc(vnode.state["ping"].avg)} ms`;
|
||||||
|
else status = "Unreachable";
|
||||||
|
}
|
||||||
|
|
||||||
|
return m("div", `Pinging ${vnode.state["host"]}: ${status}`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as taskQueue from "../task-queue";
|
||||||
|
import * as store from "../store";
|
||||||
|
import * as notifications from "../notifications";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
|
||||||
|
return m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Initiate session and refresh basic parameters",
|
||||||
|
onclick: e => {
|
||||||
|
e.target.disabled = true;
|
||||||
|
const params = Object.values(vnode.attrs["parameters"])
|
||||||
|
.map(exp => {
|
||||||
|
if (!Array.isArray(exp) || exp[0] !== "PARAM") return null;
|
||||||
|
return store.evaluateExpression(exp[1], device);
|
||||||
|
})
|
||||||
|
.filter(exp => !!exp);
|
||||||
|
|
||||||
|
const task = {
|
||||||
|
name: "getParameterValues",
|
||||||
|
parameterNames: params,
|
||||||
|
device: device["DeviceID.ID"].value[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
taskQueue
|
||||||
|
.commit(
|
||||||
|
[task],
|
||||||
|
(deviceId, err, connectionRequestStatus, tasks2) => {
|
||||||
|
if (err) {
|
||||||
|
notifications.push("error", `${deviceId}: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const t of tasks2)
|
||||||
|
if (t.status === "stale") taskQueue.deleteTask(t);
|
||||||
|
|
||||||
|
if (connectionRequestStatus !== "OK") {
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
`${deviceId}: ${connectionRequestStatus}`
|
||||||
|
);
|
||||||
|
} else if (tasks2[0].status === "stale") {
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
`${deviceId}: No contact from device`
|
||||||
|
);
|
||||||
|
} else if (tasks2[0].status === "fault") {
|
||||||
|
notifications.push("error", `${deviceId}: Refresh faulted`);
|
||||||
|
} else {
|
||||||
|
notifications.push("success", `${deviceId}: Summoned`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Summon"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "../components";
|
||||||
|
import * as notifications from "../notifications";
|
||||||
|
import * as store from "../store";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const device = vnode.attrs["device"];
|
||||||
|
let writable = true;
|
||||||
|
if (vnode.attrs["writable"] != null) writable = vnode.attrs["writable"];
|
||||||
|
|
||||||
|
const tags = [];
|
||||||
|
for (const p of Object.keys(device))
|
||||||
|
if (p.startsWith("Tags.")) tags.push(p.slice(5));
|
||||||
|
|
||||||
|
tags.sort();
|
||||||
|
|
||||||
|
if (!writable) return m(".tags", tags.map(t => m("span.tag", t)));
|
||||||
|
|
||||||
|
return m(
|
||||||
|
".tags",
|
||||||
|
tags.map(tag =>
|
||||||
|
m(
|
||||||
|
"span.tag",
|
||||||
|
tag,
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onclick: e => {
|
||||||
|
e.target.disabled = true;
|
||||||
|
const deviceId = device["DeviceID.ID"].value[0];
|
||||||
|
store
|
||||||
|
.updateTags(deviceId, { [tag]: false })
|
||||||
|
.then(() => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
notifications.push(
|
||||||
|
"success",
|
||||||
|
`${deviceId}: Tags updated`
|
||||||
|
);
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
`${deviceId}: ${err.message}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"✕"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"span.tag.writable",
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onclick: e => {
|
||||||
|
e.target.disabled = true;
|
||||||
|
const deviceId = device["DeviceID.ID"].value[0];
|
||||||
|
const tag = prompt(`Enter tag to assign to device:`);
|
||||||
|
if (!tag) {
|
||||||
|
e.target.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
store
|
||||||
|
.updateTags(deviceId, { [tag]: true })
|
||||||
|
.then(() => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
notifications.push("success", `${deviceId}: Tags updated`);
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
e.target.disabled = false;
|
||||||
|
notifications.push("error", `${deviceId}: ${err.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"🞢"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
interface Config {
|
||||||
|
_id: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Diff {
|
||||||
|
add: Config[];
|
||||||
|
remove: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenConfig(config): any {
|
||||||
|
const flatten = {};
|
||||||
|
const recuresive = (obj: any, root: string): void => {
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
const key = root ? `${root}.${k}` : k;
|
||||||
|
if (v === undefined) continue;
|
||||||
|
if (v === null || typeof v !== "object") flatten[key] = v;
|
||||||
|
else recuresive(v, key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config !== null && typeof config === "object") recuresive(config, "");
|
||||||
|
return flatten;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function structureConfig(config: Config[]): any {
|
||||||
|
config.sort((a, b) => (a._id > b._id ? 1 : a._id < b._id ? -1 : 0));
|
||||||
|
const _config = {};
|
||||||
|
for (const c of config) {
|
||||||
|
const keys = c._id.split(".");
|
||||||
|
let ref = _config;
|
||||||
|
while (keys.length > 1) {
|
||||||
|
const k = keys.shift();
|
||||||
|
if (typeof ref[k] !== "object") ref[k] = {};
|
||||||
|
ref = ref[k];
|
||||||
|
}
|
||||||
|
ref[keys[0]] = c.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toArray = function(object): any {
|
||||||
|
const MAX_BITS = 30;
|
||||||
|
const MAX_ARRAY_SIZE = MAX_BITS * 10;
|
||||||
|
|
||||||
|
if (object == null || typeof object !== "object") return object;
|
||||||
|
|
||||||
|
if (Object.keys(object).length <= MAX_ARRAY_SIZE) {
|
||||||
|
let indexes = [];
|
||||||
|
for (const key of Object.keys(object)) {
|
||||||
|
const idx = Math.floor(+key);
|
||||||
|
if (idx >= 0 && idx < MAX_ARRAY_SIZE && String(idx) === key) {
|
||||||
|
const pos = Math.floor(idx / MAX_BITS);
|
||||||
|
if (!indexes[pos]) indexes[pos] = 0;
|
||||||
|
indexes[pos] |= 1 << idx % MAX_BITS;
|
||||||
|
} else {
|
||||||
|
indexes = [];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
while (indexes.length && (index = indexes.shift()) === 1073741823);
|
||||||
|
|
||||||
|
if (index && (~index & (index + 1)) === index + 1) {
|
||||||
|
// its an array
|
||||||
|
const array = [];
|
||||||
|
for (let i = 0; i < Object.keys(object).length; i++)
|
||||||
|
array[i] = object[i];
|
||||||
|
|
||||||
|
object = array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(object)) object[k] = toArray(v);
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
||||||
|
return toArray(_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function diffConfig(current, target): Diff {
|
||||||
|
const diff = {
|
||||||
|
add: [],
|
||||||
|
remove: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(target))
|
||||||
|
if (!current[k] || current[k] !== v) diff.add.push({ _id: k, value: v });
|
||||||
|
|
||||||
|
for (const k of Object.keys(current)) if (!target[k]) diff.remove.push(k);
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import { m } from "./components";
|
||||||
|
import * as store from "./store";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
import putFormComponent from "./put-form-component";
|
||||||
|
import uiConfigComponent from "./ui-config-component";
|
||||||
|
import * as overlay from "./overlay";
|
||||||
|
import * as expressionParser from "../lib/common/expression-parser";
|
||||||
|
import { loadCodeMirror, loadYaml } from "./dynamic-loader";
|
||||||
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
|
|
||||||
|
const attributes = [
|
||||||
|
{ id: "_id", label: "Key" },
|
||||||
|
{ id: "value", label: "Value" }
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ValidationErrors {
|
||||||
|
[prop: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const object = Object.assign({}, _object);
|
||||||
|
if (action === "save") {
|
||||||
|
const id = object["_id"];
|
||||||
|
delete object["_id"];
|
||||||
|
|
||||||
|
if (!id) return void resolve({ _id: "ID can not be empty" });
|
||||||
|
try {
|
||||||
|
expressionParser.parse(object.value);
|
||||||
|
} catch (err) {
|
||||||
|
return void resolve({
|
||||||
|
value: "Config value must be valid expression"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
store
|
||||||
|
.resourceExists("config", id)
|
||||||
|
.then(exists => {
|
||||||
|
if (exists && isNew) {
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
return void resolve({ _id: "Config already exists" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exists && !isNew) {
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
return void resolve({ _id: "Config does not exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
store
|
||||||
|
.putResource("config", id, object)
|
||||||
|
.then(() => {
|
||||||
|
notifications.push(
|
||||||
|
"success",
|
||||||
|
`Config ${exists ? "updated" : "created"}`
|
||||||
|
);
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
} else if (action === "delete") {
|
||||||
|
store
|
||||||
|
.deleteResource("config", object["_id"])
|
||||||
|
.then(() => {
|
||||||
|
notifications.push("success", "Config deleted");
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error("Undefined action"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
resource: "config",
|
||||||
|
attributes: attributes
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeRegExp(str): string {
|
||||||
|
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function init(): Promise<{}> {
|
||||||
|
if (!window.authorizer.hasAccess("config", 2)) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("You are not authorized to view this page")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Promise.all([loadCodeMirror(), loadYaml()])
|
||||||
|
.then(() => {
|
||||||
|
resolve({});
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(confsResponse, searchString): Children {
|
||||||
|
const confs = confsResponse.value.sort((a, b) => {
|
||||||
|
return a._id < b._id ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
let regex;
|
||||||
|
if (searchString) {
|
||||||
|
const keywords = searchString.split(" ").filter(s => s);
|
||||||
|
if (keywords.length)
|
||||||
|
regex = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const conf of confs) {
|
||||||
|
const attrs = {};
|
||||||
|
if (regex && !regex.test(conf._id) && !regex.test(conf.value))
|
||||||
|
attrs["style"] = "display: none;";
|
||||||
|
|
||||||
|
const edit = m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Edit config value",
|
||||||
|
onclick: () => {
|
||||||
|
const cb = (): Children => {
|
||||||
|
return m(
|
||||||
|
putFormComponent,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
base: conf,
|
||||||
|
actionHandler: (action, object) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
putActionHandler(action, object, false)
|
||||||
|
.then(errors => {
|
||||||
|
const ErrorList = errors ? Object.values(errors) : [];
|
||||||
|
if (ErrorList.length) {
|
||||||
|
for (const err of ErrorList)
|
||||||
|
notifications.push("error", err);
|
||||||
|
} else {
|
||||||
|
overlay.close(cb);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formData
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
overlay.open(cb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"✎"
|
||||||
|
);
|
||||||
|
|
||||||
|
const del = m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Delete config",
|
||||||
|
onclick: () => {
|
||||||
|
if (!confirm(`Deleting ${conf._id} config. Are you sure?`)) return;
|
||||||
|
|
||||||
|
putActionHandler("delete", conf).catch(err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"✕"
|
||||||
|
);
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
m(
|
||||||
|
"tr",
|
||||||
|
attrs,
|
||||||
|
m("td.left", m("long-text", { text: conf._id })),
|
||||||
|
m(
|
||||||
|
"td.right",
|
||||||
|
m("span", [m("long-text", { text: `${conf.value}` }), edit, del])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows.length)
|
||||||
|
rows.push(m("tr.empty", m("td", { colspan: 2 }, "No config")));
|
||||||
|
|
||||||
|
return m("table", m("tbody", rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
document.title = "Config - GenieACS";
|
||||||
|
|
||||||
|
const search = m("input", {
|
||||||
|
type: "text",
|
||||||
|
placeholder: "Search config",
|
||||||
|
oninput: e => {
|
||||||
|
vnode.state["searchString"] = e.target.value;
|
||||||
|
e.redraw = false;
|
||||||
|
clearTimeout(vnode.state["timeout"]);
|
||||||
|
vnode.state["timeout"] = setTimeout(m.redraw, 250);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const confs = store.fetch("config", true);
|
||||||
|
|
||||||
|
let newConfig;
|
||||||
|
const subs = [];
|
||||||
|
|
||||||
|
if (window.authorizer.hasAccess("config", 3)) {
|
||||||
|
newConfig = m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Create new config",
|
||||||
|
onclick: () => {
|
||||||
|
const cb = (): Children => {
|
||||||
|
return m(
|
||||||
|
putFormComponent,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
actionHandler: (action, object) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
putActionHandler(action, object, true)
|
||||||
|
.then(errors => {
|
||||||
|
const errorList = errors
|
||||||
|
? Object.values(errors)
|
||||||
|
: [];
|
||||||
|
if (errorList.length) {
|
||||||
|
for (const err of errorList)
|
||||||
|
notifications.push("error", err);
|
||||||
|
} else {
|
||||||
|
overlay.close(cb);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formData
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
overlay.open(cb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"New config"
|
||||||
|
);
|
||||||
|
|
||||||
|
const subsData = [
|
||||||
|
{ name: "overview", prefix: "ui.overview.groups.", data: [] },
|
||||||
|
{ name: "charts", prefix: "ui.overview.charts.", data: [] },
|
||||||
|
{ name: "filters", prefix: "ui.filters.", data: [] },
|
||||||
|
{ name: "index page", prefix: "ui.index.", data: [] },
|
||||||
|
{ name: "device page", prefix: "ui.device.", data: [] }
|
||||||
|
];
|
||||||
|
|
||||||
|
if (confs.fulfilled) {
|
||||||
|
for (const conf of confs.value) {
|
||||||
|
for (const sub of subsData) {
|
||||||
|
if (conf["_id"].startsWith(sub["prefix"])) {
|
||||||
|
sub["data"].push(conf);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sub of subsData) {
|
||||||
|
const attrs = { prefix: sub.prefix, name: sub.name, data: sub.data };
|
||||||
|
subs.push(
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onclick: () => {
|
||||||
|
const cb = (): Children => {
|
||||||
|
return m(
|
||||||
|
uiConfigComponent,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
onUpdate: errs => {
|
||||||
|
const errors = errs ? Object.values(errs) : [];
|
||||||
|
if (errors.length) {
|
||||||
|
for (const err of errors)
|
||||||
|
notifications.push("error", err);
|
||||||
|
} else {
|
||||||
|
notifications.push(
|
||||||
|
"success",
|
||||||
|
`${sub.name.replace(
|
||||||
|
/^[a-z]/,
|
||||||
|
sub.name[0].toUpperCase()
|
||||||
|
)} config updated`
|
||||||
|
);
|
||||||
|
overlay.close(cb);
|
||||||
|
}
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
overlay.close(cb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attrs
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
overlay.open(cb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
`Edit ${sub.name}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
m("h1", "Listing config"),
|
||||||
|
m(
|
||||||
|
".all-parameters",
|
||||||
|
search,
|
||||||
|
m(
|
||||||
|
".parameter-list",
|
||||||
|
{ style: "height: 400px" },
|
||||||
|
renderTable(confs, vnode.state["searchString"])
|
||||||
|
),
|
||||||
|
m(".actions-bar", [newConfig].concat(subs))
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default window.clientConfig;
|
||||||
+685
@@ -0,0 +1,685 @@
|
|||||||
|
@import "normalize.css";
|
||||||
|
@import "codemirror/lib/codemirror.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--base-font-size: 13px;
|
||||||
|
--base-line-height: 16px;
|
||||||
|
|
||||||
|
--color1: #ebe0bf;
|
||||||
|
--color2: #b9b098;
|
||||||
|
--color3: #f2ebd5;
|
||||||
|
--color4: #046380;
|
||||||
|
--color5: #002f2f;
|
||||||
|
|
||||||
|
--fade: 0.2s;
|
||||||
|
|
||||||
|
--disabled: #666;
|
||||||
|
|
||||||
|
--status-red: hsl(0, 80%, 80%);
|
||||||
|
--status-green: hsl(125, 80%, 80%);
|
||||||
|
--status-gray: hsl(58, 3%, 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
option,
|
||||||
|
textarea {
|
||||||
|
font-family: sans-serif;
|
||||||
|
color: var(--color5);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color4);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--fade), color var(--fade);
|
||||||
|
&:visited {
|
||||||
|
color: var(--color4);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: var(--color4);
|
||||||
|
}
|
||||||
|
/* text-decoration does not propagate to inline-block children */
|
||||||
|
& span {
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strong,
|
||||||
|
b {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pending-blink {
|
||||||
|
from {
|
||||||
|
background-color: color(var(--status-green) a(90%));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
background-color: #f2eddf;
|
||||||
|
border-bottom: 1px solid var(--color1);
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& > .drawer-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
width: 720px;
|
||||||
|
margin-left: -360px;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
& > .drawer {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
border-bottom: 1px solid var(--color1);
|
||||||
|
border-left: 1px solid var(--color1);
|
||||||
|
border-right: 1px solid var(--color1);
|
||||||
|
margin-top: -1px;
|
||||||
|
box-shadow: 0 0 10px #ccc;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: height var(--fade), opacity var(--fade);
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
padding: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .status {
|
||||||
|
display: flex;
|
||||||
|
& > span {
|
||||||
|
margin-right: 1em;
|
||||||
|
padding-left: 0.2em;
|
||||||
|
padding-right: 0.2em;
|
||||||
|
border-radius: 0.4em;
|
||||||
|
}
|
||||||
|
& > .pending.active {
|
||||||
|
&.active {
|
||||||
|
animation: 0.5s linear 0s infinite alternate pending-blink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& > .stale.active {
|
||||||
|
background-color: color(var(--status-gray) a(90%));
|
||||||
|
}
|
||||||
|
& > .fault.active {
|
||||||
|
background-color: color(var(--status-red) a(90%));
|
||||||
|
}
|
||||||
|
& > .queued.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .actions {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .staging {
|
||||||
|
& .parameter {
|
||||||
|
font-style: italic;
|
||||||
|
max-width: 500px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
& input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.3em;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
& .actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .queue {
|
||||||
|
& > .pending {
|
||||||
|
animation: 0.5s linear 0s infinite alternate pending-blink;
|
||||||
|
border-radius: 0.4em;
|
||||||
|
}
|
||||||
|
& > .stale {
|
||||||
|
background-color: color(var(--status-gray) a(90%));
|
||||||
|
border-radius: 0.4em;
|
||||||
|
}
|
||||||
|
& > .fault {
|
||||||
|
background-color: color(var(--status-red) a(90%));
|
||||||
|
border-radius: 0.4em;
|
||||||
|
}
|
||||||
|
& .parameter {
|
||||||
|
font-style: italic;
|
||||||
|
max-width: 400px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
& .value {
|
||||||
|
max-width: 200px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
& > .actions {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& > .notifications-wrapper {
|
||||||
|
& > .notification {
|
||||||
|
padding: 0.3em;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid var(--color1);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 0 10px #ccc;
|
||||||
|
transition: opacity var(--fade), top var(--fade);
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background-color: color(var(--status-red) a(90%));
|
||||||
|
border: 1px solid var(--status-red);
|
||||||
|
}
|
||||||
|
&.success {
|
||||||
|
background-color: color(var(--status-green) a(90%));
|
||||||
|
border: 1px solid var(--status-green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& > nav {
|
||||||
|
display: inline-block;
|
||||||
|
overflow: visible;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 10px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
margin: 0 2px;
|
||||||
|
background-color: var(--color1);
|
||||||
|
display: inline-block;
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
border-top: 1px solid var(--color1);
|
||||||
|
border-left: 1px solid var(--color1);
|
||||||
|
border-right: 1px solid var(--color1);
|
||||||
|
transition: background-color var(--fade);
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > a,
|
||||||
|
& > a:visited,
|
||||||
|
& > a:hover {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 140px;
|
||||||
|
color: var(--color4);
|
||||||
|
text-decoration: none;
|
||||||
|
background: none;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .logo {
|
||||||
|
margin: 10px;
|
||||||
|
display: inline;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .user-menu {
|
||||||
|
float: right;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#side-menu {
|
||||||
|
width: 200px;
|
||||||
|
min-width: 200px;
|
||||||
|
& > ul {
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
& > a {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
background-color: var(--color3);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color4);
|
||||||
|
color: var(--color3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 10px;
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.table {
|
||||||
|
border-spacing: 0;
|
||||||
|
text-align: left;
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
& table.table {
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
& th {
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--color4);
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 2px solid var(--color4);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& td {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& tfoot td > a {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.highlight > tbody > tr {
|
||||||
|
& > td {
|
||||||
|
border-bottom: 1px dotted var(--color1);
|
||||||
|
}
|
||||||
|
transition: background-color var(--fade);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& tbody > tr.empty {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--disabled);
|
||||||
|
&:hover {
|
||||||
|
background-color: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--color4);
|
||||||
|
font-size: 130%;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: var(--color5);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 120%;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: var(--color5);
|
||||||
|
font-size: 110%;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.error {
|
||||||
|
color: #a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove button padding in FF */
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="button"],
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: var(--color4);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color var(--fade), color var(--fade);
|
||||||
|
&.primary {
|
||||||
|
border: 1px solid var(--color1);
|
||||||
|
background-color: var(--color3);
|
||||||
|
}
|
||||||
|
&.critical {
|
||||||
|
border: 1px solid var(--color1);
|
||||||
|
background-color: var(--color3);
|
||||||
|
color: #700;
|
||||||
|
}
|
||||||
|
&:hover:enabled {
|
||||||
|
background-color: var(--color4);
|
||||||
|
color: var(--color3);
|
||||||
|
}
|
||||||
|
&:disabled {
|
||||||
|
color: var(--disabled);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror,
|
||||||
|
textarea,
|
||||||
|
select,
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid var(--color1);
|
||||||
|
background-color: var(--color3);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: border-color var(--fade), background-color var(--fade);
|
||||||
|
&.error {
|
||||||
|
background-color: #efcbcf;
|
||||||
|
border-color: #e6b2b7;
|
||||||
|
}
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
& > input {
|
||||||
|
font-size: 90%;
|
||||||
|
width: 30em;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-overview {
|
||||||
|
& > h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-chart-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
& h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.parameter-list {
|
||||||
|
border-spacing: 0;
|
||||||
|
text-align: left;
|
||||||
|
& th {
|
||||||
|
padding: 0;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-dot {
|
||||||
|
& > svg {
|
||||||
|
margin-bottom: -0.125em;
|
||||||
|
margin-left: 0.2em;
|
||||||
|
margin-right: 0.2em;
|
||||||
|
& > circle {
|
||||||
|
stroke: var(--color5);
|
||||||
|
stroke-width: 0.08em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.inform {
|
||||||
|
& > * {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart {
|
||||||
|
margin: 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
& > svg {
|
||||||
|
& > path {
|
||||||
|
stroke: var(--color1);
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
transition: opacity var(--fade);
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
& > text {
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > path {
|
||||||
|
stroke: var(--color4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .legend {
|
||||||
|
text-align: left;
|
||||||
|
margin: 15px;
|
||||||
|
|
||||||
|
& > .legend-line {
|
||||||
|
& > .color {
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border: 1px solid var(--color1);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.na {
|
||||||
|
color: var(--disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-parameters {
|
||||||
|
& > input {
|
||||||
|
font-family: monospace;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
padding: 4px;
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .parameter-list {
|
||||||
|
font-family: monospace;
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: 200px;
|
||||||
|
|
||||||
|
& > table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-spacing: 0;
|
||||||
|
& > tbody {
|
||||||
|
& > tr {
|
||||||
|
& > td {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-bottom: 1px dotted var(--color1);
|
||||||
|
}
|
||||||
|
transition: background-color var(--fade);
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color3);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > td.left {
|
||||||
|
& > * {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& > td.right {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-right {
|
||||||
|
float: right;
|
||||||
|
clear: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-full-width {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-bar {
|
||||||
|
margin-top: 10px;
|
||||||
|
& > button {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.tag {
|
||||||
|
display: inline-block;
|
||||||
|
background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="190" height="16"><path d="M6.59 15.42l-6-6a2 2 0 0 1 0-2.83l6-6A2 2 0 0 1 8 0h180a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-1.41-.58zM4.35 6.59A2 2 0 1 0 7.17 9.4 2 2 0 0 0 4.35 6.6z" fill="#e8d5b2"></path></svg>');
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
& > .tag {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 100;
|
||||||
|
transition: opacity var(--fade), top var(--fade);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
& > .overlay {
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--color1);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 0 15px #aaa;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay > .put-form {
|
||||||
|
min-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
box-shadow: 0 0 10px #ccc;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid var(--color1);
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: opacity var(--fade);
|
||||||
|
& > .suggestion {
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.long-text {
|
||||||
|
display: inline-block;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 400px;
|
||||||
|
/*
|
||||||
|
"overflow: hidden" changes the baseline as explained here:
|
||||||
|
https://stackoverflow.com/questions/9273016/why-is-this-inline-block-element-pushed-downward
|
||||||
|
*/
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.long-text-overflowed {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.long-text {
|
||||||
|
font-family: monospace;
|
||||||
|
min-width: 10vw;
|
||||||
|
min-height: 10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.parameter-value {
|
||||||
|
& > span:empty::before {
|
||||||
|
content: "blank";
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+450
@@ -0,0 +1,450 @@
|
|||||||
|
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in
|
||||||
|
* IE on Windows Phone and in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers (opinionated).
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
* 1. Add the correct display in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
main {
|
||||||
|
/* 1 */
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct margin in IE 8.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 1em 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the gray background on active links in IE 10.
|
||||||
|
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent; /* 1 */
|
||||||
|
-webkit-text-decoration-skip: objects; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57- and Firefox 39-.
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
text-decoration: underline dotted; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font style in Android 4.3-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dfn {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct background and color in IE 9-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background-color: #ff0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in iOS 4-7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the overflow in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
svg:not(:root) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers (opinionated).
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: sans-serif; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
/* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
|
||||||
|
* controls in Android 4.
|
||||||
|
* 2. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html [type="button"],
|
||||||
|
/* 1 */ [type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct display in IE 9-.
|
||||||
|
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
display: inline-block; /* 1 */
|
||||||
|
vertical-align: baseline; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10-.
|
||||||
|
* 2. Remove the padding in IE 10-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-cancel-button,
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
* 1. Add the correct display in Edge, IE, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details,
|
||||||
|
/* 1 */ menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scripting
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hidden
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "./components";
|
||||||
|
import config from "./config";
|
||||||
|
import * as store from "./store";
|
||||||
|
|
||||||
|
export function init(args): Promise<{}> {
|
||||||
|
if (!window.authorizer.hasAccess("devices", 2)) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("You are not authorized to view this page")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
deviceId: args.id,
|
||||||
|
deviceFilter: ["=", ["PARAM", "DeviceID.ID"], args.id]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
|
||||||
|
|
||||||
|
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]).value;
|
||||||
|
if (!dev.length) return "Loading";
|
||||||
|
const conf = config.ui.device;
|
||||||
|
const cmps = [];
|
||||||
|
|
||||||
|
for (const c of Object.values(conf))
|
||||||
|
cmps.push(m.context({ device: dev[0] }, c["type"], c));
|
||||||
|
|
||||||
|
return [m("h1", vnode.attrs["deviceId"]), cmps];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
|
import { m } from "./components";
|
||||||
|
import config from "./config";
|
||||||
|
import filterComponent from "./filter-component";
|
||||||
|
import * as store from "./store";
|
||||||
|
import * as taskQueue from "./task-queue";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
import { evaluate, extractParams } from "../lib/common/expression";
|
||||||
|
import { parse, stringify } from "../lib/common/expression-parser";
|
||||||
|
import memoize from "../lib/common/memoize";
|
||||||
|
import * as smartQuery from "./smart-query";
|
||||||
|
import * as expressionParser from "../lib/common/expression-parser";
|
||||||
|
|
||||||
|
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||||
|
|
||||||
|
const memoizedParse = memoize(parse);
|
||||||
|
const memoizedJsonParse = memoize(JSON.parse);
|
||||||
|
const memoizedGetSortable = memoize(p => {
|
||||||
|
const expressionParams = extractParams(p);
|
||||||
|
if (expressionParams.length === 1) {
|
||||||
|
const param = evaluate(expressionParams[0]);
|
||||||
|
if (typeof param === "string") return param;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDownloadUrl = memoize((filter, indexParameters) => {
|
||||||
|
const columns = {};
|
||||||
|
for (const p of indexParameters) columns[p.label] = stringify(p.parameter);
|
||||||
|
return `/api/devices.csv?${m.buildQueryString({
|
||||||
|
filter: stringify(filter),
|
||||||
|
columns: JSON.stringify(columns)
|
||||||
|
})}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const unpackSmartQuery = memoize(query => {
|
||||||
|
return expressionParser.map(query, e => {
|
||||||
|
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||||
|
return smartQuery.unpack("devices", e[2], e[3]);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export function init(args): Promise<{}> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!window.authorizer.hasAccess("devices", 2))
|
||||||
|
return void reject(new Error("You are not authorized to view this page"));
|
||||||
|
|
||||||
|
const filter = args.filter;
|
||||||
|
const sort = args.sort;
|
||||||
|
const indexParameters = Object.values(config.ui.index);
|
||||||
|
resolve({ filter, indexParameters, sort });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(
|
||||||
|
devicesResponse,
|
||||||
|
parameters,
|
||||||
|
total,
|
||||||
|
selected,
|
||||||
|
showMoreCallback,
|
||||||
|
downloadUrl,
|
||||||
|
sort,
|
||||||
|
onSortChange
|
||||||
|
): Children {
|
||||||
|
const devices = devicesResponse.value;
|
||||||
|
const selectAll = m("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
checked: devices.length && selected.size === devices.length,
|
||||||
|
onchange: e => {
|
||||||
|
for (const d of devices) {
|
||||||
|
if (e.target.checked) selected.add(d["DeviceID.ID"].value[0]);
|
||||||
|
else selected.delete(d["DeviceID.ID"].value[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disabled: !total
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = [m("th", selectAll)];
|
||||||
|
for (const param of parameters) {
|
||||||
|
const label = param.label;
|
||||||
|
let _param;
|
||||||
|
if (!param.unsortable && (_param = memoizedGetSortable(param.parameter))) {
|
||||||
|
let direction = 1;
|
||||||
|
|
||||||
|
let symbol = "\u2981";
|
||||||
|
if (sort[_param] > 0) symbol = "\u2bc6";
|
||||||
|
else if (sort[_param] < 0) symbol = "\u2bc5";
|
||||||
|
|
||||||
|
const sortable = m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onclick: () => {
|
||||||
|
if (sort[_param] > 0) direction *= -1;
|
||||||
|
return onSortChange(JSON.stringify({ [_param]: direction }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
symbol
|
||||||
|
);
|
||||||
|
|
||||||
|
labels.push(m("th", [label, sortable]));
|
||||||
|
} else {
|
||||||
|
labels.push(m("th", label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const device of devices) {
|
||||||
|
const checkbox = m("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
checked: selected.has(device["DeviceID.ID"].value[0]),
|
||||||
|
onchange: e => {
|
||||||
|
if (e.target.checked) selected.add(device["DeviceID.ID"].value[0]);
|
||||||
|
else selected.delete(device["DeviceID.ID"].value[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
m(
|
||||||
|
"tr",
|
||||||
|
{
|
||||||
|
onclick: e => {
|
||||||
|
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) {
|
||||||
|
e.redraw = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected.delete(device["DeviceID.ID"].value[0]))
|
||||||
|
selected.add(device["DeviceID.ID"].value[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
m("td", checkbox),
|
||||||
|
parameters.map(p => {
|
||||||
|
const comp = m.context(
|
||||||
|
{ device: device, parameter: p.parameter },
|
||||||
|
p.type || "parameter",
|
||||||
|
p
|
||||||
|
);
|
||||||
|
return m("td", comp);
|
||||||
|
}),
|
||||||
|
m(
|
||||||
|
"td.table-row-links",
|
||||||
|
m(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
href: `#!/devices/${encodeURIComponent(
|
||||||
|
device["DeviceID.ID"].value[0]
|
||||||
|
)}`
|
||||||
|
},
|
||||||
|
"Show"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
rows.push(
|
||||||
|
m("tr.empty", m("td", { colspan: parameters.length + 1 }, "No devices"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const footerElements = [];
|
||||||
|
if (total != null) footerElements.push(`${devices.length}/${total}`);
|
||||||
|
else footerElements.push(`${devices.length}`);
|
||||||
|
|
||||||
|
footerElements.push(
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Show more devices",
|
||||||
|
onclick: showMoreCallback,
|
||||||
|
disabled: devices.length >= total || !devicesResponse.fulfilled
|
||||||
|
},
|
||||||
|
"More"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (downloadUrl) {
|
||||||
|
footerElements.push(
|
||||||
|
m("a.download-csv", { href: downloadUrl, download: "" }, "Download")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tfoot = m(
|
||||||
|
"tfoot",
|
||||||
|
m("tr", m("td", { colspan: labels.length }, footerElements))
|
||||||
|
);
|
||||||
|
|
||||||
|
return m("table.table.highlight", [
|
||||||
|
m("thead", m("tr", labels)),
|
||||||
|
m("tbody", rows),
|
||||||
|
tfoot
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderActions(selected): Children {
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Reboot selected devices",
|
||||||
|
disabled: !selected.size,
|
||||||
|
onclick: () => {
|
||||||
|
for (const d of selected) {
|
||||||
|
taskQueue.queueTask({
|
||||||
|
name: "reboot",
|
||||||
|
device: d
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Reboot"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.critical",
|
||||||
|
{
|
||||||
|
title: "Factory reset selected devices",
|
||||||
|
disabled: !selected.size,
|
||||||
|
onclick: () => {
|
||||||
|
for (const d of selected) {
|
||||||
|
taskQueue.queueTask({
|
||||||
|
name: "factoryReset",
|
||||||
|
device: d
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Reset"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Delete selected devices",
|
||||||
|
disabled: !selected.size,
|
||||||
|
onclick: () => {
|
||||||
|
const ids = Array.from(selected);
|
||||||
|
if (!confirm(`Deleting ${ids.length} devices. Are you sure?`)) return;
|
||||||
|
|
||||||
|
let counter = 1;
|
||||||
|
for (const id of ids) {
|
||||||
|
++counter;
|
||||||
|
store
|
||||||
|
.deleteResource("devices", id)
|
||||||
|
.then(() => {
|
||||||
|
notifications.push("success", `${id}: Deleted`);
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Delete"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Tag selected devices",
|
||||||
|
disabled: !selected.size,
|
||||||
|
onclick: () => {
|
||||||
|
const ids = Array.from(selected);
|
||||||
|
const tag = prompt(`Enter tag to assign to ${ids.length} devices:`);
|
||||||
|
if (!tag) return;
|
||||||
|
|
||||||
|
let counter = 1;
|
||||||
|
for (const id of ids) {
|
||||||
|
++counter;
|
||||||
|
store
|
||||||
|
.updateTags(id, { [tag]: true })
|
||||||
|
.then(() => {
|
||||||
|
notifications.push("success", `${id}: Tags updated`);
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Tag"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Untag selected devices",
|
||||||
|
disabled: !selected.size,
|
||||||
|
onclick: () => {
|
||||||
|
const ids = Array.from(selected);
|
||||||
|
const tag = prompt(
|
||||||
|
`Enter tag to unassign from ${ids.length} devices:`
|
||||||
|
);
|
||||||
|
if (!tag) return;
|
||||||
|
|
||||||
|
let counter = 1;
|
||||||
|
for (const id of ids) {
|
||||||
|
++counter;
|
||||||
|
store
|
||||||
|
.updateTags(id, { [tag]: false })
|
||||||
|
.then(() => {
|
||||||
|
notifications.push("success", `${id}: Tags updated`);
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (--counter === 0) store.fulfill(0, Date.now());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Untag"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return m(".actions-bar", buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
document.title = "Devices - GenieACS";
|
||||||
|
|
||||||
|
function showMore(): void {
|
||||||
|
vnode.state["showCount"] =
|
||||||
|
(vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilterChanged(filter): void {
|
||||||
|
const ops = { filter };
|
||||||
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
|
m.route.set("/devices", ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSortChange(sort): void {
|
||||||
|
const ops = { sort };
|
||||||
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
|
m.route.set("/devices", ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = vnode.attrs["sort"]
|
||||||
|
? memoizedJsonParse(vnode.attrs["sort"])
|
||||||
|
: {};
|
||||||
|
let filter = vnode.attrs["filter"]
|
||||||
|
? memoizedParse(vnode.attrs["filter"])
|
||||||
|
: true;
|
||||||
|
filter = unpackSmartQuery(filter);
|
||||||
|
|
||||||
|
const devs = store.fetch("devices", filter, {
|
||||||
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
const count = store.count("devices", filter);
|
||||||
|
|
||||||
|
const selected = new Set();
|
||||||
|
if (vnode.state["selected"]) {
|
||||||
|
for (const d of devs.value) {
|
||||||
|
if (vnode.state["selected"].has(d["DeviceID.ID"].value[0]))
|
||||||
|
selected.add(d["DeviceID.ID"].value[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vnode.state["selected"] = selected;
|
||||||
|
|
||||||
|
const downloadUrl = getDownloadUrl(
|
||||||
|
filter,
|
||||||
|
vnode.attrs["indexParameters"]
|
||||||
|
);
|
||||||
|
|
||||||
|
const attrs = {};
|
||||||
|
attrs["resource"] = "devices";
|
||||||
|
attrs["filter"] = vnode.attrs["filter"];
|
||||||
|
attrs["onChange"] = onFilterChanged;
|
||||||
|
|
||||||
|
return [
|
||||||
|
m("h1", "Listing devices"),
|
||||||
|
m(filterComponent, attrs),
|
||||||
|
renderTable(
|
||||||
|
devs,
|
||||||
|
vnode.attrs["indexParameters"],
|
||||||
|
count.value,
|
||||||
|
selected,
|
||||||
|
showMore,
|
||||||
|
downloadUrl,
|
||||||
|
sort,
|
||||||
|
onSortChange
|
||||||
|
),
|
||||||
|
renderActions(selected)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
import m, {
|
||||||
|
Children,
|
||||||
|
Child,
|
||||||
|
ClosureComponent,
|
||||||
|
Component,
|
||||||
|
VnodeDOM
|
||||||
|
} from "mithril";
|
||||||
|
import * as taskQueue from "./task-queue";
|
||||||
|
import * as store from "./store";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
|
||||||
|
function mparam(param): Children {
|
||||||
|
return m("span.parameter", { title: param }, `${param}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mval(val): Children {
|
||||||
|
return m("span.value", { title: val }, `${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStagingSpv(task, queueFunc, cancelFunc): Children {
|
||||||
|
function keydown(e): void {
|
||||||
|
if (e.keyCode === 13) queueFunc();
|
||||||
|
else if (e.keyCode === 27) cancelFunc();
|
||||||
|
else e.redraw = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input;
|
||||||
|
if (task.parameterValues[0][2] === "xsd:boolean") {
|
||||||
|
input = m(
|
||||||
|
"select",
|
||||||
|
{
|
||||||
|
value: task.parameterValues[0][1].toString(),
|
||||||
|
onchange: e => {
|
||||||
|
e.redraw = false;
|
||||||
|
task.parameterValues[0][1] = input.dom.value;
|
||||||
|
},
|
||||||
|
onkeydown: keydown,
|
||||||
|
oncreate: vnode => {
|
||||||
|
(vnode.dom as HTMLSelectElement).focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
m("option", { value: "true" }, "true"),
|
||||||
|
m("option", { value: "false" }, "false")
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
input = m("input", {
|
||||||
|
type: ["xsd:dateTime", "xsd:int", "xsd:unsignedInt"].includes(
|
||||||
|
task.parameterValues[0][2]
|
||||||
|
)
|
||||||
|
? "number"
|
||||||
|
: "text",
|
||||||
|
value: task.parameterValues[0][1],
|
||||||
|
oninput: e => {
|
||||||
|
e.redraw = false;
|
||||||
|
task.parameterValues[0][1] = input.dom.value;
|
||||||
|
},
|
||||||
|
onkeydown: keydown,
|
||||||
|
oncreate: vnode => {
|
||||||
|
(vnode.dom as HTMLInputElement).focus();
|
||||||
|
(vnode.dom as HTMLInputElement).select();
|
||||||
|
// Need to prevent scrolling on focus because
|
||||||
|
// we're animating height and using overflow: hidden
|
||||||
|
(vnode.dom.parentNode.parentNode as Element).scrollTop = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [m("span", "Editing ", mparam(task.parameterValues[0][0])), input];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStagingDownload(task): Children {
|
||||||
|
task.invalid = !task.fileName || !task.fileType;
|
||||||
|
const files = store.fetch("files", true);
|
||||||
|
const idParts = task.device.split("-");
|
||||||
|
const oui = decodeURIComponent(idParts[0]);
|
||||||
|
const productClass =
|
||||||
|
idParts.length === 3 ? decodeURIComponent(idParts[1]) : "";
|
||||||
|
|
||||||
|
const typesList = [
|
||||||
|
"",
|
||||||
|
"1 Firmware Upgrade Image",
|
||||||
|
"2 Web Content",
|
||||||
|
"3 Vendor Configuration File",
|
||||||
|
"4 Tone File",
|
||||||
|
"5 Ringer File"
|
||||||
|
].map(t =>
|
||||||
|
m(
|
||||||
|
"option",
|
||||||
|
{
|
||||||
|
disabled: !t,
|
||||||
|
value: t,
|
||||||
|
selected: (task.fileType || "") === t,
|
||||||
|
onclick: () => {
|
||||||
|
task.fileType = t;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
t
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filesList = [""]
|
||||||
|
.concat(
|
||||||
|
files.value
|
||||||
|
.filter(
|
||||||
|
f =>
|
||||||
|
(!f["metadata.oui"] || f["metadata.oui"] === oui) &&
|
||||||
|
(!f["metadata.productClass"] ||
|
||||||
|
f["metadata.productClass"] === productClass)
|
||||||
|
)
|
||||||
|
.map(f => f._id)
|
||||||
|
)
|
||||||
|
.map(f =>
|
||||||
|
m(
|
||||||
|
"option",
|
||||||
|
{
|
||||||
|
disabled: !f,
|
||||||
|
value: f,
|
||||||
|
selected: (task.fileName || "") === f,
|
||||||
|
onclick: () => {
|
||||||
|
task.fileName = f;
|
||||||
|
task.fileType = "";
|
||||||
|
for (const file of files.value)
|
||||||
|
if (file._id === f) task.fileType = file["metadata.fileType"];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
f
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
"Push ",
|
||||||
|
m(
|
||||||
|
"select",
|
||||||
|
{ disabled: files.fulfilling, style: "width: 350px" },
|
||||||
|
filesList
|
||||||
|
),
|
||||||
|
" as ",
|
||||||
|
m("select", typesList)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStaging(staging): Child[] {
|
||||||
|
const elements: Child[] = [];
|
||||||
|
|
||||||
|
for (const s of staging) {
|
||||||
|
const queueFunc = (): void => {
|
||||||
|
staging.delete(s);
|
||||||
|
taskQueue.queueTask(s);
|
||||||
|
};
|
||||||
|
const cancelFunc = (): void => {
|
||||||
|
staging.delete(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
let elms;
|
||||||
|
if (s.name === "setParameterValues")
|
||||||
|
elms = renderStagingSpv(s, queueFunc, cancelFunc);
|
||||||
|
else if (s.name === "download") elms = renderStagingDownload(s);
|
||||||
|
|
||||||
|
const queue = m(
|
||||||
|
"button.primary",
|
||||||
|
{ title: "Queue task", onclick: queueFunc, disabled: s.invalid },
|
||||||
|
"Queue"
|
||||||
|
);
|
||||||
|
const cancel = m(
|
||||||
|
"button",
|
||||||
|
{ title: "Cancel edit", onclick: cancelFunc },
|
||||||
|
"Cancel"
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.push(m(".staging", elms, m("div.actions", queue, cancel)));
|
||||||
|
}
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderQueue(queue): Child[] {
|
||||||
|
const details: Child[] = [];
|
||||||
|
const devices: { [deviceId: string]: any[] } = {};
|
||||||
|
for (const t of queue) {
|
||||||
|
devices[t.device] = devices[t.device] || [];
|
||||||
|
devices[t.device].push(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(devices)) {
|
||||||
|
details.push(m("strong", k));
|
||||||
|
for (const t of v) {
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
if (t.status === "fault" || t.status === "stale") {
|
||||||
|
actions.push(
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Retry this task",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.queueTask(t);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"↺"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push(
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Remove this task",
|
||||||
|
onclick: () => {
|
||||||
|
taskQueue.deleteTask(t);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"✕"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (t.name === "setParameterValues") {
|
||||||
|
details.push(
|
||||||
|
m(
|
||||||
|
`div.${t.status}`,
|
||||||
|
m(
|
||||||
|
"span",
|
||||||
|
"Set ",
|
||||||
|
mparam(t.parameterValues[0][0]),
|
||||||
|
" to '",
|
||||||
|
mval(t.parameterValues[0][1]),
|
||||||
|
"'"
|
||||||
|
),
|
||||||
|
m(".actions", actions)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (t.name === "refreshObject") {
|
||||||
|
details.push(
|
||||||
|
m(
|
||||||
|
`div.${t.status}`,
|
||||||
|
m("span", "Refresh ", mparam(t.parameterName)),
|
||||||
|
m(".actions", actions)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (t.name === "reboot") {
|
||||||
|
details.push(m(`div.${t.status}`, "Reboot", m(".actions", actions)));
|
||||||
|
} else if (t.name === "factoryReset") {
|
||||||
|
details.push(
|
||||||
|
m(`div.${t.status}`, "Factory reset", m(".actions", actions))
|
||||||
|
);
|
||||||
|
} else if (t.name === "addObject") {
|
||||||
|
details.push(
|
||||||
|
m(
|
||||||
|
`div.${t.status}`,
|
||||||
|
m("span", "Add ", mparam(t.objectName)),
|
||||||
|
m(".actions", actions)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (t.name === "deleteObject") {
|
||||||
|
details.push(
|
||||||
|
m(
|
||||||
|
`div.${t.status}`,
|
||||||
|
m("span", "Delete ", mparam(t.objectName)),
|
||||||
|
m(".actions", actions)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (t.name === "getParameterValues") {
|
||||||
|
details.push(
|
||||||
|
m(
|
||||||
|
`div.${t.status}`,
|
||||||
|
`Refresh ${t.parameterNames.length} parameters`,
|
||||||
|
m(".actions", actions)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (t.name === "download") {
|
||||||
|
details.push(
|
||||||
|
m(
|
||||||
|
`div.${t.status}`,
|
||||||
|
`Push file: ${t.fileName} (${t.fileType})`,
|
||||||
|
m(".actions", actions)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
details.push(m(`div.${t.status}`, t.name, m(".actions", actions)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNotifications(notifs): Child[] {
|
||||||
|
const notificationElements: Child[] = [];
|
||||||
|
|
||||||
|
for (const n of notifs) {
|
||||||
|
notificationElements.push(
|
||||||
|
m(
|
||||||
|
"div.notification",
|
||||||
|
{
|
||||||
|
class: n.type,
|
||||||
|
style: "position: absolute;opacity: 0",
|
||||||
|
oncreate: vnode => {
|
||||||
|
(vnode.dom as HTMLDivElement).style.opacity = "1";
|
||||||
|
},
|
||||||
|
onbeforeremove: vnode => {
|
||||||
|
(vnode.dom as HTMLDivElement).style.opacity = "0";
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
key: n.timestamp
|
||||||
|
},
|
||||||
|
n.message
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return notificationElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
const queue = taskQueue.getQueue();
|
||||||
|
const staging = taskQueue.getStaging();
|
||||||
|
const notifs = notifications.getNotifications();
|
||||||
|
|
||||||
|
let drawerElement, statusElement;
|
||||||
|
const notificationElements = renderNotifications(notifs);
|
||||||
|
const stagingElements = renderStaging(staging);
|
||||||
|
const queueElements = renderQueue(queue);
|
||||||
|
|
||||||
|
function repositionNotifications(): void {
|
||||||
|
let top = 10;
|
||||||
|
for (const c of notificationElements as VnodeDOM[]) {
|
||||||
|
(c.dom as HTMLDivElement).style.top = `${top}`;
|
||||||
|
top += (c.dom as HTMLDivElement).offsetHeight + 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resizeDrawer(): void {
|
||||||
|
let height =
|
||||||
|
statusElement.dom.offsetTop + statusElement.dom.offsetHeight;
|
||||||
|
if (stagingElements.length) {
|
||||||
|
for (const s of stagingElements as VnodeDOM[]) {
|
||||||
|
height = Math.max(
|
||||||
|
height,
|
||||||
|
(s.dom as HTMLDivElement).offsetTop +
|
||||||
|
(s.dom as HTMLDivElement).offsetHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (vnode.state["mouseIn"]) {
|
||||||
|
for (const c of drawerElement.children)
|
||||||
|
height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight);
|
||||||
|
}
|
||||||
|
drawerElement.dom.style.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stagingElements.length + queueElements.length) {
|
||||||
|
const statusCount = { queued: 0, pending: 0, fault: 0, stale: 0 };
|
||||||
|
for (const t of queue) statusCount[t["status"]] += 1;
|
||||||
|
|
||||||
|
const actions = m(
|
||||||
|
".actions",
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Commit queued tasks",
|
||||||
|
disabled: !statusCount.queued,
|
||||||
|
onclick: () => {
|
||||||
|
const tasks = Array.from(taskQueue.getQueue()).filter(
|
||||||
|
t => t["status"] === "queued"
|
||||||
|
);
|
||||||
|
taskQueue
|
||||||
|
.commit(
|
||||||
|
tasks,
|
||||||
|
(deviceId, err, connectionRequestStatus, tasks2) => {
|
||||||
|
if (err) {
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
`${deviceId}: ${err.message}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionRequestStatus !== "OK") {
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
`${deviceId}: ${connectionRequestStatus}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const t of tasks2) {
|
||||||
|
if (t.status === "stale") {
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
`${deviceId}: No contact from device`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (t.status === "fault") {
|
||||||
|
notifications.push(
|
||||||
|
"error",
|
||||||
|
`${deviceId}: Task(s) faulted`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.push(
|
||||||
|
"success",
|
||||||
|
`${deviceId}: Task(s) committed`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Commit"
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Clear tasks",
|
||||||
|
onclick: taskQueue.clear,
|
||||||
|
disabled: !queueElements.length
|
||||||
|
},
|
||||||
|
"Clear"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
statusElement = m(
|
||||||
|
".status",
|
||||||
|
m(
|
||||||
|
"span.queued",
|
||||||
|
{ class: statusCount.queued ? "active" : "" },
|
||||||
|
`Queued: ${statusCount.queued}`
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"span.pending",
|
||||||
|
{ class: statusCount.pending ? "active" : "" },
|
||||||
|
`Pending: ${statusCount.pending}`
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"span.fault",
|
||||||
|
{ class: statusCount.fault ? "active" : "" },
|
||||||
|
`Fault: ${statusCount.fault}`
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"span.stale",
|
||||||
|
{ class: statusCount.stale ? "active" : "" },
|
||||||
|
`Stale: ${statusCount.stale}`
|
||||||
|
),
|
||||||
|
actions
|
||||||
|
);
|
||||||
|
|
||||||
|
drawerElement = m(
|
||||||
|
".drawer",
|
||||||
|
{
|
||||||
|
key: "drawer",
|
||||||
|
style: "opacity: 0;height: 0;",
|
||||||
|
oncreate: vnode2 => {
|
||||||
|
vnode.state["mouseIn"] = false;
|
||||||
|
(vnode2.dom as HTMLDivElement).style.opacity = "1";
|
||||||
|
resizeDrawer();
|
||||||
|
},
|
||||||
|
onmouseover: e => {
|
||||||
|
vnode.state["mouseIn"] = true;
|
||||||
|
resizeDrawer();
|
||||||
|
e.redraw = false;
|
||||||
|
},
|
||||||
|
onmouseleave: e => {
|
||||||
|
vnode.state["mouseIn"] = false;
|
||||||
|
resizeDrawer();
|
||||||
|
e.redraw = false;
|
||||||
|
},
|
||||||
|
onupdate: resizeDrawer,
|
||||||
|
onbeforeremove: vnode2 => {
|
||||||
|
(vnode2.dom as HTMLDivElement).onmouseover = null;
|
||||||
|
(vnode2.dom as HTMLDivElement).onmouseleave = null;
|
||||||
|
(vnode2.dom as HTMLDivElement).style.opacity = "0";
|
||||||
|
(vnode2.dom as HTMLDivElement).style.height = "0";
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(resolve, 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusElement,
|
||||||
|
stagingElements.length ? stagingElements : m(".queue", queueElements)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m(
|
||||||
|
"div.drawer-wrapper",
|
||||||
|
drawerElement,
|
||||||
|
m(
|
||||||
|
"div.notifications-wrapper",
|
||||||
|
{
|
||||||
|
key: "notifications",
|
||||||
|
style: "position: relative;",
|
||||||
|
onupdate: repositionNotifications,
|
||||||
|
oncreate: repositionNotifications
|
||||||
|
},
|
||||||
|
notificationElements
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export let codeMirror;
|
||||||
|
export let yaml;
|
||||||
|
|
||||||
|
export function loadCodeMirror(): Promise<void> {
|
||||||
|
if (codeMirror) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const promises = [
|
||||||
|
import(/* webpackChunkName: "codemirror" */ "codemirror"),
|
||||||
|
import(/* webpackChunkName: "codemirror" */ "codemirror/mode/javascript/javascript"),
|
||||||
|
import(/* webpackChunkName: "codemirror" */ "codemirror/mode/yaml/yaml")
|
||||||
|
];
|
||||||
|
Promise.all(promises)
|
||||||
|
.then(modules => {
|
||||||
|
codeMirror = modules[0];
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadYaml(): Promise<void> {
|
||||||
|
if (yaml) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
import(/* webpackChunkName: "yaml" */ "yaml")
|
||||||
|
.then(module => {
|
||||||
|
yaml = module;
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ClosureComponent, Component } from "mithril";
|
||||||
|
import { m } from "./components";
|
||||||
|
|
||||||
|
export const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: function(vnode) {
|
||||||
|
document.title = "Error! - GenieACS";
|
||||||
|
return m("p.error", vnode.attrs["error"]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
import { ClosureComponent, Component, Children } from "mithril";
|
||||||
|
import { m } from "./components";
|
||||||
|
import config from "./config";
|
||||||
|
import filterComponent from "./filter-component";
|
||||||
|
import * as store from "./store";
|
||||||
|
import * as notifications from "./notifications";
|
||||||
|
import memoize from "../lib/common/memoize";
|
||||||
|
import * as smartQuery from "./smart-query";
|
||||||
|
import { map, parse, stringify } from "../lib/common/expression-parser";
|
||||||
|
import { loadYaml, yaml } from "./dynamic-loader";
|
||||||
|
|
||||||
|
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||||
|
|
||||||
|
const memoizedParse = memoize(parse);
|
||||||
|
const memoizedJsonParse = memoize(JSON.parse);
|
||||||
|
|
||||||
|
const attributes = [
|
||||||
|
{ id: "device", label: "Device" },
|
||||||
|
{ id: "channel", label: "Channel" },
|
||||||
|
{ id: "code", label: "Code" },
|
||||||
|
{ id: "message", label: "Message" },
|
||||||
|
{ id: "detail", label: "Detail", unsortable: true },
|
||||||
|
{ id: "retries", label: "Retries" },
|
||||||
|
{ id: "timestamp", label: "Timestamp" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const getDownloadUrl = memoize(filter => {
|
||||||
|
const cols = {};
|
||||||
|
for (const attr of attributes) {
|
||||||
|
cols[attr.label] =
|
||||||
|
attr.id === "timestamp" ? `DATE_STRING(${attr.id})` : attr.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/api/faults.csv?${m.buildQueryString({
|
||||||
|
filter: stringify(filter),
|
||||||
|
columns: JSON.stringify(cols)
|
||||||
|
})}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const unpackSmartQuery = memoize(query => {
|
||||||
|
return map(query, e => {
|
||||||
|
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||||
|
return smartQuery.unpack("faults", e[2], e[3]);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export function init(args): Promise<{}> {
|
||||||
|
if (!window.authorizer.hasAccess("faults", 2)) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("You are not authorized to view this page")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = args.sort;
|
||||||
|
const filter = args.filter;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
loadYaml()
|
||||||
|
.then(() => {
|
||||||
|
resolve({ filter, sort });
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(
|
||||||
|
faultsResponse,
|
||||||
|
total,
|
||||||
|
selected,
|
||||||
|
showMoreCallback,
|
||||||
|
downloadUrl,
|
||||||
|
sort,
|
||||||
|
onSortChange
|
||||||
|
): Children {
|
||||||
|
const faults = faultsResponse.value;
|
||||||
|
const selectAll = m("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
checked: faults.length && selected.size === faults.length,
|
||||||
|
onchange: e => {
|
||||||
|
for (const f of faults) {
|
||||||
|
if (e.target.checked) selected.add(f["_id"]);
|
||||||
|
else selected.delete(f["_id"]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disabled: !total
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = [m("th", selectAll)];
|
||||||
|
for (const attr of attributes) {
|
||||||
|
const label = attr.label;
|
||||||
|
|
||||||
|
if (attr.unsortable) {
|
||||||
|
labels.push(m("th", label));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset direction by 1 since sort["_id"] = -1
|
||||||
|
let direction = 2;
|
||||||
|
|
||||||
|
let symbol = "\u2981";
|
||||||
|
if (sort[attr.id] > 0) symbol = "\u2bc6";
|
||||||
|
else if (sort[attr.id] < 0) symbol = "\u2bc5";
|
||||||
|
|
||||||
|
const sortable = m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onclick: () => {
|
||||||
|
if (sort[attr.id] > 0) direction *= -1;
|
||||||
|
return onSortChange(JSON.stringify({ [attr.id]: direction }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
symbol
|
||||||
|
);
|
||||||
|
|
||||||
|
labels.push(m("th", [label, sortable]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (const f of faults) {
|
||||||
|
const checkbox = m("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
checked: selected.has(f["_id"]),
|
||||||
|
onchange: e => {
|
||||||
|
if (e.target.checked) selected.add(f["_id"]);
|
||||||
|
else selected.delete(f["_id"]);
|
||||||
|
},
|
||||||
|
onclick: e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.redraw = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceHref = `#!/devices/${encodeURIComponent(f["device"])}`;
|
||||||
|
|
||||||
|
const tds = [m("td", checkbox)];
|
||||||
|
for (const attr of attributes) {
|
||||||
|
if (attr.id === "device")
|
||||||
|
tds.push(m("td", m("a", { href: deviceHref }, f[attr.id])));
|
||||||
|
else if (attr.id === "timestamp")
|
||||||
|
tds.push(m("td", new Date(f[attr.id]).toLocaleString()));
|
||||||
|
else if (attr.id === "detail")
|
||||||
|
tds.push(m("td", m("long-text", { text: yaml.stringify(f[attr.id]) })));
|
||||||
|
else tds.push(m("td", f[attr.id]));
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
m(
|
||||||
|
"tr",
|
||||||
|
{
|
||||||
|
onclick: e => {
|
||||||
|
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) {
|
||||||
|
e.redraw = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected.delete(f["_id"])) selected.add(f["_id"]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tds
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows.length)
|
||||||
|
rows.push(m("tr.empty", m("td", { colspan: 7 }, "No faults")));
|
||||||
|
|
||||||
|
const footerElements = [];
|
||||||
|
if (total != null) footerElements.push(`${faults.length}/${total}`);
|
||||||
|
else footerElements.push(`${faults.length}`);
|
||||||
|
|
||||||
|
footerElements.push(
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
title: "Show more faults",
|
||||||
|
onclick: showMoreCallback,
|
||||||
|
disabled: faults.length >= total || !faultsResponse.fulfilled
|
||||||
|
},
|
||||||
|
"More"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (downloadUrl) {
|
||||||
|
footerElements.push(
|
||||||
|
m("a.download-csv", { href: downloadUrl, download: "" }, "Download")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tfoot = m(
|
||||||
|
"tfoot",
|
||||||
|
m("tr", m("td", { colspan: labels.length }, footerElements))
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
m(
|
||||||
|
"button.primary",
|
||||||
|
{
|
||||||
|
title: "Delete selected faults",
|
||||||
|
disabled: !selected.size,
|
||||||
|
onclick: e => {
|
||||||
|
e.redraw = false;
|
||||||
|
e.target.disabled = true;
|
||||||
|
Promise.all(
|
||||||
|
Array.from(selected).map(id => store.deleteResource("faults", id))
|
||||||
|
)
|
||||||
|
.then(res => {
|
||||||
|
notifications.push("success", `${res.length} faults deleted`);
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
notifications.push("error", err.message);
|
||||||
|
store.fulfill(0, Date.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Delete"
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
m(
|
||||||
|
"table.table.highlight",
|
||||||
|
m("thead", m("tr", labels)),
|
||||||
|
m("tbody", rows),
|
||||||
|
tfoot
|
||||||
|
),
|
||||||
|
buttons
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: vnode => {
|
||||||
|
document.title = "Faults - GenieACS";
|
||||||
|
|
||||||
|
function showMore(): void {
|
||||||
|
vnode.state["showCount"] =
|
||||||
|
(vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilterChanged(filter): void {
|
||||||
|
const ops = { filter };
|
||||||
|
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||||
|
m.route.set("/faults", ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSortChange(sort): void {
|
||||||
|
const ops = { sort };
|
||||||
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
|
m.route.set("/faults", ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = vnode.attrs["sort"]
|
||||||
|
? memoizedJsonParse(vnode.attrs["sort"])
|
||||||
|
: {};
|
||||||
|
let filter = vnode.attrs["filter"]
|
||||||
|
? memoizedParse(vnode.attrs["filter"])
|
||||||
|
: true;
|
||||||
|
filter = unpackSmartQuery(filter);
|
||||||
|
|
||||||
|
const faults = store.fetch("faults", filter, {
|
||||||
|
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
const count = store.count("faults", filter);
|
||||||
|
|
||||||
|
const selected = new Set();
|
||||||
|
if (vnode.state["selected"]) {
|
||||||
|
for (const f of faults.value)
|
||||||
|
if (vnode.state["selected"].has(f["_id"])) selected.add(f["_id"]);
|
||||||
|
}
|
||||||
|
vnode.state["selected"] = selected;
|
||||||
|
|
||||||
|
const downloadUrl = getDownloadUrl(filter);
|
||||||
|
|
||||||
|
const attrs = {};
|
||||||
|
attrs["resource"] = "faults";
|
||||||
|
attrs["filter"] = vnode.attrs["filter"];
|
||||||
|
attrs["onChange"] = onFilterChanged;
|
||||||
|
|
||||||
|
return [
|
||||||
|
m("h1", "Listing faults"),
|
||||||
|
m(filterComponent, attrs),
|
||||||
|
renderTable(
|
||||||
|
faults,
|
||||||
|
count.value,
|
||||||
|
selected,
|
||||||
|
showMore,
|
||||||
|
downloadUrl,
|
||||||
|
sort,
|
||||||
|
onSortChange
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user