TypeScript!

This commit is contained in:
Zaid Abdulla committed 2019-02-20 03:36:52 -08:00
1 parent ee8c43747e
commit 5d3c81cb23
105 files changed
+12054 -1901

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
!/bin/.*.ts
!/tools/.*.ts
+7 -4
View File
@@ -3,11 +3,12 @@
"es6": true, "es6": true,
"node": true "node": true
}, },
"parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"ecmaVersion": 8, "project": "./tsconfig.json"
"sourceType": "module"
}, },
"extends": "eslint:recommended", "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 }]
} }
} }
+2
View File
@@ -15,3 +15,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
View File
@@ -17,4 +17,10 @@ config/ext
debug/* debug/*
!debug/.gitkeep !debug/.gitkeep
dist
.eslintrc.json .eslintrc.json
.eslintignore
ava.config.js
tsconfig.json
bin/.*
tools/.*
+6
View File
@@ -0,0 +1,6 @@
export default {
"compileEnhancements": false,
"extensions": ["ts"],
"require": ["esm", "ts-node/register/transpile-only"]
};
+9 -11
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env -S node -r esm #!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
"use strict"; import * as path from "path";
import path from "path"; import * as fs from "fs";
import fs from "fs";
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
@@ -13,7 +12,6 @@ import * as cwmp from "../lib/cwmp";
import * as db from "../lib/db"; import * as db from "../lib/db";
import * as extensions from "../lib/extensions"; import * as extensions from "../lib/extensions";
import * as cache from "../lib/cache"; import * as cache from "../lib/cache";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
logger.init("cwmp", VERSION); logger.init("cwmp", VERSION);
@@ -21,14 +19,14 @@ logger.init("cwmp", VERSION);
const SERVICE_ADDRESS = config.get("CWMP_INTERFACE"); const SERVICE_ADDRESS = config.get("CWMP_INTERFACE");
const SERVICE_PORT = config.get("CWMP_PORT"); const SERVICE_PORT = config.get("CWMP_PORT");
function exitWorkerGracefully() { function exitWorkerGracefully(): void {
db.disconnect(); db.disconnect();
cache.disconnect(); cache.disconnect();
extensions.killAll(); extensions.killAll();
cluster.worker.disconnect(); cluster.worker.disconnect();
} }
function exitWorkerUngracefully() { function exitWorkerUngracefully(): void {
extensions.killAll(() => { extensions.killAll(() => {
process.exit(1); process.exit(1);
}); });
@@ -66,14 +64,14 @@ if (!cluster.worker) {
let ssl; let ssl;
if (config.get("CWMP_SSL")) { if (config.get("CWMP_SSL")) {
ssl = { ssl = {
key: path.resolve(config.get("CONFIG_DIR"), "cwmp.key"), key: path.resolve("" + config.get("CONFIG_DIR"), "cwmp.key"),
cert: path.resolve(config.get("CONFIG_DIR"), "cwmp.crt"), cert: path.resolve("" + config.get("CONFIG_DIR"), "cwmp.crt"),
ca: path.resolve(config.get("CONFIG_DIR"), "cwmp.ca-bundle") ca: path.resolve("" + config.get("CONFIG_DIR"), "cwmp.ca-bundle")
}; };
} }
process.on("uncaughtException", err => { process.on("uncaughtException", err => {
if (err.code === "ERR_IPC_DISCONNECTED") return; if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({ logger.error({
message: "Uncaught exception", message: "Uncaught exception",
exception: err, exception: err,
+6 -4
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env -S node -r esm #!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
/** /**
* Copyright 2013-2018 Zaid Abdulla * Copyright 2013-2018 Zaid Abdulla
* *
@@ -17,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: {
+9 -10
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env -S node -r esm #!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
"use strict"; import * as path from "path";
import path from "path"; import * as fs from "fs";
import fs from "fs";
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
@@ -19,13 +18,13 @@ logger.init("fs", VERSION);
const SERVICE_ADDRESS = config.get("FS_INTERFACE"); const SERVICE_ADDRESS = config.get("FS_INTERFACE");
const SERVICE_PORT = config.get("FS_PORT"); const SERVICE_PORT = config.get("FS_PORT");
function exitWorkerGracefully() { function exitWorkerGracefully(): void {
db.disconnect(); db.disconnect();
cache.disconnect(); cache.disconnect();
cluster.worker.disconnect(); cluster.worker.disconnect();
} }
function exitWorkerUngracefully() { function exitWorkerUngracefully(): void {
process.exit(1); process.exit(1);
} }
@@ -61,14 +60,14 @@ if (!cluster.worker) {
let ssl; let ssl;
if (config.get("FS_SSL")) { if (config.get("FS_SSL")) {
ssl = { ssl = {
key: path.resolve(config.get("CONFIG_DIR"), "fs.key"), key: path.resolve("" + config.get("CONFIG_DIR"), "fs.key"),
cert: path.resolve(config.get("CONFIG_DIR"), "fs.crt"), cert: path.resolve("" + config.get("CONFIG_DIR"), "fs.crt"),
ca: path.resolve(config.get("CONFIG_DIR"), "fs.ca-bundle") ca: path.resolve("" + config.get("CONFIG_DIR"), "fs.ca-bundle")
}; };
} }
process.on("uncaughtException", err => { process.on("uncaughtException", err => {
if (err.code === "ERR_IPC_DISCONNECTED") return; if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({ logger.error({
message: "Uncaught exception", message: "Uncaught exception",
exception: err, exception: err,
+9 -10
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env -S node -r esm #!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
"use strict"; import * as path from "path";
import path from "path"; import * as fs from "fs";
import fs from "fs";
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), "..")); process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
@@ -20,14 +19,14 @@ logger.init("nbi", VERSION);
const SERVICE_ADDRESS = config.get("NBI_INTERFACE"); const SERVICE_ADDRESS = config.get("NBI_INTERFACE");
const SERVICE_PORT = config.get("NBI_PORT"); const SERVICE_PORT = config.get("NBI_PORT");
function exitWorkerGracefully() { function exitWorkerGracefully(): void {
db.disconnect(); db.disconnect();
cache.disconnect(); cache.disconnect();
extensions.killAll(); extensions.killAll();
cluster.worker.disconnect(); cluster.worker.disconnect();
} }
function exitWorkerUngracefully() { function exitWorkerUngracefully(): void {
extensions.killAll(() => { extensions.killAll(() => {
process.exit(1); process.exit(1);
}); });
@@ -65,14 +64,14 @@ if (!cluster.worker) {
let ssl; let ssl;
if (config.get("NBI_SSL")) { if (config.get("NBI_SSL")) {
ssl = { ssl = {
key: path.resolve(config.get("CONFIG_DIR"), "nbi.key"), key: path.resolve("" + config.get("CONFIG_DIR"), "nbi.key"),
cert: path.resolve(config.get("CONFIG_DIR"), "nbi.crt"), cert: path.resolve("" + config.get("CONFIG_DIR"), "nbi.crt"),
ca: path.resolve(config.get("CONFIG_DIR"), "nbi.ca-bundle") ca: path.resolve("" + config.get("CONFIG_DIR"), "nbi.ca-bundle")
}; };
} }
process.on("uncaughtException", err => { process.on("uncaughtException", err => {
if (err.code === "ERR_IPC_DISCONNECTED") return; if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({ logger.error({
message: "Uncaught exception", message: "Uncaught exception",
exception: err, exception: err,
+8 -9
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env -S node -r esm #!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
"use strict";
import * as path from "path"; import * as path from "path";
import * as fs from "fs"; import * as fs from "fs";
@@ -20,14 +19,14 @@ logger.init("ui", VERSION);
const SERVICE_ADDRESS = config.get("UI_INTERFACE"); const SERVICE_ADDRESS = config.get("UI_INTERFACE");
const SERVICE_PORT = config.get("UI_PORT"); const SERVICE_PORT = config.get("UI_PORT");
function exitWorkerGracefully() { function exitWorkerGracefully(): void {
db.disconnect(); db.disconnect();
db2.disconnect(); db2.disconnect();
cache.disconnect(); cache.disconnect();
cluster.worker.disconnect(); cluster.worker.disconnect();
} }
function exitWorkerUngracefully() { function exitWorkerUngracefully(): void {
process.exit(1); process.exit(1);
} }
@@ -63,16 +62,16 @@ if (!cluster.worker) {
let ssl; let ssl;
if (config.get("UI_SSL")) { if (config.get("UI_SSL")) {
ssl = { ssl = {
key: path.resolve(config.get("CONFIG_DIR"), "ui.key"), key: path.resolve("" + config.get("CONFIG_DIR"), "ui.key"),
cert: path.resolve(config.get("CONFIG_DIR"), "ui.crt"), cert: path.resolve("" + config.get("CONFIG_DIR"), "ui.crt"),
ca: path.resolve(config.get("CONFIG_DIR"), "ui.ca-bundle") ca: path.resolve("" + config.get("CONFIG_DIR"), "ui.ca-bundle")
}; };
} }
let stopping = false; let stopping = false;
process.on("uncaughtException", err => { process.on("uncaughtException", err => {
if (err.code === "ERR_IPC_DISCONNECTED") return; if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({ logger.error({
message: "Uncaught exception", message: "Uncaught exception",
exception: err, exception: err,
@@ -101,7 +100,7 @@ if (!cluster.worker) {
.catch(exitWorkerUngracefully); .catch(exitWorkerUngracefully);
}); });
const _listener = (req, res) => { const _listener = (req, res): void => {
if (stopping) res.setHeader("Connection", "close"); if (stopping) res.setHeader("Connection", "close");
listener(req, res); listener(req, res);
}; };
+47 -28
View File
@@ -1,17 +1,18 @@
const path = require("path"); import * as path from "path";
const fs = require("fs"); import * as fs from "fs";
const { promisify } = require("util"); import { promisify } from "util";
const { rollup } = require("rollup"); import { rollup, WarningHandler } from "rollup";
const rollupReplace = require("rollup-plugin-replace"); import rollupReplace from "rollup-plugin-replace";
const rollupJson = require("rollup-plugin-json"); import rollupJson from "rollup-plugin-json";
const { terser } = require("rollup-plugin-terser"); import typescript from "rollup-plugin-typescript";
const webpack = require("webpack"); import { terser } from "rollup-plugin-terser";
const postcss = require("postcss"); import webpack from "webpack";
const postcssImport = require("postcss-import"); import postcss from "postcss";
const postcssCssNext = require("postcss-cssnext"); import postcssImport from "postcss-import";
const cssnano = require("cssnano"); import postcssCssNext from "postcss-cssnext";
import cssnano from "cssnano";
const MODE = "production"; const MODE = process.env["NODE_ENV"] || "production";
const BUILD_METADATA = new Date() const BUILD_METADATA = new Date()
.toISOString() .toISOString()
@@ -49,10 +50,12 @@ const externals = [
"jsonwebtoken", "jsonwebtoken",
"stream", "stream",
"mithril", "mithril",
"parsimmon" "parsimmon",
"codemirror",
"codemirror/mode/javascript/javascript"
]; ];
function rmDirSync(dirPath) { function rmDirSync(dirPath): void {
if (!fs.existsSync(dirPath)) return; if (!fs.existsSync(dirPath)) return;
const files = fs.readdirSync(dirPath); const files = fs.readdirSync(dirPath);
@@ -64,7 +67,7 @@ function rmDirSync(dirPath) {
fs.rmdirSync(dirPath); fs.rmdirSync(dirPath);
} }
async function init() { async function init(): Promise<void> {
// Delete any old output directory // Delete any old output directory
rmDirSync(OUTPUT_DIR); rmDirSync(OUTPUT_DIR);
@@ -77,7 +80,9 @@ async function init() {
fs.mkdirSync(OUTPUT_DIR + "/tools"); fs.mkdirSync(OUTPUT_DIR + "/tools");
// Create package.json // Create package.json
const packageJson = require("../package.json"); const packageJson = JSON.parse(
fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString()
);
delete packageJson["devDependencies"]; delete packageJson["devDependencies"];
packageJson["scripts"] = { packageJson["scripts"] = {
install: packageJson["scripts"].install, install: packageJson["scripts"].install,
@@ -90,7 +95,7 @@ async function init() {
); );
} }
async function copyStatic() { async function copyStatic(): Promise<void> {
const files = [ const files = [
"LICENSE", "LICENSE",
"README.md", "README.md",
@@ -111,7 +116,7 @@ async function copyStatic() {
} }
} }
async function generateCss() { async function generateCss(): Promise<void> {
const cssInPath = path.resolve(INPUT_DIR, "ui/css/app.css"); const cssInPath = path.resolve(INPUT_DIR, "ui/css/app.css");
const cssOutPath = path.resolve(OUTPUT_DIR, "public/app.css"); const cssOutPath = path.resolve(OUTPUT_DIR, "public/app.css");
const cssIn = fs.readFileSync(cssInPath); const cssIn = fs.readFileSync(cssInPath);
@@ -123,7 +128,7 @@ async function generateCss() {
fs.writeFileSync(cssOutPath, cssOut.css); fs.writeFileSync(cssOutPath, cssOut.css);
} }
async function generateToolsJs() { async function generateToolsJs(): Promise<void> {
for (const bin of ["configure-ui", "dump-data-model"]) { for (const bin of ["configure-ui", "dump-data-model"]) {
const inputFile = path.resolve(INPUT_DIR, `tools/${bin}`); const inputFile = path.resolve(INPUT_DIR, `tools/${bin}`);
const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`); const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`);
@@ -136,7 +141,11 @@ async function generateToolsJs() {
plugins: [ plugins: [
rollupReplace({ rollupReplace({
delimiters: ["", ""], delimiters: ["", ""],
"#!/usr/bin/env -S node -r esm": "" "#!/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 MODE === "production" ? terser() : null
] ]
@@ -155,7 +164,7 @@ async function generateToolsJs() {
} }
} }
async function generateBackendJs() { async function generateBackendJs(): Promise<void> {
for (const bin of [ for (const bin of [
"genieacs-cwmp", "genieacs-cwmp",
"genieacs-ext", "genieacs-ext",
@@ -178,7 +187,7 @@ async function generateBackendJs() {
plugins: [ plugins: [
rollupReplace({ rollupReplace({
delimiters: ["", ""], delimiters: ["", ""],
"#!/usr/bin/env -S node -r esm": "" "#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": ""
}), }),
rollupJson({ preferConst: true }), rollupJson({ preferConst: true }),
{ {
@@ -191,6 +200,10 @@ async function generateBackendJs() {
return null; return null;
} }
}, },
typescript({
tsconfig: "./tsconfig.json",
include: [`bin/${bin}`, "lib/**/*.ts"]
}),
MODE === "production" ? terser() : null MODE === "production" ? terser() : null
] ]
}); });
@@ -208,23 +221,26 @@ async function generateBackendJs() {
} }
} }
async function generateFrontendJs() { async function generateFrontendJs(): Promise<void> {
const inputFile = path.resolve(INPUT_DIR, "ui/app.js"); const inputFile = path.resolve(INPUT_DIR, "ui/app.ts");
const outputFile = path.resolve(OUTPUT_DIR, "public/app.js"); const outputFile = path.resolve(OUTPUT_DIR, "public/app.js");
const bundle = await rollup({ const bundle = await rollup({
input: inputFile, input: inputFile,
external: externals, external: externals,
plugins: [rollupJson({ preferConst: true })], plugins: [
rollupJson({ preferConst: true }),
typescript({ tsconfig: "./tsconfig.json" })
],
inlineDynamicImports: true, inlineDynamicImports: true,
treeshake: { treeshake: {
propertyReadSideEffects: false, propertyReadSideEffects: false,
pureExternalModules: true pureExternalModules: true
}, },
onwarn: (warning, warn) => { onwarn: ((warning, warn) => {
// Ignore circular dependency warnings // Ignore circular dependency warnings
if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning); if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning);
} }) as WarningHandler
}); });
await bundle.write({ await bundle.write({
@@ -236,6 +252,9 @@ async function generateFrontendJs() {
const webpackConf = { const webpackConf = {
mode: MODE, mode: MODE,
entry: outputFile, entry: outputFile,
resolve: {
aliasFields: ["module"]
},
output: { output: {
path: path.resolve(OUTPUT_DIR, "public"), path: path.resolve(OUTPUT_DIR, "public"),
filename: "app.js" filename: "app.js"
+20 -19
View File
@@ -16,19 +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";
import crypto from "crypto"; import * as crypto from "crypto";
import dgram from "dgram"; import * as dgram from "dgram";
import URL from "url"; import * as URL from "url";
import http from "http"; import * as http from "http";
import * as config from "./config"; import * as config from "./config";
import * as db from "./db"; import * as db from "./db";
import * as common from "./common"; import * as common from "./common";
import * as auth from "./auth"; import * as auth from "./auth";
import * as cache from "./cache"; import * as cache from "./cache";
function udpConReq(address, un, key, callback) { function udpConReq(address, un, key, callback): boolean {
if (!address) return false; if (!address) return false;
let [host, port] = address.split(":", 2); let [host, port] = address.split(":", 2);
@@ -53,7 +52,9 @@ function udpConReq(address, un, key, callback) {
); );
const client = dgram.createSocket({ type: "udp4", reuseAddr: true }); const client = dgram.createSocket({ type: "udp4", reuseAddr: true });
const UDP_CONNECTION_REQUEST_PORT = config.get("UDP_CONNECTION_REQUEST_PORT"); const UDP_CONNECTION_REQUEST_PORT = +config.get(
"UDP_CONNECTION_REQUEST_PORT"
);
if (UDP_CONNECTION_REQUEST_PORT) if (UDP_CONNECTION_REQUEST_PORT)
// When a device is NAT'ed, the UDP Connection Request must originate from // When a device is NAT'ed, the UDP Connection Request must originate from
@@ -65,7 +66,7 @@ function udpConReq(address, un, key, callback) {
client.bind({ port: UDP_CONNECTION_REQUEST_PORT, exclusive: true }); client.bind({ port: UDP_CONNECTION_REQUEST_PORT, exclusive: true });
let count = 3; let count = 3;
const func = err => { const func = (err: Error): void => {
if (err || --count <= 0) { if (err || --count <= 0) {
client.close(); client.close();
return void callback(err); return void callback(err);
@@ -83,8 +84,8 @@ function httpConReq(
allowBasicAuth, allowBasicAuth,
timeout, timeout,
callback callback
) { ): void {
const options = URL.parse(url); const options: http.RequestOptions = URL.parse(url);
if (options.protocol !== "http:") { if (options.protocol !== "http:") {
return void callback( return void callback(
new Error("Invalid connection request URL or protocol") new Error("Invalid connection request URL or protocol")
@@ -92,7 +93,7 @@ function httpConReq(
} }
options.agent = new http.Agent({ maxSockets: 1 }); options.agent = new http.Agent({ maxSockets: 1 });
function statusToError(statusCode) { function statusToError(statusCode): Error {
switch (statusCode) { switch (statusCode) {
case 200: case 200:
case 204: case 204:
@@ -112,7 +113,7 @@ function httpConReq(
const authHeader = auth.parseAuthHeader( const authHeader = auth.parseAuthHeader(
response.headers["www-authenticate"] response.headers["www-authenticate"]
); );
if (authHeader.method === "Basic") { if (authHeader["method"] === "Basic") {
if (!allowBasicAuth) { if (!allowBasicAuth) {
request.abort(); request.abort();
return void callback( return void callback(
@@ -122,7 +123,7 @@ function httpConReq(
options.headers = { options.headers = {
Authorization: auth.basic(username || "", password || "") Authorization: auth.basic(username || "", password || "")
}; };
} else if (authHeader.method === "Digest") { } else if (authHeader["method"] === "Digest") {
options.headers = { options.headers = {
Authorization: auth.digest( Authorization: auth.digest(
username || "", username || "",
@@ -187,7 +188,7 @@ function httpConReq(
}); });
} }
export function connectionRequest(deviceId, callback) { export function connectionRequest(deviceId, callback): void {
const CONNECTION_REQUEST_TIMEOUT = config.get( const CONNECTION_REQUEST_TIMEOUT = config.get(
"CONNECTION_REQUEST_TIMEOUT", "CONNECTION_REQUEST_TIMEOUT",
deviceId deviceId
@@ -235,7 +236,7 @@ export function connectionRequest(deviceId, callback) {
if (managementServer.ConnectionRequestPassword) if (managementServer.ConnectionRequestPassword)
password = managementServer.ConnectionRequestPassword._value; password = managementServer.ConnectionRequestPassword._value;
const conReq = () => { const conReq = (): void => {
const udpSent = udpConReq( const udpSent = udpConReq(
udpConnectionRequestAddress, udpConnectionRequestAddress,
username, username,
@@ -284,7 +285,7 @@ export function connectionRequest(deviceId, callback) {
}); });
} }
export function watchTask(deviceId, taskId, timeout, callback) { export function watchTask(deviceId, taskId, timeout, callback): void {
setTimeout(() => { setTimeout(() => {
db.tasksCollection.findOne( db.tasksCollection.findOne(
{ _id: taskId }, { _id: taskId },
@@ -313,7 +314,7 @@ export function watchTask(deviceId, taskId, timeout, callback) {
}, 500); }, 500);
} }
function sanitizeTask(task, callback) { function sanitizeTask(task, callback): void {
task.timestamp = new Date(task.timestamp || Date.now()); task.timestamp = new Date(task.timestamp || Date.now());
if (task.expiry) { if (task.expiry) {
if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry)) if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry))
@@ -324,7 +325,7 @@ function sanitizeTask(task, callback) {
callback(task); callback(task);
} }
export function insertTasks(tasks, callback) { export function insertTasks(tasks, callback): void {
if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE) tasks = [tasks]; if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE) tasks = [tasks];
else if (!tasks || tasks.length === 0) else if (!tasks || tasks.length === 0)
return void callback(null, tasks || []); return void callback(null, tasks || []);
@@ -344,7 +345,7 @@ export function insertTasks(tasks, callback) {
} }
} }
export function deleteDevice(deviceId, callback) { export function deleteDevice(deviceId, callback): void {
db.tasksCollection.deleteMany({ device: deviceId }, err => { db.tasksCollection.deleteMany({ device: deviceId }, err => {
if (err) return void callback(err); if (err) return void callback(err);
db.devicesCollection.deleteOne({ _id: deviceId }, err => { db.devicesCollection.deleteOne({ _id: deviceId }, err => {
+16 -10
View File
@@ -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";
import { createHash } from "crypto"; import { createHash } from "crypto";
export 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 @@ export function parseAuthHeader(authHeader) {
return res; return res;
} }
export 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");
} }
export 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 @@ export function digest(username, password, uri, httpMethod, body, authHeader) {
else qop = authHeader.qop; else qop = authHeader.qop;
} }
let ha1 = createHash("md5"); const ha1 = createHash("md5");
ha1 ha1
.update(username) .update(username)
.update(":") .update(":")
@@ -76,19 +82,19 @@ export 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 = 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 = 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 @@ export 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}"`;
+31 -14
View File
@@ -16,19 +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";
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
import * as config from "./config"; import * as config from "./config";
const MAX_CACHE_TTL = config.get("MAX_CACHE_TTL"); const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
let mongoClient; let mongoClient;
let mongoCollection; let mongoCollection;
let mongoTimeOffset = 0; let mongoTimeOffset = 0;
export function connect(callback) { export function connect(callback): void {
const MONGODB_CONNECTION_URL = config.get("MONGODB_CONNECTION_URL"); const MONGODB_CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
MongoClient.connect( MongoClient.connect(
MONGODB_CONNECTION_URL, MONGODB_CONNECTION_URL,
@@ -51,11 +50,11 @@ export function connect(callback) {
); );
} }
export function disconnect() { export function disconnect(): void {
if (mongoClient) mongoClient.close(); if (mongoClient) mongoClient.close();
} }
export function get(key, callback) { export function get(key, callback): void {
const expire = new Date(Date.now() - mongoTimeOffset); const expire = new Date(Date.now() - mongoTimeOffset);
if (Array.isArray(key)) { if (Array.isArray(key)) {
mongoCollection.find({ _id: { $in: key } }).toArray((err, res) => { mongoCollection.find({ _id: { $in: key } }).toArray((err, res) => {
@@ -83,21 +82,39 @@ export function get(key, callback) {
} }
} }
export function del(key, callback) { export function del(key, callback): void {
if (Array.isArray(key)) if (Array.isArray(key))
mongoCollection.deleteMany({ _id: { $in: key } }, callback); mongoCollection.deleteMany({ _id: { $in: key } }, callback);
else mongoCollection.deleteOne({ _id: key }, callback); else mongoCollection.deleteOne({ _id: key }, callback);
} }
export function set(key, value, ttl, callback) { export function set(
if (!callback && typeof v === "function") { 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; callback = ttl;
ttl = null; ttl = null;
} }
if (!ttl) ttl = MAX_CACHE_TTL; if (!ttl) ttl = MAX_CACHE_TTL;
const expire = new Date(Date.now() - mongoTimeOffset + ttl * 1000); const expire = new Date(
Date.now() - mongoTimeOffset + (ttl as number) * 1000
);
mongoCollection.replaceOne( mongoCollection.replaceOne(
{ _id: key }, { _id: key },
{ _id: key, value: value, expire: expire }, { _id: key, value: value, expire: expire },
@@ -106,7 +123,7 @@ export function set(key, value, ttl, callback) {
); );
} }
export function pop(key, callback) { export function pop(key, callback): void {
mongoCollection.findOneAndDelete({ _id: key }, (err, res) => { mongoCollection.findOneAndDelete({ _id: key }, (err, res) => {
if (err || !res["value"]) return void callback(err); if (err || !res["value"]) return void callback(err);
@@ -117,12 +134,12 @@ export function pop(key, callback) {
}); });
} }
export function lock(lockName, ttl, callback) { export function lock(lockName, ttl, callback): void {
const token = Math.random() const token = Math.random()
.toString(36) .toString(36)
.slice(2); .slice(2);
function unlockOrExtend(extendTtl) { function unlockOrExtend(extendTtl): void {
if (!extendTtl) { if (!extendTtl) {
mongoCollection.deleteOne({ _id: lockName, value: token }, (err, res) => { mongoCollection.deleteOne({ _id: lockName, value: token }, (err, res) => {
if (err || res["result"]["n"] !== 1) if (err || res["result"]["n"] !== 1)
@@ -135,7 +152,7 @@ export function lock(lockName, ttl, callback) {
{ expire: expire }, { expire: expire },
(err, res) => { (err, res) => {
if (err || res["result"]["n"] !== 1) if (err || res["result"]["n"] !== 1)
throw err | new Error("Lock expired"); throw err || new Error("Lock expired");
} }
); );
} }
+6 -5
View File
@@ -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";
import cluster from "cluster"; import cluster from "cluster";
import { cpus } from "os"; import { cpus } from "os";
@@ -25,10 +24,12 @@ import * as logger from "./logger";
let respawnTimestamp = 0; let respawnTimestamp = 0;
let tooManyCrashesTimestamp = 0; let tooManyCrashesTimestamp = 0;
function restartWorker(worker, code, signal) { function restartWorker(worker, code, signal): void {
const msg = { const msg = {
message: "Worker died", message: "Worker died",
pid: worker.process.pid pid: worker.process.pid,
exitCode: null,
signal: null
}; };
if (code != null) msg.exitCode = code; if (code != null) msg.exitCode = code;
@@ -63,7 +64,7 @@ function restartWorker(worker, code, signal) {
}, respawnTimestamp - now); }, respawnTimestamp - now);
} }
export function start(workerCount, servicePort, serviceAddress) { export function start(workerCount, servicePort, serviceAddress): void {
cluster.on("listening", (worker, address) => { cluster.on("listening", (worker, address) => {
if ( if (
(address.addressType === 4 || address.addressType === 6) && (address.addressType === 4 || address.addressType === 6) &&
@@ -86,7 +87,7 @@ export function start(workerCount, servicePort, serviceAddress) {
for (let i = 0; i < workerCount; ++i) cluster.fork(); for (let i = 0; i < workerCount; ++i) cluster.fork();
} }
export function stop() { export function stop(): void {
cluster.removeListener("exit", restartWorker); cluster.removeListener("exit", restartWorker);
for (const pid in cluster.workers) cluster.workers[pid].kill(); for (const pid in cluster.workers) cluster.workers[pid].kill();
} }
+15 -10
View File
@@ -16,7 +16,8 @@
* 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 { ParamPath } from "./types";
export const UNDEFINED_TYPE = "[object Undefined]"; export const UNDEFINED_TYPE = "[object Undefined]";
export const NULL_TYPE = "[object Null]"; export const NULL_TYPE = "[object Null]";
@@ -28,11 +29,11 @@ export const OBJECT_TYPE = "[object Object]";
export const REGEXP_TYPE = "[object RegExp]"; export const REGEXP_TYPE = "[object RegExp]";
export const DATE_TYPE = "[object Date]"; export const DATE_TYPE = "[object Date]";
export const typeOf = obj => Object.prototype.toString.call(obj); export const typeOf = (obj): string => Object.prototype.toString.call(obj);
export function generateDeviceId(deviceIdStruct) { export function generateDeviceId(deviceIdStruct): string {
// Escapes everything except alphanumerics and underscore // Escapes everything except alphanumerics and underscore
function esc(str) { function esc(str): string {
return str.replace(/[^A-Za-z0-9_]/g, chr => { return str.replace(/[^A-Za-z0-9_]/g, chr => {
const buf = Buffer.from(chr, "utf8"); const buf = Buffer.from(chr, "utf8");
let rep = ""; let rep = "";
@@ -54,11 +55,12 @@ export function generateDeviceId(deviceIdStruct) {
return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]); return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]);
} }
export function parseAlias(pattern, start, res) { export function parseAlias(pattern, start, res): number {
const aliases = []; const aliases = [];
let i = start; let i = start;
while (i < pattern.length && pattern[i] !== "]") { while (i < pattern.length && pattern[i] !== "]") {
const alias = []; const alias = [];
/* eslint-disable-next-line @typescript-eslint/no-use-before-define */
let j = (i = parsePath(pattern, i, alias) + 1); let j = (i = parsePath(pattern, i, alias) + 1);
while (pattern[j] !== "]" && pattern[j] !== ",") { while (pattern[j] !== "]" && pattern[j] !== ",") {
if (pattern[j] === '"' && i === j) { if (pattern[j] === '"' && i === j) {
@@ -87,7 +89,7 @@ export function parseAlias(pattern, start, res) {
} }
// Sort to ensure identical expressions have idential string representation // Sort to ensure identical expressions have idential string representation
function srt(a, b) { function srt(a, b): number {
const jMax = Math.min(a.length, b.length); const jMax = Math.min(a.length, b.length);
for (let j = 0; j < jMax; j += 2) { for (let j = 0; j < jMax; j += 2) {
const kMax = Math.min(a[j].length, b[j].length); const kMax = Math.min(a[j].length, b[j].length);
@@ -126,8 +128,11 @@ export function parseAlias(pattern, start, res) {
return i; return i;
} }
export function parsePath(pattern, start, res) { /* eslint-disable @typescript-eslint/no-use-before-define */
const path = []; export function parsePath(pattern, start, res): number;
export function parsePath(pattern, start?): ParamPath;
export function parsePath(pattern, start?, res?): number | ParamPath {
const path = [] as ParamPath;
path.wildcard = 0; path.wildcard = 0;
path.alias = 0; path.alias = 0;
let i = start || 0; let i = start || 0;
@@ -163,7 +168,7 @@ export function parsePath(pattern, start, res) {
return i; return i;
} }
export function addPathMeta(path) { export function addPathMeta(path): ParamPath {
if (path.alias != null && path.wildcard != null) return path; if (path.alias != null && path.wildcard != null) return path;
path.alias = 0; path.alias = 0;
@@ -183,6 +188,6 @@ export function addPathMeta(path) {
} }
// Source: http://stackoverflow.com/a/6969486 // Source: http://stackoverflow.com/a/6969486
export function escapeRegExp(str) { export function escapeRegExp(str): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
} }
@@ -1,20 +1,27 @@
"use strict";
import pass from "./validators/pass"; import pass from "./validators/pass";
import test from "./validators/test"; import test from "./validators/test";
import { PermissionSet } from "../types";
const validators = { pass, test }; const validators = { pass, test };
export default class Authorizer { export default class Authorizer {
constructor(permissionSets) { private permissionSets: PermissionSet[];
private validatorCache: WeakMap<
object,
(mutationType, mutation, any) => boolean
>;
private hasAccessCache: Map<string, boolean>;
public constructor(permissionSets) {
this.permissionSets = permissionSets; this.permissionSets = permissionSets;
this.validatorCache = new WeakMap(); this.validatorCache = new WeakMap();
this.hasAccessCache = {}; this.hasAccessCache = new Map();
} }
hasAccess(resourceType, access) { public hasAccess(resourceType, access): boolean {
const cacheKey = `${resourceType}-${access}`; const cacheKey = `${resourceType}-${access}`;
if (cacheKey in this.hasAccessCache) return this.hasAccessCache[cacheKey]; if (this.hasAccessCache.has(cacheKey))
return this.hasAccessCache.get(cacheKey);
let has = false; let has = false;
for (const permissionSet of this.permissionSets) { for (const permissionSet of this.permissionSets) {
@@ -28,11 +35,14 @@ export default class Authorizer {
} }
} }
this.hasAccessCache[cacheKey] = has; this.hasAccessCache.set(cacheKey, has);
return has; return has;
} }
getValidator(resourceType, resource) { public getValidator(
resourceType,
resource
): (mutationType: string, mutation: any, args: any) => boolean {
if (this.validatorCache.has(resource)) if (this.validatorCache.has(resource))
return this.validatorCache.get(resource); return this.validatorCache.get(resource);
@@ -53,7 +63,11 @@ export default class Authorizer {
} }
} }
const validator = (mutationType, mutation, any) => { const validator = (
mutationType: string,
mutation: any,
any: any
): boolean => {
let valid = false; let valid = false;
for (const [k, v] of Object.entries(funcs)) { for (const [k, v] of Object.entries(funcs)) {
if (v) { if (v) {
@@ -1,8 +1,9 @@
"use strict";
import { map, parseLikePattern } from "./expression-parser"; import { map, parseLikePattern } from "./expression-parser";
import { Expression } from "../types";
function or(cnf1, cnf2) { type CNF = number[][];
function or(cnf1: CNF, cnf2: CNF): CNF {
if (cnf1.length === 0) return cnf2; if (cnf1.length === 0) return cnf2;
else if (cnf2.length === 0) return cnf1; else if (cnf2.length === 0) return cnf1;
@@ -19,7 +20,30 @@ function or(cnf1, cnf2) {
return res; return res;
} }
export function booleanCnf(exp) { 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 ranges = new Map();
const likePatterns = new Map(); const likePatterns = new Map();
map(exp, e => { map(exp, e => {
@@ -51,15 +75,20 @@ export function booleanCnf(exp) {
}); });
for (const [k, v] of ranges) { for (const [k, v] of ranges) {
if (k.endsWith(":number")) if (k.endsWith(":number")) {
ranges.set(k, Array.from(v).sort((a, b) => a - b)); ranges.set(
else ranges.set(k, Array.from(v).sort()); k,
Array.from(v as number[]).sort((a: number, b: number) => a - b)
);
} else {
ranges.set(k, Array.from(v).sort());
}
} }
const expressions = new Map(); const expressions: Map<string, number> = new Map();
const mutuallyExclusive = new Map(); const mutuallyExclusive: Map<string, Set<number>> = new Map();
function getVariable(lhs, op, rhs = "") { function getVariable(lhs, op, rhs = ""): number {
const key = `${lhs}:${op}:${rhs}`; const key = `${lhs}:${op}:${rhs}`;
let f = expressions.get(key); let f = expressions.get(key);
if (!f) { if (!f) {
@@ -72,7 +101,13 @@ export function booleanCnf(exp) {
return f; return f;
} }
function getComparisonVariables(lhs, from, fromInclusive, to, toInclusive) { function getComparisonVariables(
lhs,
from,
fromInclusive,
to,
toInclusive
): number[] {
const type = from != null ? typeof from : typeof to; const type = from != null ? typeof from : typeof to;
const range = ranges.get(`${lhs}:${type}`); const range = ranges.get(`${lhs}:${type}`);
const vars = []; const vars = [];
@@ -109,7 +144,7 @@ export function booleanCnf(exp) {
}); });
} }
function getLikeVariables(lhs, pat, esc) { function getLikeVariables(lhs, pat, esc): number[] {
const vars = []; const vars = [];
const pats = likePatterns.get(lhs); const pats = likePatterns.get(lhs);
pat = pats.get(`${esc || ""}:${pat}`); pat = pats.get(`${esc || ""}:${pat}`);
@@ -125,7 +160,11 @@ export function booleanCnf(exp) {
}); });
} }
const comparisonOperators = { interface ComparisonOperators {
[name: string]: (v: string) => [string, boolean, string, boolean];
}
const comparisonOperators: ComparisonOperators = {
"=": v => [v, true, v, true], "=": v => [v, true, v, true],
">": v => [v, false, null, null], ">": v => [v, false, null, null],
">=": v => [v, true, null, null], ">=": v => [v, true, null, null],
@@ -133,7 +172,7 @@ export function booleanCnf(exp) {
"<=": v => [null, null, v, true] "<=": v => [null, null, v, true]
}; };
function recursive(clause, negate) { function recursive(clause, negate): CNF {
let op = clause[0]; let op = clause[0];
if ((op === "AND" && !negate) || (op === "OR" && negate)) { if ((op === "AND" && !negate) || (op === "OR" && negate)) {
@@ -218,33 +257,10 @@ export function booleanCnf(exp) {
const cnf = recursive(exp, false); const cnf = recursive(exp, false);
for (const m of mutuallyExclusive.values()) { for (const m of mutuallyExclusive.values()) {
const ar = Array.from(m); const ar: number[] = Array.from(m);
for (let i = 0; i < ar.length; ++i) 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]]); for (let j = i + 1; j < ar.length; ++j) cnf.push([0 - ar[i], 0 - ar[j]]);
} }
return { vars: expressions.size, clauses: cnf }; return { vars: expressions.size, clauses: cnf };
} }
function likePatternIncludes(pat1, pat2, idx1 = 0, idx2 = 0) {
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;
}
@@ -1,9 +1,8 @@
"use strict"; import * as parsimmon from "parsimmon";
import { Expression } from "../types";
import parsimmon from "parsimmon";
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n"). // Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
function interpretEscapes(str) { function interpretEscapes(str): string {
const escapes = { const escapes = {
b: "\b", b: "\b",
f: "\f", f: "\f",
@@ -22,7 +21,7 @@ function interpretEscapes(str) {
}); });
} }
export function map(exp, callback) { export function map(exp, callback): Expression {
if (!Array.isArray(exp)) return callback(exp); if (!Array.isArray(exp)) return callback(exp);
let clone; let clone;
@@ -37,7 +36,7 @@ export function map(exp, callback) {
return callback(clone || exp); return callback(clone || exp);
} }
function binaryLeft(operatorsParser, nextParser) { function binaryLeft(operatorsParser, nextParser): parsimmon.Parser<{}> {
return parsimmon.seqMap( return parsimmon.seqMap(
nextParser, nextParser,
parsimmon.seq(operatorsParser, nextParser).many(), parsimmon.seq(operatorsParser, nextParser).many(),
@@ -257,7 +256,7 @@ const lang = parsimmon.createLanguage({
); );
}, },
Expression: function(r) { Expression: function(r) {
function unary(operatorsParser, nextParser) { function unary(operatorsParser, nextParser): parsimmon.Parser<{}> {
return parsimmon.seq(operatorsParser, nextParser).or(nextParser); return parsimmon.seq(operatorsParser, nextParser).or(nextParser);
} }
@@ -271,12 +270,12 @@ const lang = parsimmon.createLanguage({
} }
}); });
export function parse(str) { export function parse(str): Expression {
if (!str) return null; if (!str) return null;
return lang.Expression.tryParse(str); return lang.Expression.tryParse(str);
} }
export function stringify(exp, level = 0) { export function stringify(exp, level = 0): string {
if (!Array.isArray(exp)) return JSON.stringify(exp); if (!Array.isArray(exp)) return JSON.stringify(exp);
const opLevels = { const opLevels = {
@@ -302,7 +301,7 @@ export function stringify(exp, level = 0) {
const op = exp[0].toUpperCase(); const op = exp[0].toUpperCase();
function wrap(e) { function wrap(e): string {
if (opLevels[op] <= level) return `(${e})`; if (opLevels[op] <= level) return `(${e})`;
else return e; else return e;
} }
@@ -360,7 +359,7 @@ export function stringify(exp, level = 0) {
} }
} }
export function parseLikePattern(pat, esc) { export function parseLikePattern(pat, esc): string[] {
const chars = pat.split(""); const chars = pat.split("");
for (let i = 0; i < chars.length; ++i) { for (let i = 0; i < chars.length; ++i) {
@@ -1,15 +1,14 @@
"use strict";
import { map, parseLikePattern } from "./expression-parser"; import { map, parseLikePattern } from "./expression-parser";
import { booleanCnf } from "./expression-cnf"; import { booleanCnf } from "./expression-cnf";
import { naiveDpll } from "./sat-solver"; import { naiveDpll } from "./sat-solver";
import { Expression } from "../types";
const isArray = Array.isArray; const isArray = Array.isArray;
const regExpCache = new WeakMap(); const regExpCache = new WeakMap();
const REDUCE_SKIP = {}; const REDUCE_SKIP = {};
function reduce(exp, callback) { function reduce(exp, callback): Expression {
let loop = true; let loop = true;
while (loop) { while (loop) {
loop = false; loop = false;
@@ -26,8 +25,45 @@ function reduce(exp, callback) {
return exp; return exp;
} }
export function evaluate(exp, obj, now, cb) { export function likePatternToRegExp(pat, esc = "", flags = ""): RegExp {
function getRegExp(pat, esc) { 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}`; const k = `${esc || ""}:${pat}`;
let c = regExpCache.get(exp); let c = regExpCache.get(exp);
if (!c) regExpCache.set(exp, (c = {})); if (!c) regExpCache.set(exp, (c = {}));
@@ -162,11 +198,11 @@ export function evaluate(exp, obj, now, cb) {
}); });
} }
export function and(exp1, exp2) { export function and(exp1, exp2): Expression {
if (!isArray(exp1)) return exp1 ? exp2 : exp1; if (!isArray(exp1)) return exp1 ? exp2 : exp1;
if (!isArray(exp2)) return exp2 ? exp1 : exp2; if (!isArray(exp2)) return exp2 ? exp1 : exp2;
let res = ["AND"]; let res: Expression = ["AND"];
if (exp1[0] === "AND") res = res.concat(exp1.slice(1)); if (exp1[0] === "AND") res = res.concat(exp1.slice(1));
else res.push(exp1); else res.push(exp1);
@@ -177,11 +213,11 @@ export function and(exp1, exp2) {
return res; return res;
} }
export function or(exp1, exp2) { export function or(exp1, exp2): Expression {
if (!isArray(exp1)) return exp1 ? exp1 : exp2; if (!isArray(exp1)) return exp1 ? exp1 : exp2;
if (!isArray(exp2)) return exp2 ? exp2 : exp1; if (!isArray(exp2)) return exp2 ? exp2 : exp1;
let res = ["OR"]; let res: Expression = ["OR"];
if (exp1[0] === "OR") res = res.concat(exp1.slice(1)); if (exp1[0] === "OR") res = res.concat(exp1.slice(1));
else res.push(exp1); else res.push(exp1);
@@ -192,49 +228,19 @@ export function or(exp1, exp2) {
return res; return res;
} }
export function not(exp) { export function not(exp): Expression {
if (isArray(exp) && exp[0] === "NOT") return exp[1]; if (isArray(exp) && exp[0] === "NOT") return exp[1];
return ["NOT", exp]; return ["NOT", exp];
} }
export function subset(exp1, exp2) { export function subset(exp1, exp2): boolean {
const e = evaluate(["NOT", ["OR", ["NOT", exp1], exp2]]); const e = evaluate(["NOT", ["OR", ["NOT", exp1], exp2]]);
if (!isArray(e)) return !e; if (!isArray(e)) return !e;
const { vars, clauses } = booleanCnf(e); const { vars, clauses } = booleanCnf(e);
return !naiveDpll(clauses, vars); return !naiveDpll(clauses, vars);
} }
export function likePatternToRegExp(pat, esc = "", flags = "") { export function extractParams(exp): string[] {
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 extractParams(exp) {
const params = new Set(); const params = new Set();
map(exp, e => { map(exp, e => {
if (isArray(e) && e[0] === "PARAM") params.add(e[1]); if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
@@ -1,10 +1,8 @@
"use strict";
let cache1 = new Map(); let cache1 = new Map();
let cache2 = new Map(); let cache2 = new Map();
const keys = new WeakMap(); const keys = new WeakMap();
function getKey(obj) { function getKey(obj): string {
if (obj === null) return "null"; if (obj === null) return "null";
else if (obj === undefined) return "undefined"; else if (obj === undefined) return "undefined";
@@ -22,7 +20,7 @@ function getKey(obj) {
return k; return k;
} }
export default function memoize(func) { export default function memoize(func): Function {
return (...args) => { return (...args) => {
const key = JSON.stringify(args.map(getKey)) + getKey(func); const key = JSON.stringify(args.map(getKey)) + getKey(func);
@@ -14,9 +14,10 @@
* The code is very similar to the pure table solver, except we use a * The code is very similar to the pure table solver, except we use a
* unit-propagating, derived-varlist-returing satisfiableAt. * unit-propagating, derived-varlist-returing satisfiableAt.
*/ */
"use strict";
function satisfiableAt(clauses, varVals, varnr, val) { /* eslint-disable @typescript-eslint/no-use-before-define */
function satisfiableAt(clauses, varVals, varnr, val): boolean {
if (varnr !== 0) varVals[varnr] = val; if (varnr !== 0) varVals[varnr] = val;
const propRes = unitPropagate(clauses, varVals); const propRes = unitPropagate(clauses, varVals);
@@ -87,7 +88,7 @@ function satisfiableAt(clauses, varVals, varnr, val) {
* undetermined. The values of variables on the list will be restored (set to * undetermined. The values of variables on the list will be restored (set to
* 0) during backtracking. * 0) during backtracking.
*/ */
function unitPropagate(clauses, varVals) { function unitPropagate(clauses, varVals): number[] | 1 | -1 {
// all vars with a value derived in the forthcoming main loop // all vars with a value derived in the forthcoming main loop
const derivedVars = []; const derivedVars = [];
@@ -167,7 +168,7 @@ function unitPropagate(clauses, varVals) {
return derivedVars; return derivedVars;
} }
export function naiveDpll(clauses, maxvarnr) { export function naiveDpll(clauses, maxvarnr): boolean {
// variable values are 0 if not set, 1 if positive, -1 if negative // variable values are 0 if not set, 1 if positive, -1 if negative
const varVals = new Int32Array(maxvarnr + 1); const varVals = new Int32Array(maxvarnr + 1);
return satisfiableAt(clauses, varVals, 0, 0); return satisfiableAt(clauses, varVals, 0, 0);
-3
View File
@@ -1,3 +0,0 @@
export default function pass() {
return 1;
}
+3
View File
@@ -0,0 +1,3 @@
export default function pass(): number {
return 1;
}
@@ -1,4 +1,4 @@
export default function test(resourceType, resource) { export default function test(resourceType, resource): number {
if (resourceType === "devices") { if (resourceType === "devices") {
if (resource["Tags.test"] && resource["Tags.test"].value[0]) return 1; if (resource["Tags.test"] && resource["Tags.test"].value[0]) return 1;
} else if (resourceType === "presets") { } else if (resourceType === "presets") {
+12 -8
View File
@@ -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";
import { resolve } from "path"; import { resolve } from "path";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
@@ -90,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
@@ -119,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);
@@ -182,7 +181,9 @@ setConfig("CONFIG_DIR", options["CONFIG_DIR"]["default"]);
// Configuration file // Configuration file
const configFile = JSON.parse( const configFile = JSON.parse(
readFileSync(resolve(allConfig.CONFIG_DIR, "config.json")) 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))
@@ -194,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"]);
export 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}`;
@@ -222,7 +226,7 @@ export function get(optionName, deviceId) {
return null; return null;
} }
export 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;
@@ -235,7 +239,7 @@ export function getDefault(optionName) {
export let auth; export let auth;
// Load authentication scripts // Load authentication scripts
try { try {
auth = require(resolve(allConfig.CONFIG_DIR, "auth.js")); auth = require(resolve(allConfig.CONFIG_DIR as string, "auth.js"));
} catch (error) { } catch (error) {
// No auth.js exists // No auth.js exists
} }
+94 -40
View File
@@ -16,11 +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";
import zlib from "zlib"; import * as zlib from "zlib";
import crypto from "crypto"; import * as crypto from "crypto";
import fs from "fs"; import * as fs from "fs";
import { Socket } from "net";
import * as config from "./config"; import * as config from "./config";
import * as common from "./common"; import * as common from "./common";
import * as soap from "./soap"; import * as soap from "./soap";
@@ -32,9 +32,19 @@ import * as localCache from "./local-cache";
import * as db from "./db"; import * as db from "./db";
import * as logger from "./logger"; import * as logger from "./logger";
import * as scheduling from "./scheduling"; import * as scheduling from "./scheduling";
import {
SessionContext,
ParamPath,
AcsRequest,
SetAcsRequest,
SessionFault,
Operation
} 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 +53,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,7 +63,11 @@ 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";
@@ -69,7 +83,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 +144,9 @@ function writeResponse(sessionContext, res, close) {
} }
} }
function recordFault(sessionContext, fault, provisions, channels) { function recordFault(sessionContext, fault, provisions, channels): void;
function recordFault(sessionContext, fault): void;
function recordFault(sessionContext, fault, provisions?, channels?): void {
if (!provisions) { if (!provisions) {
provisions = sessionContext.provisions; provisions = sessionContext.provisions;
channels = sessionContext.channels; channels = sessionContext.channels;
@@ -185,7 +201,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);
@@ -213,7 +229,7 @@ function inform(sessionContext, rpc) {
}); });
} }
function transferComplete(sessionContext, rpc) { function transferComplete(sessionContext, rpc): void {
session.transferComplete( session.transferComplete(
sessionContext, sessionContext,
rpc.cpeRequest, rpc.cpeRequest,
@@ -250,7 +266,7 @@ function transferComplete(sessionContext, rpc) {
} }
// Append providions and remove duplicates // Append providions 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();
@@ -281,7 +297,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);
@@ -289,7 +305,7 @@ function applyPresets(sessionContext) {
const blackList = {}; const blackList = {};
let whiteList = null; let whiteList = null;
let whiteListProvisions = null; let whiteListProvisions = null;
const RETRY_DELAY = localCache.getConfig( const RETRY_DELAY = +localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.retryDelay", "cwmp.retryDelay",
sessionContext.configContext sessionContext.configContext
@@ -318,13 +334,17 @@ 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(
["Events", "*"] as ParamPath,
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[1]] = true;
} }
const parameters = {}; const parameters: { [name: string]: ParamPath } = {};
const filteredPresets = []; const filteredPresets = [];
for (const preset of presets) { for (const preset of presets) {
@@ -478,7 +498,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);
@@ -604,7 +624,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;
@@ -714,24 +734,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;
} }
} }
@@ -751,7 +776,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);
@@ -768,10 +797,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
export 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;
@@ -840,7 +867,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);
@@ -850,7 +899,8 @@ function getDueTasksAndFaultsAndOperations(deviceId, timestamp, callback) {
null, null,
res.tasks || [], res.tasks || [],
res.faults || {}, res.faults || {},
res.operations || {} res.operations || {},
0
); );
} }
@@ -899,8 +949,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;
@@ -917,7 +971,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({
@@ -996,7 +1050,7 @@ function processRequest(sessionContext, rpc) {
} }
} }
export function listener(httpRequest, httpResponse) { export function listener(httpRequest, httpResponse): void {
stats.totalRequests += 1; stats.totalRequests += 1;
if (httpRequest.method !== "POST") { if (httpRequest.method !== "POST") {
@@ -1064,7 +1118,7 @@ export 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;
@@ -1143,7 +1197,7 @@ export 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",
@@ -1201,7 +1255,7 @@ export function listener(httpRequest, httpResponse) {
_sessionContext.cacheSnapshot = cacheSnapshot; _sessionContext.cacheSnapshot = cacheSnapshot;
_sessionContext.configContext = configContext; _sessionContext.configContext = configContext;
_sessionContext.debug = localCache.getConfig( _sessionContext.debug = !!localCache.getConfig(
cacheSnapshot, cacheSnapshot,
"cwmp.debug", "cwmp.debug",
configContext configContext
+34 -33
View File
@@ -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";
import { MongoClient, ObjectID } from "mongodb"; import { MongoClient, ObjectID } from "mongodb";
import { get } from "./config"; import { get } from "./config";
@@ -36,9 +35,9 @@ export let tasksCollection,
export let client; export let client;
export function connect(callback) { export function connect(callback): void {
MongoClient.connect( MongoClient.connect(
get("MONGODB_CONNECTION_URL"), "" + get("MONGODB_CONNECTION_URL"),
{ useNewUrlParser: true }, { useNewUrlParser: true },
(err, _client) => { (err, _client) => {
if (err) return void callback(err); if (err) return void callback(err);
@@ -63,13 +62,13 @@ export function connect(callback) {
); );
} }
export function disconnect() { export function disconnect(): void {
if (client) 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();
@@ -88,14 +87,14 @@ function optimizeProjection(obj) {
return obj; return obj;
} }
export function fetchDevice(id, timestamp, patterns, callback) { export function fetchDevice(id, timestamp, patterns, callback): void {
const MAX_DEPTH = get("MAX_DEPTH", id); const MAX_DEPTH = +get("MAX_DEPTH", id);
if (!patterns || !patterns.length) patterns = [[[], (1 << MAX_DEPTH) - 1]]; if (!patterns || !patterns.length) patterns = [[[], (1 << MAX_DEPTH) - 1]];
let projection = {}; let projection = {};
const projectionTree = {}; const projectionTree = {};
function func(path, pats, projTree) { function func(path, pats, projTree): void {
const children = {}; const children = {};
for (const pat of pats) { for (const pat of pats) {
const fragment = pat[0][path.length] || "*"; const fragment = pat[0][path.length] || "*";
@@ -232,16 +231,16 @@ export function fetchDevice(id, timestamp, patterns, callback) {
} }
} }
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, ts, descendantsFetched, projTree): void {
let thisFetched = false; let thisFetched = false;
if (descendantsFetched) { if (descendantsFetched) {
thisFetched = true; thisFetched = true;
@@ -278,7 +277,11 @@ export 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: {
object?: [number, number];
writable?: [number, number];
value?: [number, [string | number | boolean, string]];
} = {};
let t = obj["_timestamp"] || 1; let t = obj["_timestamp"] || 1;
if (ts > t) t = ts; if (ts > t) t = ts;
@@ -392,7 +395,7 @@ export 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"], ["Tags"],
timestamp, timestamp,
@@ -400,7 +403,7 @@ export function fetchDevice(id, timestamp, patterns, callback) {
]); ]);
} }
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], ["Tags", t],
@@ -478,7 +481,7 @@ export function saveDevice(
isNew, isNew,
sessionTimestamp, sessionTimestamp,
callback 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()) {
@@ -501,7 +504,7 @@ export function saveDevice(
.concat("_timestamp") .concat("_timestamp")
.join(".") .join(".")
] = 1; ] = 1;
} else if (diff[2] !== diff[1]) { } else {
const parent = deviceData.paths.get(diff[0].slice(0, -1)); 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"][
@@ -515,8 +518,6 @@ export function saveDevice(
} }
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];
@@ -726,7 +727,7 @@ export function saveDevice(
); );
} }
export function getFaults(deviceId, callback) { export function getFaults(deviceId, callback): void {
faultsCollection faultsCollection
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } }) .find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
.toArray((err, res) => { .toArray((err, res) => {
@@ -747,7 +748,7 @@ export function getFaults(deviceId, callback) {
}); });
} }
export 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;
@@ -758,11 +759,11 @@ export function saveFault(deviceId, channel, fault, callback) {
faultsCollection.replaceOne({ _id: id }, fault, { upsert: true }, callback); faultsCollection.replaceOne({ _id: id }, fault, { upsert: true }, callback);
} }
export function deleteFault(deviceId, channel, callback) { export function deleteFault(deviceId, channel, callback): void {
faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` }, callback); faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` }, callback);
} }
export 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 = [];
@@ -807,14 +808,14 @@ export function getDueTasks(deviceId, timestamp, callback) {
); );
} }
export function clearTasks(deviceId, taskIds, callback) { export function clearTasks(deviceId, taskIds, callback): void {
tasksCollection.deleteMany( tasksCollection.deleteMany(
{ _id: { $in: taskIds.map(id => new ObjectID(id)) } }, { _id: { $in: taskIds.map(id => new ObjectID(id)) } },
callback callback
); );
} }
export function getOperations(deviceId, callback) { export function getOperations(deviceId, callback): void {
operationsCollection operationsCollection
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } }) .find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
.toArray((err, res) => { .toArray((err, res) => {
@@ -835,7 +836,7 @@ export function getOperations(deviceId, callback) {
}); });
} }
export 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;
@@ -851,34 +852,34 @@ export function saveOperation(deviceId, commandKey, operation, callback) {
); );
} }
export function deleteOperation(deviceId, commandKey, callback) { export function deleteOperation(deviceId, commandKey, callback): void {
operationsCollection.deleteOne( operationsCollection.deleteOne(
{ _id: `${deviceId}:${commandKey}` }, { _id: `${deviceId}:${commandKey}` },
callback callback
); );
} }
export function getPresets(callback) { export function getPresets(callback): void {
presetsCollection.find().toArray(callback); presetsCollection.find().toArray(callback);
} }
export function getObjects(callback) { export function getObjects(callback): void {
objectsCollection.find().toArray(callback); objectsCollection.find().toArray(callback);
} }
export function getProvisions(callback) { export function getProvisions(callback): void {
provisionsCollection.find().toArray(callback); provisionsCollection.find().toArray(callback);
} }
export function getVirtualParameters(callback) { export function getVirtualParameters(callback): void {
virtualParametersCollection.find().toArray(callback); virtualParametersCollection.find().toArray(callback);
} }
export function getFiles(callback) { export function getFiles(callback): void {
filesCollection.find().toArray(callback); filesCollection.find().toArray(callback);
} }
export function getConfig(callback) { export function getConfig(callback): void {
configCollection.find().toArray((err, res) => { configCollection.find().toArray((err, res) => {
if (err) return void callback(err); if (err) return void callback(err);
@@ -16,16 +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";
import * as config from "./config"; import * as config from "./config";
import * as common from "./common"; import * as common from "./common";
import * as device from "./device"; import * as device from "./device";
import * as scheduling from "./scheduling"; import * as scheduling from "./scheduling";
const MAX_DEPTH = config.get("MAX_DEPTH"); const MAX_DEPTH = +config.get("MAX_DEPTH");
export function refresh(sessionContext, provision, declarations) { export function refresh(sessionContext, provision, declarations): boolean {
const path = common.parsePath(provision[1]).slice(); const path = common.parsePath(provision[1]).slice();
const l = path.length; const l = path.length;
path.length = MAX_DEPTH; path.length = MAX_DEPTH;
@@ -48,7 +47,7 @@ export function refresh(sessionContext, provision, declarations) {
return true; return true;
} }
export function value(sessionContext, provision, declarations) { export function value(sessionContext, provision, declarations): boolean {
const path = common.parsePath(provision[1]); const path = common.parsePath(provision[1]);
declarations.push({ declarations.push({
path: path, path: path,
@@ -62,7 +61,7 @@ export function value(sessionContext, provision, declarations) {
return true; return true;
} }
export function tag(sessionContext, provision, declarations) { export function tag(sessionContext, provision, declarations): boolean {
const path = ["Tags", provision[1]]; const path = ["Tags", provision[1]];
declarations.push({ declarations.push({
@@ -77,7 +76,7 @@ export function tag(sessionContext, provision, declarations) {
return true; return true;
} }
export function reboot(sessionContext, provision, declarations) { export function reboot(sessionContext, provision, declarations): boolean {
declarations.push({ declarations.push({
path: ["Reboot"], path: ["Reboot"],
pathGet: 1, pathGet: 1,
@@ -90,7 +89,7 @@ export function reboot(sessionContext, provision, declarations) {
return true; return true;
} }
export function reset(sessionContext, provision, declarations) { export function reset(sessionContext, provision, declarations): boolean {
declarations.push({ declarations.push({
path: ["FactoryReset"], path: ["FactoryReset"],
pathGet: 1, pathGet: 1,
@@ -103,7 +102,7 @@ export function reset(sessionContext, provision, declarations) {
return true; return true;
} }
export function download(sessionContext, provision, declarations) { export function download(sessionContext, provision, declarations): boolean {
const alias = [ const alias = [
["FileType"], ["FileType"],
provision[1] || "", provision[1] || "",
@@ -140,7 +139,7 @@ export function instances(
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;
+62 -42
View File
@@ -16,7 +16,8 @@
* 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 { ParamPath, DeviceData, Declaration, Clear } from "./types";
const CHANGE_FLAGS = { const CHANGE_FLAGS = {
object: 2, object: 2,
@@ -24,7 +25,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 +33,16 @@ function parseBool(v) {
else return null; else return null;
} }
export function sanitizeParameterValue(parameterValue) { export function sanitizeParameterValue(
parameterValue
): [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 +55,22 @@ export 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]);
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]);
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,10 +88,10 @@ export function sanitizeParameterValue(parameterValue) {
return parameterValue; return parameterValue;
} }
export function getAliasDeclarations(path, timestamp) { export function getAliasDeclarations(path, timestamp): Declaration[] {
const decs = []; const decs = [] as Declaration[];
function recursive(p, prefix) { function recursive(p, prefix): void {
const pattern = prefix.concat(p); const pattern = prefix.concat(p);
for (let i = prefix.length; i < pattern.length; ++i) { for (let i = prefix.length; i < pattern.length; ++i) {
@@ -113,13 +118,17 @@ export function getAliasDeclarations(path, timestamp) {
return decs; return decs;
} }
export function unpack(deviceData, path, revision) { export function unpack(
let allMatches = []; deviceData,
path: ParamPath,
revision?: number
): ParamPath[] {
let allMatches = [] as ParamPath[];
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.slice() as ParamPath;
wildcardPath.wildcard = path.wildcard; wildcardPath.wildcard = path.wildcard;
for (let i = 0; i < wildcardPath.length; ++i) { for (let i = 0; i < wildcardPath.length; ++i) {
if (Array.isArray(wildcardPath[i])) { if (Array.isArray(wildcardPath[i])) {
@@ -134,7 +143,9 @@ export function unpack(deviceData, path, revision) {
for (let i = path.length - 1; i >= 0; --i) { for (let i = path.length - 1; i >= 0; --i) {
if (Array.isArray(path[i])) { if (Array.isArray(path[i])) {
for (let j = 0; j < path[i].length; j += 2) { for (let j = 0; j < path[i].length; j += 2) {
const p = wildcardPath.slice(0, i + 1).concat(path[i][j]); const p = wildcardPath
.slice(0, i + 1)
.concat(path[i][j]) as ParamPath;
p.alias = p.alias =
((wildcardPath.alias & ((1 << (i + 1)) - 1)) + path[i][j].alias) << ((wildcardPath.alias & ((1 << (i + 1)) - 1)) + path[i][j].alias) <<
(i + 1); (i + 1);
@@ -143,7 +154,7 @@ export function unpack(deviceData, path, revision) {
path[i][j].wildcard) << path[i][j].wildcard) <<
(i + 1); (i + 1);
const unpacked = unpack(deviceData, p, revision); const unpacked = unpack(deviceData, p, revision);
const filtered = []; const filtered = [] as ParamPath[];
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 (
@@ -161,7 +172,7 @@ export function unpack(deviceData, path, revision) {
if (k < 0) { if (k < 0) {
filtered.push(allMatches[m]); filtered.push(allMatches[m]);
allMatches[m] = []; allMatches[m] = null;
} }
} }
} }
@@ -172,21 +183,19 @@ export 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[i];
const b = p2[i];
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;
}); });
@@ -200,7 +209,7 @@ export function clear(
timestamp, timestamp,
attributes, attributes,
changeFlags = 0 changeFlags = 0
) { ): void {
const changeTrackers = {}; const changeTrackers = {};
timestamp = timestamp || 0; timestamp = timestamp || 0;
@@ -259,7 +268,7 @@ export function clear(
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 {
const t = typeof a; const t = typeof a;
if ( if (
a === null || a === null ||
@@ -274,7 +283,13 @@ function compareEquality(a, b) {
return JSON.stringify(a) === JSON.stringify(b); return JSON.stringify(a) === JSON.stringify(b);
} }
export function set(deviceData, path, timestamp, attributes, toClear) { export function set(
deviceData: DeviceData,
path: ParamPath,
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);
@@ -330,7 +345,7 @@ export function set(deviceData, path, timestamp, attributes, toClear) {
if (path.length > 1) { if (path.length > 1) {
toClear = set( toClear = set(
deviceData, deviceData,
path.slice(0, path.length - 1), path.slice(0, path.length - 1) as ParamPath,
timestamp, timestamp,
{ object: [timestamp, 1] }, { object: [timestamp, 1] },
toClear toClear
@@ -372,7 +387,12 @@ export function set(deviceData, path, timestamp, attributes, toClear) {
return toClear; return toClear;
} }
export function track(deviceData, path, marker, attributes) { export function track(
deviceData: DeviceData,
path: ParamPath,
marker: string,
attributes?
): void {
path = deviceData.paths.add(path); path = deviceData.paths.add(path);
let f = 1; let f = 1;
@@ -388,7 +408,7 @@ export function track(deviceData, path, marker, attributes) {
cur[marker] |= f; cur[marker] |= f;
} }
export function clearTrackers(deviceData, tracker) { export function clearTrackers(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];
+6 -7
View File
@@ -16,18 +16,17 @@
* 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 { spawn } from "child_process"; import { spawn } from "child_process";
import crypto from "crypto"; import * as crypto from "crypto";
import * as config from "./config"; import * as config from "./config";
const TIMEOUT = config.get("EXT_TIMEOUT"); const TIMEOUT = +config.get("EXT_TIMEOUT");
const processes = {}; const processes = {};
const jobs = new Map(); const jobs = new Map();
function messageHandler(message) { function messageHandler(message): void {
const func = jobs.get(message[0]); const func = jobs.get(message[0]);
if (func) { if (func) {
jobs.delete(message[0]); jobs.delete(message[0]);
@@ -35,7 +34,7 @@ function messageHandler(message) {
} }
} }
export 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"); const id = crypto.randomBytes(8).toString("hex");
@@ -77,7 +76,7 @@ export function run(args, callback) {
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;
@@ -96,7 +95,7 @@ function kill(process, callback) {
}, 100); }, 100);
} }
export 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;
+4 -5
View File
@@ -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";
import url from "url"; import * as url from "url";
import querystring from "querystring"; import * as querystring from "querystring";
import { GridFSBucket } from "mongodb"; import { GridFSBucket } from "mongodb";
import * as db from "./db"; import * as db from "./db";
export 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));
@@ -46,7 +45,7 @@ export function listener(request, response) {
}); });
const bucket = new 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 {
@@ -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.
export 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;
+17 -12
View File
@@ -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";
interface Instance {
[name: string]: string;
}
export default class InstanceSet { export default class InstanceSet {
constructor() { 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 @@ export default 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,23 +109,23 @@ export default 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;
} }
} }
+37 -18
View File
@@ -16,11 +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 vm from "vm";
import crypto from "crypto";
import * as vm from "vm";
import * as crypto from "crypto";
import * as config from "./config"; import * as config from "./config";
import * as db from "./db"; import * as db from "./db";
import * as cache from "./cache"; import * as cache from "./cache";
@@ -28,6 +26,7 @@ import * as query from "./query";
import * as logger from "./logger"; import * as logger from "./logger";
import * as scheduling from "./scheduling"; import * as scheduling from "./scheduling";
import * as expression from "./common/expression"; import * as expression from "./common/expression";
import { Preset } from "./types";
const REFRESH = 3000; const REFRESH = 3000;
const EVICT_TIMEOUT = 60000; const EVICT_TIMEOUT = 60000;
@@ -36,7 +35,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) {
@@ -65,7 +64,7 @@ function computeHash(snapshot) {
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))
@@ -75,7 +74,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);
@@ -126,7 +125,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) {
@@ -369,7 +368,16 @@ function refresh(callback) {
}); });
} }
export function getCurrentSnapshot(callback) { interface GetCurrentSnapshotCallback {
(err: null, currentSnapshot: string): void;
(err: Error, currentSnapshot?: null): void;
}
export function getCurrentSnapshot(callback: GetCurrentSnapshotCallback);
export function getCurrentSnapshot(): Promise<string>;
export function getCurrentSnapshot(
callback?: GetCurrentSnapshotCallback
): Promise<string> | void {
if (Date.now() < nextRefresh) { if (Date.now() < nextRefresh) {
if (callback) return callback(null, currentSnapshot); if (callback) return callback(null, currentSnapshot);
return Promise.resolve(currentSnapshot); return Promise.resolve(currentSnapshot);
@@ -389,35 +397,46 @@ export function getCurrentSnapshot(callback) {
}); });
} }
export function hasSnapshot(hash) { export function hasSnapshot(hash): boolean {
return snapshots.has(hash); return snapshots.has(hash);
} }
export 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;
} }
export 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;
} }
export 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;
} }
export 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;
} }
export function getConfig(snapshotKey, key, context, now) { export function getConfig(
snapshotKey,
key,
context?,
now?
): string | number | boolean | null {
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");
@@ -446,21 +465,21 @@ export function getConfig(snapshotKey, key, context, now) {
return expression.evaluate(snapshot.config[key], context, now || Date.now()); return expression.evaluate(snapshot.config[key], context, now || Date.now());
} }
export function getAuthConfig(snapshotKey) { export function getAuthConfig(snapshotKey): {} {
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.config.auth; return snapshot.config.auth;
} }
export function getPermissionsConfig(snapshotKey) { export function getPermissionsConfig(snapshotKey): {} {
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.config.permissions; return snapshot.config.permissions;
} }
export function getUiConfig(snapshotKey) { export function getUiConfig(snapshotKey): {} {
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");
+33 -21
View File
@@ -16,10 +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 fs from "fs"; import * as fs from "fs";
import os from "os"; import * as os from "os";
import * as config from "./config"; import * as config from "./config";
@@ -28,20 +27,35 @@ 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();
} }
export 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 @@ export 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 @@ export function init(service, version) {
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
} }
export function close() { export function close(): void {
accessLogStream.end(); accessLogStream.end();
logStream.end(); logStream.end();
} }
export 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 =
@@ -198,7 +210,7 @@ export function flatten(details) {
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>";
@@ -211,7 +223,7 @@ 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 = "";
@@ -255,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);
@@ -265,22 +277,22 @@ function log(details) {
} }
} }
export function info(details) { export function info(details): void {
details.severity = "info"; details.severity = "info";
log(details); log(details);
} }
export function warn(details) { export function warn(details): void {
details.severity = "warn"; details.severity = "warn";
log(details); log(details);
} }
export function error(details) { export function error(details): void {
details.severity = "error"; details.severity = "error";
log(details); log(details);
} }
export 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);
@@ -290,17 +302,17 @@ export function accessLog(details) {
} }
} }
export function accessInfo(details) { export function accessInfo(details): void {
details.severity = "info"; details.severity = "info";
accessLog(details); accessLog(details);
} }
export function accessWarn(details) { export function accessWarn(details): void {
details.severity = "warn"; details.severity = "warn";
accessLog(details); accessLog(details);
} }
export function accessError(details) { export function accessError(details): void {
details.severity = "error"; details.severity = "error";
accessLog(details); accessLog(details);
} }
+31 -24
View File
@@ -16,12 +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";
import url from "url"; import * as url from "url";
import mongodb from "mongodb"; import * as mongodb from "mongodb";
import querystring from "querystring"; import * as querystring from "querystring";
import vm from "vm"; import * as vm from "vm";
import { exec } from "child_process"; import { exec } from "child_process";
import * as config from "./config"; import * as config from "./config";
import * as db from "./db"; import * as db from "./db";
@@ -43,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}`);
@@ -51,17 +50,20 @@ function throwError(err, httpResponse) {
throw err; throw err;
} }
export 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;
@@ -80,7 +82,7 @@ export 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 },
@@ -117,7 +119,7 @@ export 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 },
@@ -320,7 +322,7 @@ export 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);
@@ -340,7 +342,7 @@ export 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(
@@ -499,15 +501,18 @@ export 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);
@@ -577,7 +582,7 @@ export 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}`);
@@ -606,14 +611,14 @@ export 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")
@@ -624,9 +629,11 @@ export 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);
+16 -7
View File
@@ -16,19 +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";
import { ParamPath } from "./types";
export default class PathSet { export default class PathSet {
constructor() { private lengthIndex: Set<ParamPath>[];
private fragmentIndex: Map<string, Set<ParamPath>>[];
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: ParamPath): ParamPath {
const p = this.get(path); const p = this.get(path);
if (p) return p; if (p) return p;
@@ -50,7 +54,7 @@ export default class PathSet {
let fragmentIndexSet = fragmentIndex.get(fragment); let fragmentIndexSet = fragmentIndex.get(fragment);
if (!fragmentIndexSet) { if (!fragmentIndexSet) {
fragmentIndexSet = new Set(); fragmentIndexSet = new Set<ParamPath>();
fragmentIndex.set(fragment, fragmentIndexSet); fragmentIndex.set(fragment, fragmentIndexSet);
} }
@@ -65,7 +69,7 @@ export default class PathSet {
return path; return path;
} }
get(pattern) { public get(pattern: ParamPath): ParamPath {
const lengthIndex = this.lengthIndex[pattern.length]; const lengthIndex = this.lengthIndex[pattern.length];
if (!lengthIndex || !lengthIndex.size) return null; if (!lengthIndex || !lengthIndex.size) return null;
@@ -102,7 +106,12 @@ export default class PathSet {
return null; return null;
} }
find(pattern, superset = false, subset = false, depth = pattern.length) { public find(
pattern: ParamPath,
superset: boolean = false,
subset: boolean = false,
depth: number = pattern.length
): ParamPath[] {
const res = []; const res = [];
const groups = [[]]; const groups = [[]];
+18 -19
View File
@@ -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";
import * as common from "./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,10 +52,10 @@ function normalize(input) {
return input; return input;
} }
export 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.conact(expandValue(j)); for (const j of value) a = a.concat(expandValue(j));
return [a]; return [a];
} else if (common.typeOf(value) !== common.OBJECT_TYPE) { } else if (common.typeOf(value) !== common.OBJECT_TYPE) {
const n = normalize(value); const n = normalize(value);
@@ -89,7 +88,7 @@ export 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;
} }
export 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 @@ export 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) {
} }
} }
export 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 @@ export 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,7 +216,7 @@ export function test(params, query) {
return true; return true;
} }
function matchType(src, dst) { function matchType(src, dst): string | number | boolean {
const type = typeof src; const type = typeof src;
if (type === "string") { if (type === "string") {
return String(dst); return String(dst);
@@ -233,7 +233,7 @@ function matchType(src, dst) {
return dst; return dst;
} }
export function testFilter(obj, filter) { export function testFilter(obj, filter): boolean {
for (const [k, v] of Object.entries(filter)) { for (const [k, v] of Object.entries(filter)) {
const [param, op] = k.split(/([^a-zA-Z0-9\-_.].*)/, 2); const [param, op] = k.split(/([^a-zA-Z0-9\-_.].*)/, 2);
const val = matchType(obj[param], v); const val = matchType(obj[param], v);
@@ -264,12 +264,11 @@ export function testFilter(obj, filter) {
return true; return true;
} }
export function convertMongoQueryToFilters(query, filters) { export function convertMongoQueryToFilters(query, filters = {}): {} {
filters = filters || {};
for (let [k, v] of Object.entries(query)) { for (let [k, v] of Object.entries(query)) {
if (k[0] === "$") { if (k[0] === "$") {
if (k === "$and") if (k === "$and")
for (const vv of v) convertMongoQueryToFilters(vv, filters); for (const vv of v as any[]) convertMongoQueryToFilters(vv, filters);
else throw new Error(`Operator ${k} not supported`); else throw new Error(`Operator ${k} not supported`);
} else if (k === "_tags") { } else if (k === "_tags") {
if (common.typeOf(v) === common.OBJECT_TYPE) { if (common.typeOf(v) === common.OBJECT_TYPE) {
@@ -279,7 +278,7 @@ export function convertMongoQueryToFilters(query, filters) {
else throw new Error(`Operator ${kk} not supported`); else throw new Error(`Operator ${kk} not supported`);
} }
} else { } else {
v = v.replace(/[^a-zA-Z0-9-]+/g, "_"); v = (v as string).replace(/[^a-zA-Z0-9-]+/g, "_");
filters[`Tags.${v}`] = true; filters[`Tags.${v}`] = true;
} }
} else { } else {
@@ -346,11 +345,11 @@ export function convertMongoQueryToFilters(query, filters) {
return filters; return filters;
} }
export function sanitizeQueryTypes(query, types) { export 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)) {
+30 -18
View File
@@ -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";
import vm from "vm";
import * as vm from "vm";
import seedrandom from "seedrandom"; import seedrandom from "seedrandom";
import * as common from "./common"; import * as common from "./common";
import * as device from "./device"; import * as device from "./device";
import * as extensions from "./extensions"; import * as extensions from "./extensions";
import * as logger from "./logger"; import * as logger from "./logger";
import * as scheduling from "./scheduling"; import * as scheduling from "./scheduling";
import { ParamPath, Fault } from "./types";
// 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, 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() {
@@ -189,7 +197,11 @@ class ParameterWrapper {
} }
} }
function declare(path, timestamps, values) { function declare(
path: ParamPath,
timestamps: { [attr: string]: number },
values: { [attr: string]: any }
): ParameterWrapper {
state.uncommitted = true; state.uncommitted = true;
if (!timestamps) timestamps = {}; if (!timestamps) timestamps = {};
@@ -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, timestamp, 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([common.parsePath(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,7 +282,7 @@ function ext() {
throw EXT; throw EXT;
} }
function log(msg, meta) { function log(msg, 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,
@@ -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): 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: {
@@ -335,7 +347,7 @@ export function run(
maxRevision, maxRevision,
callback, callback,
extCounter = 0 extCounter = 0
) { ): void {
state = { state = {
sessionContext: sessionContext, sessionContext: sessionContext,
revision: startRevision, revision: startRevision,
+7 -9
View File
@@ -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";
import crypto from "crypto"; import * as crypto from "crypto";
import * as later from "later";
import later from "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) {
); );
} }
export function variance(deviceId, vrnc) { export function variance(deviceId, vrnc): number {
return (md532(deviceId) >>> 0) % vrnc; return (md532(deviceId) >>> 0) % vrnc;
} }
export 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;
} }
export 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));
} }
export 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];
+16 -9
View File
@@ -16,15 +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";
import fs from "fs"; import * as fs from "fs";
import http from "http"; import * as http from "http";
import https from "https"; import * as https from "https";
let server, listener; let server: http.Server | https.Server;
let listener: (...args) => void;
function closeServer(timeout, callback) { function closeServer(timeout, callback): void {
if (!server) return void callback(); if (!server) return void callback();
setTimeout(() => { setTimeout(() => {
@@ -48,13 +48,20 @@ function closeServer(timeout, callback) {
}); });
} }
export function start(port, networkInterface, ssl, _listener, onConnection) { export function start(
port,
networkInterface,
ssl,
_listener,
onConnection?
): void {
listener = _listener; listener = _listener;
if (ssl) { if (ssl) {
const options = { const options = {
key: fs.readFileSync(ssl.key), key: fs.readFileSync(ssl.key),
cert: fs.readFileSync(ssl.cert) cert: fs.readFileSync(ssl.cert),
ca: null
}; };
try { try {
@@ -76,7 +83,7 @@ export function start(port, networkInterface, ssl, _listener, onConnection) {
server.listen(port, networkInterface); server.listen(port, networkInterface);
} }
export function stop() { export function stop(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
reject(new Error("Could not close server in a timely manner")); reject(new Error("Could not close server in a timely manner"));
+203 -96
View File
@@ -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";
import * as config from "./config"; import * as config from "./config";
import * as common from "./common"; import * as common from "./common";
@@ -29,8 +28,20 @@ import { estimateGpnCount } from "./gpn-heuristic";
import PathSet from "./path-set"; import PathSet from "./path-set";
import VersionedMap from "./versioned-map"; import VersionedMap from "./versioned-map";
import InstanceSet from "./instance-set"; import InstanceSet from "./instance-set";
import {
Attributes,
SessionContext,
ParamPath,
DeviceData,
VirtualParameterDeclaration,
GetAcsRequest,
SetAcsRequest,
AttributeTimestamps,
AttributeValues,
Fault
} from "./types";
function initDeviceData() { function initDeviceData(): DeviceData {
return { return {
paths: new PathSet(), paths: new PathSet(),
timestamps: new VersionedMap(), timestamps: new VersionedMap(),
@@ -41,9 +52,14 @@ function initDeviceData() {
}; };
} }
export function init(deviceId, cwmpVersion, timeout, callback) { export function init(
deviceId: string,
cwmpVersion: string,
timeout: number,
callback: (err: Error, sessionContext: SessionContext) => void
): void {
const timestamp = Date.now(); const timestamp = Date.now();
const sessionContext = { const sessionContext: SessionContext = {
timestamp: timestamp, timestamp: timestamp,
deviceId: deviceId, deviceId: deviceId,
deviceData: initDeviceData(), deviceData: initDeviceData(),
@@ -63,7 +79,7 @@ export function init(deviceId, cwmpVersion, timeout, callback) {
callback(null, sessionContext); callback(null, sessionContext);
} }
function loadParameters(sessionContext, callback) { function loadParameters(sessionContext: SessionContext, callback): void {
if (!sessionContext.toLoad || !sessionContext.toLoad.size) if (!sessionContext.toLoad || !sessionContext.toLoad.size)
return void callback(); return void callback();
@@ -79,7 +95,7 @@ function loadParameters(sessionContext, callback) {
// Device not available in database, mark as new // Device not available in database, mark as new
sessionContext.new = true; sessionContext.new = true;
loaded = [ loaded = [
[[], (1 << config.get("MAX_DEPTH", sessionContext.deviceId)) - 1] [[], (1 << +config.get("MAX_DEPTH", sessionContext.deviceId)) - 1]
]; ];
parameters = []; parameters = [];
} }
@@ -104,7 +120,7 @@ function loadParameters(sessionContext, callback) {
); );
} }
function generateRpcId(sessionContext) { function generateRpcId(sessionContext: SessionContext): string {
return ( return (
sessionContext.timestamp.toString(16) + sessionContext.timestamp.toString(16) +
("0" + sessionContext.cycle.toString(16)).slice(-2) + ("0" + sessionContext.cycle.toString(16)).slice(-2) +
@@ -112,11 +128,11 @@ function generateRpcId(sessionContext) {
); );
} }
export function inform(sessionContext, rpcReq, callback) { export function inform(sessionContext: SessionContext, rpcReq, callback): void {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
const params = [ const params: [ParamPath, number, Attributes][] = [
[ [
["DeviceID", "Manufacturer"], ["DeviceID", "Manufacturer"] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -126,7 +142,7 @@ export function inform(sessionContext, rpcReq, callback) {
], ],
[ [
["DeviceID", "OUI"], ["DeviceID", "OUI"] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -136,7 +152,7 @@ export function inform(sessionContext, rpcReq, callback) {
], ],
[ [
["DeviceID", "ProductClass"], ["DeviceID", "ProductClass"] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -146,7 +162,7 @@ export function inform(sessionContext, rpcReq, callback) {
], ],
[ [
["DeviceID", "SerialNumber"], ["DeviceID", "SerialNumber"] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -169,7 +185,7 @@ export function inform(sessionContext, rpcReq, callback) {
} }
params.push([ params.push([
["Events", "Inform"], ["Events", "Inform"] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -180,7 +196,7 @@ export function inform(sessionContext, rpcReq, callback) {
for (const e of rpcReq.event) { for (const e of rpcReq.event) {
params.push([ params.push([
["Events", e.replace(/\s+/g, "_")], ["Events", e.replace(/\s+/g, "_")] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -191,7 +207,7 @@ export function inform(sessionContext, rpcReq, callback) {
} }
// Preload DeviceID params // Preload DeviceID params
loadPath(sessionContext, ["DeviceID", "*"]); loadPath(sessionContext, ["DeviceID", "*"] as ParamPath);
for (const p of params) loadPath(sessionContext, p[0]); for (const p of params) loadPath(sessionContext, p[0]);
@@ -200,7 +216,7 @@ export function inform(sessionContext, rpcReq, callback) {
if (sessionContext.new) { if (sessionContext.new) {
params.push([ params.push([
["DeviceID", "ID"], ["DeviceID", "ID"] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -209,7 +225,7 @@ export function inform(sessionContext, rpcReq, callback) {
} }
]); ]);
params.push([ params.push([
["Events", "Registered"], ["Events", "Registered"] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -244,7 +260,11 @@ export function inform(sessionContext, rpcReq, callback) {
}); });
} }
export function transferComplete(sessionContext, rpcReq, callback) { export function transferComplete(
sessionContext: SessionContext,
rpcReq,
callback
): void {
const revision = const revision =
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1; (sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision; sessionContext.deviceData.timestamps.revision = revision;
@@ -278,7 +298,7 @@ export function transferComplete(sessionContext, rpcReq, callback) {
); );
} }
loadPath(sessionContext, ["Downloads", instance, "*"]); loadPath(sessionContext, ["Downloads", instance, "*"] as ParamPath);
loadParameters(sessionContext, err => { loadParameters(sessionContext, err => {
if (err) return void callback(err); if (err) return void callback(err);
@@ -292,7 +312,7 @@ export function transferComplete(sessionContext, rpcReq, callback) {
"Downloads", "Downloads",
instance, instance,
"LastDownload" "LastDownload"
]); ] as ParamPath);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, p,
@@ -305,7 +325,7 @@ export function transferComplete(sessionContext, rpcReq, callback) {
"Downloads", "Downloads",
instance, instance,
"LastFileType" "LastFileType"
]); ] as ParamPath);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, p,
@@ -318,7 +338,7 @@ export function transferComplete(sessionContext, rpcReq, callback) {
"Downloads", "Downloads",
instance, instance,
"LastFileName" "LastFileName"
]); ] as ParamPath);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, p,
@@ -331,7 +351,7 @@ export function transferComplete(sessionContext, rpcReq, callback) {
"Downloads", "Downloads",
instance, instance,
"LastTargetFileName" "LastTargetFileName"
]); ] as ParamPath);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, p,
@@ -344,7 +364,7 @@ export function transferComplete(sessionContext, rpcReq, callback) {
"Downloads", "Downloads",
instance, instance,
"StartTime" "StartTime"
]); ] as ParamPath);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, p,
@@ -357,7 +377,7 @@ export function transferComplete(sessionContext, rpcReq, callback) {
"Downloads", "Downloads",
instance, instance,
"CompleteTime" "CompleteTime"
]); ] as ParamPath);
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
p, p,
@@ -372,8 +392,12 @@ export function transferComplete(sessionContext, rpcReq, callback) {
}); });
} }
function revertDownloadParameters(sessionContext, instance, callback) { function revertDownloadParameters(
loadPath(sessionContext, ["Downloads", instance, "*"]); sessionContext: SessionContext,
instance,
callback
): void {
loadPath(sessionContext, ["Downloads", instance, "*"] as ParamPath);
loadParameters(sessionContext, err => { loadParameters(sessionContext, err => {
if (err) return void callback(err); if (err) return void callback(err);
@@ -386,7 +410,7 @@ function revertDownloadParameters(sessionContext, instance, callback) {
"Downloads", "Downloads",
instance, instance,
"LastDownload" "LastDownload"
]); ] as ParamPath);
const lastDownload = sessionContext.deviceData.attributes.get(p); const lastDownload = sessionContext.deviceData.attributes.get(p);
@@ -394,7 +418,7 @@ function revertDownloadParameters(sessionContext, instance, callback) {
"Downloads", "Downloads",
instance, instance,
"Download" "Download"
]); ] as ParamPath);
const toClear = device.set(sessionContext.deviceData, p, timestamp, { const toClear = device.set(sessionContext.deviceData, p, timestamp, {
value: [ value: [
@@ -410,7 +434,10 @@ function revertDownloadParameters(sessionContext, instance, callback) {
}); });
} }
export function timeoutOperations(sessionContext, callback) { export function timeoutOperations(
sessionContext: SessionContext,
callback
): void {
const revision = const revision =
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1; (sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
sessionContext.deviceData.timestamps.revision = revision; sessionContext.deviceData.timestamps.revision = revision;
@@ -427,7 +454,7 @@ export function timeoutOperations(sessionContext, callback) {
} }
const DOWNLOAD_TIMEOUT = const DOWNLOAD_TIMEOUT =
localCache.getConfig( +localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.downloadTimeout", "cwmp.downloadTimeout",
sessionContext.configContext sessionContext.configContext
@@ -460,11 +487,15 @@ export function timeoutOperations(sessionContext, callback) {
if ((counter -= 2) === 1) callback(null, faults, operations); if ((counter -= 2) === 1) callback(null, faults, operations);
} }
export function addProvisions(sessionContext, channel, provisions) { export function addProvisions(
sessionContext: SessionContext,
channel,
provisions
): void {
// Multiply by two because every iteration is two // Multiply by two because every iteration is two
// phases: read and update // phases: read and update
const MAX_ITERATIONS = const MAX_ITERATIONS =
localCache.getConfig( +localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations", "cwmp.maxCommitIterations",
sessionContext.configContext sessionContext.configContext
@@ -513,11 +544,11 @@ export function addProvisions(sessionContext, channel, provisions) {
} }
} }
export function clearProvisions(sessionContext) { export function clearProvisions(sessionContext: SessionContext): void {
// Multiply by two because every iteration is two // Multiply by two because every iteration is two
// phases: read and update // phases: read and update
const MAX_ITERATIONS = const MAX_ITERATIONS =
localCache.getConfig( +localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations", "cwmp.maxCommitIterations",
sessionContext.configContext sessionContext.configContext
@@ -551,7 +582,7 @@ function runProvisions(
startRevision, startRevision,
endRevision, endRevision,
callback callback
) { ): void {
let done = true; let done = true;
let allDeclarations = []; let allDeclarations = [];
let allClear = []; let allClear = [];
@@ -616,7 +647,7 @@ function runVirtualParameters(
startRevision, startRevision,
endRevision, endRevision,
callback callback
) { ): void {
let done = true; let done = true;
const virtualParameterUpdates = []; const virtualParameterUpdates = [];
let allDeclarations = []; let allDeclarations = [];
@@ -660,7 +691,11 @@ function runVirtualParameters(
return void (counter = 0); return void (counter = 0);
} }
const ret = {}; const ret: {
writable?: number;
value?: [string | number | boolean, string?];
} = {};
if (_returnValue.writable != null) { if (_returnValue.writable != null) {
ret.writable = +!!_returnValue.writable; ret.writable = +!!_returnValue.writable;
} else if ( } else if (
@@ -758,7 +793,10 @@ function runVirtualParameters(
} }
} }
function runDeclarations(sessionContext, declarations) { function runDeclarations(
sessionContext: SessionContext,
declarations
): VirtualParameterDeclaration[] {
if (!sessionContext.syncState) { if (!sessionContext.syncState) {
sessionContext.syncState = { sessionContext.syncState = {
refreshAttributes: { refreshAttributes: {
@@ -768,7 +806,7 @@ function runDeclarations(sessionContext, declarations) {
value: new Set() value: new Set()
}, },
spv: new Map(), spv: new Map(),
gpn: new Set(), gpn: new Set<ParamPath>(),
gpnPatterns: new Map(), gpnPatterns: new Map(),
tags: new Map(), tags: new Map(),
virtualParameterDeclarations: [], virtualParameterDeclarations: [],
@@ -782,14 +820,20 @@ function runDeclarations(sessionContext, declarations) {
} }
const allDeclareTimestamps = new Map(); const allDeclareTimestamps = new Map();
const allDeclareAttributeTimestamps = new Map(); const allDeclareAttributeTimestamps = new Map<
const allDeclareAttributeValues = new Map(); ParamPath,
AttributeTimestamps
>();
const allDeclareAttributeValues = new Map<ParamPath, AttributeValues>();
const allVirtualParameters = localCache.getVirtualParameters( const allVirtualParameters = localCache.getVirtualParameters(
sessionContext.cacheSnapshot sessionContext.cacheSnapshot
); );
function mergeAttributeTimestamps(p, attrs) { function mergeAttributeTimestamps(
p: ParamPath,
attrs: AttributeTimestamps
): void {
let cur = allDeclareAttributeTimestamps.get(p); let cur = allDeclareAttributeTimestamps.get(p);
if (!cur) { if (!cur) {
allDeclareAttributeTimestamps.set(p, attrs); allDeclareAttributeTimestamps.set(p, attrs);
@@ -801,7 +845,11 @@ function runDeclarations(sessionContext, declarations) {
} }
} }
function mergeAttributeValues(p, attrs, defer) { function mergeAttributeValues(
p: ParamPath,
attrs: AttributeValues,
defer: boolean
): void {
let cur = allDeclareAttributeValues.get(p); let cur = allDeclareAttributeValues.get(p);
if (!cur) { if (!cur) {
if (!defer) allDeclareAttributeValues.set(p, attrs); if (!defer) allDeclareAttributeValues.set(p, attrs);
@@ -816,19 +864,26 @@ function runDeclarations(sessionContext, declarations) {
let unpacked; let unpacked;
if ((path.alias | path.wildcard) & 1 || path[0] === "VirtualParameters") { if ((path.alias | path.wildcard) & 1 || path[0] === "VirtualParameters") {
sessionContext.deviceData.paths.add(["VirtualParameters"]); sessionContext.deviceData.paths.add(["VirtualParameters"] as ParamPath);
if ((path.alias | path.wildcard) & 2) { if ((path.alias | path.wildcard) & 2) {
sessionContext.deviceData.paths.add(["VirtualParameters", "*"]); sessionContext.deviceData.paths.add([
for (const k of Object.keys(allVirtualParameters)) "VirtualParameters",
sessionContext.deviceData.paths.add(["VirtualParameters", k]); "*"
] as ParamPath);
for (const k of Object.keys(allVirtualParameters)) {
sessionContext.deviceData.paths.add([
"VirtualParameters",
k
] as ParamPath);
}
} }
} }
if ((path.alias | path.wildcard) & 1 || path[0] === "Reboot") if ((path.alias | path.wildcard) & 1 || path[0] === "Reboot")
sessionContext.deviceData.paths.add(["Reboot"]); sessionContext.deviceData.paths.add(["Reboot"] as ParamPath);
if ((path.alias | path.wildcard) & 1 || path[0] === "FactoryReset") if ((path.alias | path.wildcard) & 1 || path[0] === "FactoryReset")
sessionContext.deviceData.paths.add(["FactoryReset"]); sessionContext.deviceData.paths.add(["FactoryReset"] as ParamPath);
if (path.alias) { if (path.alias) {
const aliasDecs = device.getAliasDeclarations( const aliasDecs = device.getAliasDeclarations(
@@ -958,7 +1013,11 @@ function runDeclarations(sessionContext, declarations) {
); );
} }
export function rpcRequest(sessionContext, _declarations, callback) { export function rpcRequest(
sessionContext: SessionContext,
_declarations,
callback
): void {
if (sessionContext.rpcRequest != null) { if (sessionContext.rpcRequest != null) {
return void callback( return void callback(
null, null,
@@ -1097,7 +1156,7 @@ export function rpcRequest(sessionContext, _declarations, callback) {
// Multiply by two because every iteration is two // Multiply by two because every iteration is two
// phases: read and update // phases: read and update
const MAX_ITERATIONS = const MAX_ITERATIONS =
localCache.getConfig( +localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.maxCommitIterations", "cwmp.maxCommitIterations",
sessionContext.configContext sessionContext.configContext
@@ -1162,7 +1221,7 @@ export function rpcRequest(sessionContext, _declarations, callback) {
for (const k of Object.keys(allVirtualParameters)) { for (const k of Object.keys(allVirtualParameters)) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["VirtualParameters", k], ["VirtualParameters", k] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0] object: [timestamp, 0]
@@ -1272,7 +1331,7 @@ export function rpcRequest(sessionContext, _declarations, callback) {
if (index == null) { if (index == null) {
index = 0; index = 0;
for (const p of sessionContext.deviceData.paths.find( for (const p of sessionContext.deviceData.paths.find(
["Downloads", "*"], ["Downloads", "*"] as ParamPath,
false, false,
true true
)) { )) {
@@ -1285,7 +1344,7 @@ export function rpcRequest(sessionContext, _declarations, callback) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads"], ["Downloads"] as ParamPath,
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] }, { object: [timestamp, 1], writable: [timestamp, 1] },
toClear toClear
@@ -1293,7 +1352,7 @@ export function rpcRequest(sessionContext, _declarations, callback) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", "" + index], ["Downloads", "" + index] as ParamPath,
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] }, { object: [timestamp, 1], writable: [timestamp, 1] },
toClear toClear
@@ -1327,7 +1386,7 @@ export function rpcRequest(sessionContext, _declarations, callback) {
for (const [k, v] of Object.entries(params)) { for (const [k, v] of Object.entries(params)) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", `${index}`, k], ["Downloads", `${index}`, k] as ParamPath,
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
@@ -1340,7 +1399,7 @@ export function rpcRequest(sessionContext, _declarations, callback) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", `${index}`, "*"], ["Downloads", `${index}`, "*"] as ParamPath,
timestamp, timestamp,
null, null,
toClear toClear
@@ -1445,7 +1504,7 @@ export function rpcRequest(sessionContext, _declarations, callback) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["VirtualParameters", vparams[i][0]], ["VirtualParameters", vparams[i][0]] as ParamPath,
timestamp, timestamp,
vpu, vpu,
toClear toClear
@@ -1458,7 +1517,7 @@ export function rpcRequest(sessionContext, _declarations, callback) {
}); });
} }
function generateGetRpcRequest(sessionContext) { function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest {
const syncState = sessionContext.syncState; const syncState = sessionContext.syncState;
if (!syncState) return null; if (!syncState) return null;
@@ -1482,7 +1541,10 @@ function generateGetRpcRequest(sessionContext) {
} }
if (!found) { if (!found) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1)); const p = sessionContext.deviceData.paths.add(path.slice(
0,
-1
) as ParamPath);
syncState.gpn.add(p); syncState.gpn.add(p);
const f = 1 << p.length; const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p)); syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
@@ -1510,7 +1572,10 @@ function generateGetRpcRequest(sessionContext) {
} }
if (!found) { if (!found) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1)); const p = sessionContext.deviceData.paths.add(path.slice(
0,
-1
) as ParamPath);
syncState.gpn.add(p); syncState.gpn.add(p);
const f = 1 << p.length; const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p)); syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
@@ -1519,7 +1584,10 @@ function generateGetRpcRequest(sessionContext) {
syncState.refreshAttributes.object.clear(); syncState.refreshAttributes.object.clear();
for (const path of syncState.refreshAttributes.writable) { for (const path of syncState.refreshAttributes.writable) {
const p = sessionContext.deviceData.paths.add(path.slice(0, -1)); const p = sessionContext.deviceData.paths.add(path.slice(
0,
-1
) as ParamPath);
syncState.gpn.add(p); syncState.gpn.add(p);
const f = 1 << p.length; const f = 1 << p.length;
syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p)); syncState.gpnPatterns.set(p, f | syncState.gpnPatterns.get(p));
@@ -1613,7 +1681,7 @@ function generateGetRpcRequest(sessionContext) {
return null; return null;
} }
function generateSetRpcRequest(sessionContext) { function generateSetRpcRequest(sessionContext): SetAcsRequest {
const syncState = sessionContext.syncState; const syncState = sessionContext.syncState;
if (!syncState) return null; if (!syncState) return null;
@@ -1653,19 +1721,23 @@ function generateSetRpcRequest(sessionContext) {
sessionContext.configContext sessionContext.configContext
); );
const DATETIME_MILLISECONDS = localCache.getConfig( const DATETIME_MILLISECONDS = !!localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.datetimeMilliseconds", "cwmp.datetimeMilliseconds",
sessionContext.configContext sessionContext.configContext
); );
const BOOLEAN_LITERAL = localCache.getConfig( const BOOLEAN_LITERAL = !!localCache.getConfig(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.booleanLiteral", "cwmp.booleanLiteral",
sessionContext.configContext sessionContext.configContext
); );
const parameterValues = []; const parameterValues = [] as [
ParamPath,
string | number | boolean,
string
][];
for (const [k, v] of syncState.spv) { for (const [k, v] of syncState.spv) {
syncState.spv.delete(k); syncState.spv.delete(k);
const attrs = sessionContext.deviceData.attributes.get(k); const attrs = sessionContext.deviceData.attributes.get(k);
@@ -1693,7 +1765,14 @@ function generateSetRpcRequest(sessionContext) {
if (parameterValues.length) { if (parameterValues.length) {
return { return {
name: "SetParameterValues", name: "SetParameterValues",
parameterList: parameterValues.map(p => [p[0].join("."), p[1], p[2]]), parameterList: parameterValues.map(
p =>
[p[0].join("."), p[1], p[2]] as [
string,
string | number | boolean,
string
]
),
DATETIME_MILLISECONDS: DATETIME_MILLISECONDS, DATETIME_MILLISECONDS: DATETIME_MILLISECONDS,
BOOLEAN_LITERAL: BOOLEAN_LITERAL BOOLEAN_LITERAL: BOOLEAN_LITERAL
}; };
@@ -1764,7 +1843,13 @@ function generateSetRpcRequest(sessionContext) {
function generateGetVirtualParameterProvisions( function generateGetVirtualParameterProvisions(
sessionContext, sessionContext,
virtualParameterDeclarations virtualParameterDeclarations
) { ): [
ParamPath,
AttributeTimestamps,
AttributeValues,
AttributeTimestamps,
AttributeValues
][] {
let provisions; let provisions;
if (virtualParameterDeclarations) { if (virtualParameterDeclarations) {
for (const declaration of virtualParameterDeclarations) { for (const declaration of virtualParameterDeclarations) {
@@ -1804,7 +1889,13 @@ function generateGetVirtualParameterProvisions(
function generateSetVirtualParameterProvisions( function generateSetVirtualParameterProvisions(
sessionContext, sessionContext,
virtualParameterDeclarations virtualParameterDeclarations
) { ): [
ParamPath,
AttributeTimestamps,
AttributeValues,
AttributeTimestamps,
AttributeValues
][] {
let provisions; let provisions;
if (virtualParameterDeclarations) { if (virtualParameterDeclarations) {
for (const declaration of virtualParameterDeclarations) { for (const declaration of virtualParameterDeclarations) {
@@ -1850,21 +1941,27 @@ function generateSetVirtualParameterProvisions(
function processDeclarations( function processDeclarations(
sessionContext, sessionContext,
allDeclareTimestamps, allDeclareTimestamps,
allDeclareAttributeTimestamps, allDeclareAttributeTimestamps: Map<ParamPath, AttributeTimestamps>,
allDeclareAttributeValues allDeclareAttributeValues: Map<ParamPath, AttributeValues>
) { ): VirtualParameterDeclaration[] {
const deviceData = sessionContext.deviceData; const deviceData = sessionContext.deviceData;
const syncState = sessionContext.syncState; const syncState = sessionContext.syncState;
const root = sessionContext.deviceData.paths.add([]); const root = sessionContext.deviceData.paths.add([]);
const paths = deviceData.paths.find([], false, true, 99); const paths = deviceData.paths.find([], false, true, 99);
paths.sort((a, b) => paths.sort(
a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard (a, b): number =>
a.wildcard === b.wildcard ? a.length - b.length : a.wildcard - b.wildcard
); );
const virtualParameterDeclarations = []; const virtualParameterDeclarations = [] as VirtualParameterDeclaration[];
function func(leafParam, leafIsObject, leafTimestamp, _paths) { function func(
leafParam: ParamPath,
leafIsObject: number,
leafTimestamp: number,
_paths: ParamPath[]
): void {
const currentPath = _paths[0]; const currentPath = _paths[0];
const children = new Map(); const children = new Map();
let declareTimestamp = 0; let declareTimestamp = 0;
@@ -2122,7 +2219,11 @@ function processDeclarations(
return virtualParameterDeclarations; return virtualParameterDeclarations;
} }
function loadPath(sessionContext, path, depth) { function loadPath(
sessionContext: SessionContext,
path: ParamPath,
depth?: number
): boolean {
depth = depth || (1 << path.length) - 1; depth = depth || (1 << path.length) - 1;
if (sessionContext.new || !depth) return true; if (sessionContext.new || !depth) return true;
@@ -2132,12 +2233,13 @@ function loadPath(sessionContext, path, depth) {
let trimWildcard = path.length; let trimWildcard = path.length;
while (trimWildcard && path[trimWildcard - 1] === "*") --trimWildcard; while (trimWildcard && path[trimWildcard - 1] === "*") --trimWildcard;
if (trimWildcard < path.length) path = path.slice(0, trimWildcard); if (trimWildcard < path.length)
path = path.slice(0, trimWildcard) as ParamPath;
for (let i = 0; i <= path.length; ++i) { for (let i = 0; i <= path.length; ++i) {
const d = i === path.length ? 99 : i; const d = i === path.length ? 99 : i;
for (const sup of sessionContext.deviceData.paths.find( for (const sup of sessionContext.deviceData.paths.find(
path.slice(0, i), path.slice(0, i) as ParamPath,
true, true,
false, false,
d d
@@ -2165,7 +2267,7 @@ function processInstances(
minInstances, minInstances,
maxInstances, maxInstances,
defer defer
) { ): void {
let instancesToCreate, instancesToDelete; let instancesToCreate, instancesToDelete;
if (parent[0] === "Downloads") { if (parent[0] === "Downloads") {
if (parent.length !== 1) return; if (parent.length !== 1) return;
@@ -2217,10 +2319,10 @@ function processInstances(
} }
} }
function clear(sessionContext, toClear, callback) { function clear(sessionContext, toClear, callback): void {
if (!toClear || !toClear.length) return void callback(); if (!toClear || !toClear.length) return void callback();
const MAX_DEPTH = config.get("MAX_DEPTH", sessionContext.deviceId); const MAX_DEPTH = +config.get("MAX_DEPTH", sessionContext.deviceId);
for (const c of toClear) { for (const c of toClear) {
if (c[1]) { if (c[1]) {
@@ -2250,7 +2352,7 @@ function clear(sessionContext, toClear, callback) {
}); });
} }
export function rpcResponse(sessionContext, id, rpcRes, callback) { export function rpcResponse(sessionContext, id, rpcRes, callback): void {
if (id !== generateRpcId(sessionContext)) if (id !== generateRpcId(sessionContext))
return void callback(new Error("Request ID not recognized")); return void callback(new Error("Request ID not recognized"));
@@ -2398,7 +2500,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback) {
if (rpcReq.nextLevel) { if (rpcReq.nextLevel) {
loadPath(sessionContext, root, (1 << (root.length + 1)) - 1); loadPath(sessionContext, root, (1 << (root.length + 1)) - 1);
} else { } else {
const MAX_DEPTH = config.get("MAX_DEPTH", sessionContext.deviceId); const MAX_DEPTH = +config.get("MAX_DEPTH", sessionContext.deviceId);
loadPath(sessionContext, root, (1 << MAX_DEPTH) - 1); loadPath(sessionContext, root, (1 << MAX_DEPTH) - 1);
} }
@@ -2495,7 +2597,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback) {
case "DownloadResponse": case "DownloadResponse":
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", rpcReq.instance, "Download"], ["Downloads", rpcReq.instance, "Download"] as ParamPath,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] }, { value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear toClear
@@ -2504,7 +2606,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback) {
if (rpcRes.status === 0) { if (rpcRes.status === 0) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", rpcReq.instance, "LastDownload"], ["Downloads", rpcReq.instance, "LastDownload"] as ParamPath,
timestamp + 1, timestamp + 1,
{ {
value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]]
@@ -2514,7 +2616,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", rpcReq.instance, "LastFileType"], ["Downloads", rpcReq.instance, "LastFileType"] as ParamPath,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear toClear
@@ -2522,7 +2624,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", rpcReq.instance, "LastFileName"], ["Downloads", rpcReq.instance, "LastFileName"] as ParamPath,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear toClear
@@ -2530,7 +2632,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", rpcReq.instance, "LastTargetFileName"], ["Downloads", rpcReq.instance, "LastTargetFileName"] as ParamPath,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] }, { value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear toClear
@@ -2538,7 +2640,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", rpcReq.instance, "StartTime"], ["Downloads", rpcReq.instance, "StartTime"] as ParamPath,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] }, { value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] },
toClear toClear
@@ -2546,7 +2648,7 @@ export function rpcResponse(sessionContext, id, rpcRes, callback) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
["Downloads", rpcReq.instance, "CompleteTime"], ["Downloads", rpcReq.instance, "CompleteTime"] as ParamPath,
timestamp + 1, timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] }, { value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] },
toClear toClear
@@ -2583,7 +2685,12 @@ export function rpcResponse(sessionContext, id, rpcRes, callback) {
clear(sessionContext, toClear, callback); clear(sessionContext, toClear, callback);
} }
export function rpcFault(sessionContext, id, faultResponse, callback) { export function rpcFault(
sessionContext,
id,
faultResponse,
callback: (err: Error, fault?: Fault) => void
): void {
const rpcReq = sessionContext.rpcRequest; const rpcReq = sessionContext.rpcRequest;
delete sessionContext.syncState; delete sessionContext.syncState;
delete sessionContext.rpcRequest; delete sessionContext.rpcRequest;
@@ -2627,7 +2734,7 @@ export function rpcFault(sessionContext, id, faultResponse, callback) {
callback(null, fault); callback(null, fault);
} }
export function deserialize(sessionContextString, callback) { export function deserialize(sessionContextString, callback): void {
const sessionContext = JSON.parse(sessionContextString); const sessionContext = JSON.parse(sessionContextString);
for (const decs of sessionContext.declarations) for (const decs of sessionContext.declarations)
@@ -2654,7 +2761,7 @@ export function deserialize(sessionContextString, callback) {
}); });
} }
export function serialize(sessionContext, callback) { export function serialize(sessionContext, callback): void {
const deviceData = []; const deviceData = [];
for (const path of sessionContext.deviceData.paths.find( for (const path of sessionContext.deviceData.paths.find(
+93 -54
View File
@@ -16,11 +16,21 @@
* 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 libxmljs from "libxmljs"; import { parseXml, Document, Element, ParserOptions } from "libxmljs";
import * as config from "./config"; import * as config from "./config";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
import {
CpeResponse,
CpeGetResponse,
CpeSetResponse,
InformRequest,
AcsResponse,
CpeRequest,
FaultStruct,
SpvFault,
CpeFault
} from "./types";
const SERVER_NAME = `GenieACS/${VERSION}`; const SERVER_NAME = `GenieACS/${VERSION}`;
@@ -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 };
} }
export 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 @@ export 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 @@ export 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 @@ export 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 @@ export 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;
} }
export 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 @@ export 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 @@ export 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);
+305
View File
@@ -0,0 +1,305 @@
import PathSet from "./path-set";
import VersionedMap from "./versioned-map";
import InstanceSet from "./instance-set";
import { IncomingMessage, ServerResponse } from "http";
export interface ParamPath extends Array<any> {
wildcard: number;
alias: number;
}
export type Expression = string | number | boolean | null | any[];
export interface Fault {
code: string;
message: string;
detail?: {
name: string;
message: string;
stack?: string;
};
}
export interface SessionFault extends Fault {
channel: string;
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<ParamPath, number>;
attributes: VersionedMap<ParamPath, Attributes>;
loaded: Map<ParamPath, number>;
trackers: Map<ParamPath, { [name: string]: number }>;
changes: Set<string>;
}
export type VirtualParameterDeclaration = [
ParamPath,
{ 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<ParamPath>;
object: Set<ParamPath>;
writable: Set<ParamPath>;
value: Set<ParamPath>;
};
spv: Map<ParamPath, [string | number | boolean, string]>;
gpn: Set<ParamPath>;
gpnPatterns: Map<ParamPath, number>;
tags: Map<ParamPath, [string | number | boolean, string]>;
virtualParameterDeclarations: VirtualParameterDeclaration[][];
instancesToDelete: Map<ParamPath, Set<ParamPath>>;
instancesToCreate: Map<ParamPath, InstanceSet>;
downloadsToDelete: Set<ParamPath>;
downloadsToCreate: InstanceSet;
downloadsValues: Map<ParamPath, string | number>;
downloadsDownload: Map<ParamPath, number>;
}
export interface SessionContext {
sessionId?: string;
timestamp: number;
deviceId: string;
deviceData: DeviceData;
cwmpVersion: string;
timeout: number;
provisions: any[];
channels: { [channel: string]: number };
virtualParameters: any[];
revisions: number[];
rpcCount: number;
iteration: number;
cycle: number;
extensionsCache: any;
declarations: any[];
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<ParamPath, 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;
}
export interface GetAcsRequest extends AcsRequest {
name: "GetParameterNames" | "GetParameterValues";
objectName?: string;
parameterNames?: string[];
parameterPath?: string;
nextLevel?: boolean;
}
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;
next?: 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: ParamPath;
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 = [
ParamPath,
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;
};
}[];
+15 -9
View File
@@ -1,5 +1,3 @@
"use strict";
import { Z_SYNC_FLUSH } from "zlib"; import { Z_SYNC_FLUSH } from "zlib";
import Koa from "koa"; import Koa from "koa";
import Router from "koa-router"; import Router from "koa-router";
@@ -14,14 +12,21 @@ import Authorizer from "./common/authorizer";
import { parse } from "./common/expression-parser"; import { parse } from "./common/expression-parser";
import * as logger from "./logger"; import * as logger from "./logger";
import * as localCache from "./local-cache"; import * as localCache from "./local-cache";
import { PermissionSet } from "./types";
declare module "koa" {
interface Request {
body: any;
}
}
const koa = new Koa(); const koa = new Koa();
const router = new Router(); const router = new Router();
const JWT_SECRET = config.get("UI_JWT_SECRET"); const JWT_SECRET = "" + config.get("UI_JWT_SECRET");
const JWT_COOKIE = "genieacs-ui-jwt"; const JWT_COOKIE = "genieacs-ui-jwt";
function getPermissionSets(ctx) { function getPermissionSets(ctx): PermissionSet[] {
const allPermissions = localCache.getPermissionsConfig( const allPermissions = localCache.getPermissionsConfig(
ctx.state.configSnapshot ctx.state.configSnapshot
); );
@@ -50,9 +55,9 @@ function getPermissionSets(ctx) {
return permissionSets; return permissionSets;
} }
function authSimple(ctx, username, password) { function authSimple(ctx, username, password): Promise<string[]> {
const authConfig = localCache.getAuthConfig(ctx.state.configSnapshot); const authConfig = localCache.getAuthConfig(ctx.state.configSnapshot);
const user = authConfig.simple.users[username]; const user = authConfig["simple"].users[username];
if (user && user.password === password) { if (user && user.password === password) {
const roles = user.roles.split(",").map(s => s.trim()); const roles = user.roles.split(",").map(s => s.trim());
return Promise.resolve(roles); return Promise.resolve(roles);
@@ -100,10 +105,11 @@ router.post("/login", async ctx => {
const log = { const log = {
message: "Log in", message: "Log in",
context: ctx, context: ctx,
username: username username: username,
method: null
}; };
function success(roles, method) { function success(roles, method): void {
log.method = method; log.method = method;
const token = jwt.sign({ username, roles }, JWT_SECRET); const token = jwt.sign({ username, roles }, JWT_SECRET);
ctx.cookies.set(JWT_COOKIE, token); ctx.cookies.set(JWT_COOKIE, token);
@@ -111,7 +117,7 @@ router.post("/login", async ctx => {
logger.accessInfo(log); logger.accessInfo(log);
} }
function failure() { function failure(): void {
ctx.status = 400; ctx.status = 400;
ctx.body = "Incorrect username or password"; ctx.body = "Incorrect username or password";
log.message += " failed"; log.message += " failed";
@@ -1,9 +1,7 @@
"use strict"; import { exec } from "child_process";
import { ObjectID } from "mongodb";
import * as db from "./db"; import * as db from "./db";
import { del } from "../cache"; import { del } from "../cache";
import { ObjectID } from "mongodb";
import { exec } from "child_process";
import { import {
insertTasks, insertTasks,
watchTask, watchTask,
@@ -11,7 +9,7 @@ import {
deleteDevice deleteDevice
} from "../api-functions"; } from "../api-functions";
function deleteFault(id) { function deleteFault(id): Promise<void> {
const deviceId = id.split(":", 1)[0]; const deviceId = id.split(":", 1)[0];
const channel = id.slice(deviceId.length + 1); const channel = id.slice(deviceId.length + 1);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -30,7 +28,7 @@ function deleteFault(id) {
}); });
} }
export function deleteResource(resource, id) { export function deleteResource(resource, id): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let promise; let promise;
@@ -80,7 +78,11 @@ export function deleteResource(resource, id) {
}); });
} }
export function postTasks(deviceId, tasks, timeout) { export function postTasks(
deviceId,
tasks,
timeout
): Promise<{ connectionRequest: string; tasks: any[] }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
for (const task of tasks) task.device = deviceId; for (const task of tasks) task.device = deviceId;
@@ -139,7 +141,17 @@ export function postTasks(deviceId, tasks, timeout) {
}); });
} }
export function ping(host) { 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) => { return new Promise((resolve, reject) => {
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) return void reject(err); if (err) return void reject(err);
@@ -162,7 +174,7 @@ export function ping(host) {
}); });
} }
export function putResource(resource, id, data) { export function putResource(resource, id, data): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let promise; let promise;
+18 -16
View File
@@ -1,6 +1,4 @@
"use strict"; import * as stream from "stream";
import stream from "stream";
import Router from "koa-router"; import Router from "koa-router";
import * as db from "./db"; import * as db from "./db";
import * as apiFunctions from "./api-functions"; import * as apiFunctions from "./api-functions";
@@ -8,11 +6,12 @@ import { evaluate, and, extractParams } from "../common/expression";
import { parse } from "../common/expression-parser"; import { parse } from "../common/expression-parser";
import * as logger from "../logger"; import * as logger from "../logger";
import { getConfig } from "../local-cache"; import { getConfig } from "../local-cache";
import { QueryOptions, Expression } from "../types";
const router = new Router(); const router = new Router();
export default router; export default router;
function logUnauthorizedWarning(log) { function logUnauthorizedWarning(log): void {
log.message += " not authorized"; log.message += " not authorized";
logger.accessWarn(log); logger.accessWarn(log);
} }
@@ -91,13 +90,14 @@ router.get(`/devices/:id.csv`, async ctx => {
for (const [resource, flags] of Object.entries(resources)) { for (const [resource, flags] of Object.entries(resources)) {
router.head(`/${resource}`, async (ctx, next) => { router.head(`/${resource}`, async (ctx, next) => {
let filter = true; let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter); if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
const log = { const log = {
message: `Count ${resource}`, message: `Count ${resource}`,
context: ctx, context: ctx,
filter: ctx.request.query.filter filter: ctx.request.query.filter,
count: null
}; };
if (!ctx.state.authorizer.hasAccess(resource, 1)) { if (!ctx.state.authorizer.hasAccess(resource, 1)) {
@@ -115,7 +115,7 @@ for (const [resource, flags] of Object.entries(resources)) {
const count = await db.count(resource, filter); const count = await db.count(resource, filter);
ctx.set("X-Total-Count", count); ctx.set("X-Total-Count", `${count}`);
ctx.body = ""; ctx.body = "";
log.count = count; log.count = count;
@@ -123,8 +123,8 @@ for (const [resource, flags] of Object.entries(resources)) {
}); });
router.get(`/${resource}`, async (ctx, next) => { router.get(`/${resource}`, async (ctx, next) => {
const options = {}; const options: QueryOptions = {};
let filter = true; let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter); 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.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip; if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
@@ -174,8 +174,8 @@ for (const [resource, flags] of Object.entries(resources)) {
// CSV download // CSV download
router.get(`/${resource}.csv`, async (ctx, next) => { router.get(`/${resource}.csv`, async (ctx, next) => {
const options = { projection: {} }; const options: QueryOptions = { projection: {} };
let filter = true; let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter); 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.limit) options.limit = +ctx.request.query.limit;
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip; if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
@@ -356,9 +356,9 @@ for (const [resource, flags] of Object.entries(resources)) {
return void (ctx.status = 403); return void (ctx.status = 403);
} }
const err = await apiFunctions.putResource(resource, id, obj); try {
await apiFunctions.putResource(resource, id, obj);
if (err) { } catch (err) {
log.message += " failed"; log.message += " failed";
logger.accessWarn(log); logger.accessWarn(log);
ctx.body = err; ctx.body = err;
@@ -379,7 +379,8 @@ router.put("/files/:id", async (ctx, next) => {
const log = { const log = {
message: `Upload ${resource}`, message: `Upload ${resource}`,
context: ctx, context: ctx,
id: id id: id,
metadata: null
}; };
const authorizer = ctx.state.authorizer; const authorizer = ctx.state.authorizer;
@@ -412,7 +413,8 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
const log = { const log = {
message: "Commit tasks", message: "Commit tasks",
context: ctx, context: ctx,
deviceId: ctx.params.id deviceId: ctx.params.id,
tasks: null
}; };
const authorizer = ctx.state.authorizer; const authorizer = ctx.state.authorizer;
+29 -25
View File
@@ -1,10 +1,9 @@
"use strict";
import { MongoClient, GridFSBucket } from "mongodb"; import { MongoClient, GridFSBucket } from "mongodb";
import * as config from "../config"; import * as config from "../config";
import * as mongodbFunctions from "./mongodb-functions"; import * as mongodbFunctions from "./mongodb-functions";
import * as expression from "../common/expression"; import * as expression from "../common/expression";
import { preProcessPreset } from "./mongodb-functions"; import { preProcessPreset } from "./mongodb-functions";
import { QueryOptions } from "../types";
const CACHE_TTL = 300000; const CACHE_TTL = 300000;
@@ -14,17 +13,17 @@ const RESOURCE_COLLECTION = {
files: "fs.files" files: "fs.files"
}; };
function ensureIndexes(client) { function ensureIndexes(client): void {
client client
.db() .db()
.collection("cache") .collection("cache")
.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }); .createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
} }
function getClient() { function getClient(): Promise<MongoClient> {
if (!_clientPromise) { if (!_clientPromise) {
_clientPromise = new Promise((resolve, reject) => { _clientPromise = new Promise((resolve, reject) => {
const CONNECTION_URL = config.get("MONGODB_CONNECTION_URL"); const CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
MongoClient.connect( MongoClient.connect(
CONNECTION_URL, CONNECTION_URL,
{ useNewUrlParser: true }, { useNewUrlParser: true },
@@ -40,14 +39,14 @@ function getClient() {
return _clientPromise; return _clientPromise;
} }
export function cache(key, valueGetter, ttl) { export function cache<T>(key, valueGetter: () => Promise<T>, ttl): Promise<T> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getClient() getClient()
.then(client => { .then(client => {
const collection = client.db().collection("cache"); const collection = client.db().collection("cache");
collection.findOne({ _id: key }, (err, doc) => { collection.findOne({ _id: key }, (err, doc) => {
if (err) return void reject(err); if (err) return void reject(err);
if (doc != null) return void resolve(JSON.parse(doc.value)); if (doc != null) return void resolve(JSON.parse(doc.value) as T);
valueGetter() valueGetter()
.then(res => { .then(res => {
const expire = Date.now() + (ttl || CACHE_TTL); const expire = Date.now() + (ttl || CACHE_TTL);
@@ -73,7 +72,12 @@ export function cache(key, valueGetter, ttl) {
}); });
} }
export function query(resource, filter, options, callback) { export function query(
resource,
filter,
options?: QueryOptions,
callback?: (doc: any) => void
): Promise<any[]> {
options = options || {}; options = options || {};
let q; let q;
filter = expression.evaluate(filter, null, Date.now()); filter = expression.evaluate(filter, null, Date.now());
@@ -159,7 +163,7 @@ export function query(resource, filter, options, callback) {
}); });
} }
export function count(resource, filter) { export function count(resource, filter): Promise<number> {
let q; let q;
filter = expression.evaluate(filter, null, Date.now()); filter = expression.evaluate(filter, null, Date.now());
@@ -188,7 +192,7 @@ export function count(resource, filter) {
}); });
} }
export function updateDeviceTags(deviceId, tags) { export function updateDeviceTags(deviceId, tags): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const add = []; const add = [];
const pull = []; const pull = [];
@@ -213,7 +217,7 @@ export function updateDeviceTags(deviceId, tags) {
}); });
} }
function putResource(resource, id, object) { function putResource(resource, id, object): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getClient() getClient()
.then(client => { .then(client => {
@@ -229,7 +233,7 @@ function putResource(resource, id, object) {
}); });
} }
function deleteResource(resource, id) { function deleteResource(resource, id): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getClient() getClient()
.then(client => { .then(client => {
@@ -245,40 +249,40 @@ function deleteResource(resource, id) {
}); });
} }
export function putPreset(id, object) { export function putPreset(id, object): Promise<void> {
object = preProcessPreset(object); object = preProcessPreset(object);
return putResource("presets", id, object); return putResource("presets", id, object);
} }
export function deletePreset(id) { export function deletePreset(id): Promise<void> {
return deleteResource("presets", id); return deleteResource("presets", id);
} }
export function putProvision(id, object) { export function putProvision(id, object): Promise<void> {
return putResource("provisions", id, object); return putResource("provisions", id, object);
} }
export function deleteProvision(id) { export function deleteProvision(id): Promise<void> {
return deleteResource("provisions", id); return deleteResource("provisions", id);
} }
export function putVirtualParameter(id, object) { export function putVirtualParameter(id, object): Promise<void> {
return putResource("virtualParameters", id, object); return putResource("virtualParameters", id, object);
} }
export function deleteVirtualParameter(id) { export function deleteVirtualParameter(id): Promise<void> {
return deleteResource("virtualParameters", id); return deleteResource("virtualParameters", id);
} }
export function putConfig(id, object) { export function putConfig(id, object): Promise<void> {
return putResource("config", id, object); return putResource("config", id, object);
} }
export function deleteConfig(id) { export function deleteConfig(id): Promise<void> {
return deleteResource("config", id); return deleteResource("config", id);
} }
export function putFile(filename, metadata, contentStream) { export function putFile(filename, metadata, contentStream): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getClient() getClient()
.then(client => { .then(client => {
@@ -295,7 +299,7 @@ export function putFile(filename, metadata, contentStream) {
}); });
} }
export function deleteFile(filename) { export function deleteFile(filename): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getClient() getClient()
.then(client => { .then(client => {
@@ -309,15 +313,15 @@ export function deleteFile(filename) {
}); });
} }
export function deleteFault(id) { export function deleteFault(id): Promise<void> {
return deleteResource("faults", id); return deleteResource("faults", id);
} }
export function deleteTask(id) { export function deleteTask(id): Promise<void> {
return deleteResource("tasks", id); return deleteResource("tasks", id);
} }
export function disconnect() { export function disconnect(): void {
if (_clientPromise) { if (_clientPromise) {
_clientPromise.then(client => { _clientPromise.then(client => {
client.close(); client.close();
@@ -1,11 +1,11 @@
"use strict";
import { ObjectID } from "mongodb"; import { ObjectID } from "mongodb";
import { map, parse, stringify } from "../common/expression-parser"; import { map, parse, stringify } from "../common/expression-parser";
import { likePatternToRegExp, evaluate } from "../common/expression"; import { likePatternToRegExp, evaluate } from "../common/expression";
import { Expression, Fault, Task } from "../types";
const isArray = Array.isArray; const isArray = Array.isArray;
export function processDeviceFilter(filter) { export function processDeviceFilter(filter): Expression {
return map(filter, exp => { return map(filter, exp => {
if (!isArray(exp)) return exp; if (!isArray(exp)) return exp;
@@ -46,7 +46,7 @@ export function processDeviceFilter(filter) {
}); });
if (typeof e[2] === "number") { if (typeof e[2] === "number") {
const alt = e.slice(); const alt = (e as any[]).slice();
alt[2] = new Date(e[2]); alt[2] = new Date(e[2]);
e = ["OR", e, alt]; e = ["OR", e, alt];
} }
@@ -56,7 +56,7 @@ export function processDeviceFilter(filter) {
}); });
} }
export function processTasksFilter(filter) { export function processTasksFilter(filter): Expression {
return map(filter, exp => { return map(filter, exp => {
if (!isArray(exp)) return exp; if (!isArray(exp)) return exp;
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
@@ -72,7 +72,7 @@ export function processTasksFilter(filter) {
}); });
} }
export function processFaultsFilter(filter) { export function processFaultsFilter(filter): Expression {
return map(filter, exp => { return map(filter, exp => {
if (!isArray(exp)) return exp; if (!isArray(exp)) return exp;
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
@@ -86,7 +86,7 @@ export function processFaultsFilter(filter) {
}); });
} }
export function filterToMongoQuery(exp) { export function filterToMongoQuery(exp: Expression): {} {
const ops = { const ops = {
OR: 0, OR: 0,
AND: 0, AND: 0,
@@ -103,7 +103,7 @@ export function filterToMongoQuery(exp) {
"IS NOT NULL": 1 "IS NOT NULL": 1
}; };
function recursive(filter, negate, res = {}) { function recursive(filter, negate, res = {}): {} {
const op = filter[0]; const op = filter[0];
if (ops[op] === 0) { if (ops[op] === 0) {
@@ -219,7 +219,7 @@ export function filterToMongoQuery(exp) {
return recursive(_exp, false); return recursive(_exp, false);
} }
export function processDeviceProjection(projection) { export function processDeviceProjection(projection: {}): {} {
if (!projection) return projection; if (!projection) return projection;
const p = {}; const p = {};
for (const [k, v] of Object.entries(projection)) { for (const [k, v] of Object.entries(projection)) {
@@ -245,7 +245,7 @@ export function processDeviceProjection(projection) {
return p; return p;
} }
export function processDeviceSort(sort) { export function processDeviceSort(sort: {}): {} {
if (!sort) return sort; if (!sort) return sort;
const s = {}; const s = {};
for (const [k, v] of Object.entries(sort)) { for (const [k, v] of Object.entries(sort)) {
@@ -261,13 +261,24 @@ export function processDeviceSort(sort) {
return s; return s;
} }
function parseDate(d) { function parseDate(d): number | string {
const n = +d; const n = +d;
return isNaN(n) ? "" + d : n; return isNaN(n) ? "" + d : n;
} }
export function flattenDevice(device) { interface FlatDevice {
function recursive(input, root, output, timestamp) { [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)) { for (const [name, tree] of Object.entries(input)) {
if (!root) { if (!root) {
if (name === "_lastInform") { if (name === "_lastInform") {
@@ -356,7 +367,7 @@ export function flattenDevice(device) {
objectTimestamp: timestamp objectTimestamp: timestamp
}; };
for (const t of tree) { for (const t of tree as string[]) {
output[`Tags.${t}`] = { output[`Tags.${t}`] = {
value: [true, "xsd:boolean"], value: [true, "xsd:boolean"],
valueTimestamp: timestamp, valueTimestamp: timestamp,
@@ -410,14 +421,14 @@ export function flattenDevice(device) {
return newDevice; return newDevice;
} }
export function flattenFault(fault) { export function flattenFault(fault): Fault {
const f = Object.assign({}, fault); const f = Object.assign({}, fault);
if (f.timestamp) f.timestamp = +f.timestamp; if (f.timestamp) f.timestamp = +f.timestamp;
if (f.expiry) f.expiry = +f.expiry; if (f.expiry) f.expiry = +f.expiry;
return f; return f;
} }
export function flattenTask(task) { export function flattenTask(task): Task {
const t = Object.assign({}, task); const t = Object.assign({}, task);
t._id = "" + t._id; t._id = "" + t._id;
if (t.timestamp) t.timestamp = +t.timestamp; if (t.timestamp) t.timestamp = +t.timestamp;
@@ -425,9 +436,9 @@ export function flattenTask(task) {
return t; return t;
} }
function mongoQueryToFilter(query) { function mongoQueryToFilter(query): Expression {
const expressions = []; const expressions = [];
function recursive(_query) { function recursive(_query): void {
for (const [k, v] of Object.entries(_query)) { for (const [k, v] of Object.entries(_query)) {
if (k[0] === "$") { if (k[0] === "$") {
if (k === "$and") for (const vv of Object.values(v)) recursive(vv); if (k === "$and") for (const vv of Object.values(v)) recursive(vv);
@@ -492,7 +503,7 @@ function mongoQueryToFilter(query) {
return expressions; return expressions;
} }
export function flattenPreset(preset) { export function flattenPreset(preset): {} {
const p = Object.assign({}, preset); const p = Object.assign({}, preset);
if (p.precondition) { if (p.precondition) {
p.precondition = mongoQueryToFilter(JSON.parse(p.precondition)); p.precondition = mongoQueryToFilter(JSON.parse(p.precondition));
@@ -521,7 +532,7 @@ export function flattenPreset(preset) {
return p; return p;
} }
export function flattenFile(file) { export function flattenFile(file): {} {
const f = {}; const f = {};
f["_id"] = file["_id"]; f["_id"] = file["_id"];
if (file.metadata) { if (file.metadata) {
@@ -533,7 +544,7 @@ export function flattenFile(file) {
return f; return f;
} }
export function preProcessPreset(data) { export function preProcessPreset(data): {} {
const preset = Object.assign({}, data); const preset = Object.assign({}, data);
preset.precondition = preset.precondition preset.precondition = preset.precondition
? filterToMongoQuery(parse(preset.precondition)) ? filterToMongoQuery(parse(preset.precondition))
@@ -561,7 +572,8 @@ export function preProcessPreset(data) {
const configuration = { const configuration = {
type: "provision", type: "provision",
name: preset.provision name: preset.provision,
args: null
}; };
if (preset.provisionArgs) if (preset.provisionArgs)
+35 -26
View File
@@ -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;
export default 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 @@ export default 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 @@ export default 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 @@ export default 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 @@ export default 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 maxRev = Object.keys(revisionsObj).reduce( const maxRev = Object.keys(revisionsObj).reduce(
(acc, cur) => (+cur > acc ? +cur : acc), (acc, cur) => (+cur > acc ? +cur : acc),
@@ -134,7 +143,7 @@ export default class VersionedMap {
const revisions = []; const revisions = [];
let prev = NONEXISTENT; let prev: V | symbol = NONEXISTENT;
for (let i = 0; i <= maxRev; ++i) { for (let i = 0; i <= maxRev; ++i) {
let v = prev; let v = prev;
if (del & (1 << i)) v = NONEXISTENT; if (del & (1 << i)) v = NONEXISTENT;
@@ -147,28 +156,28 @@ export default 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];
+9507 -271
View File
File diff suppressed because it is too large. Load diff
+39 -20
View File
@@ -16,39 +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.3.0", "jsonwebtoken": "^8.4.0",
"koa": "^2.7.0",
"koa-bodyparser": "^4.2.1", "koa-bodyparser": "^4.2.1",
"koa-compress": "^3.0.0", "koa-compress": "^3.0.0",
"koa-jwt": "^3.3.2", "koa-jwt": "^3.5.1",
"koa-router": "^7.4.0", "koa-router": "^7.4.0",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"koa": "^2.5.2",
"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", "parsimmon": "^1.12.0",
"seedrandom": "^2.4.4" "seedrandom": "^2.4.4"
}, },
"devDependencies": { "devDependencies": {
"ava": "^1.1.0", "@types/jsonwebtoken": "^8.3.0",
"babel-eslint": "^8.2.6", "@types/koa": "^2.0.48",
"codemirror": "^5.40.0", "@types/koa-compress": "^2.0.8",
"cssnano": "^4.0.5", "@types/koa-router": "^7.0.39",
"eslint": "^5.10.0", "@types/koa-static": "^4.0.1",
"esm": "^3.1.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", "mithril": "^1.1.6",
"postcss-cssnext": "^3.1.0", "postcss-cssnext": "^3.1.0",
"postcss-import": "^11.1.0", "postcss-import": "^12.0.1",
"prettier": "^1.15.3", "prettier": "^1.16.4",
"rollup": "^1.1.2", "rollup": "^1.2.1",
"rollup-plugin-json": "^3.1.0", "rollup-plugin-json": "^3.1.0",
"rollup-plugin-replace": "^2.1.0", "rollup-plugin-replace": "^2.1.0",
"rollup-plugin-terser": "^4.0.2", "rollup-plugin-terser": "^4.0.4",
"webpack": "^4.16.3", "rollup-plugin-typescript": "^1.0.0",
"yaml": "^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"
@@ -57,8 +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": "ava -r esm --verbose --color", "test": "ava --verbose --color",
"lint": "eslint --fix lib ui build test bin/* tools/*; prettier --parser babylon --write \"lib/**/*.js\" \"ui/**/*.js\" \"build/**/*.js\" \"test/**/*.js\" bin/* tools/*", "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\"' _ {} \\;",
"build": "node build/build" "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"
} }
} }
+19 -11
View File
@@ -1,7 +1,6 @@
"use strict";
import ava from "ava"; import ava from "ava";
import * as common from "../lib/common.js"; import * as common from "../lib/common";
import { ParamPath } from "../lib/types";
ava("typeOf", t => { ava("typeOf", t => {
const cases = [ const cases = [
@@ -65,24 +64,24 @@ ava("generateDeviceId", t => {
}); });
ava("parsePath", t => { ava("parsePath", t => {
const case1 = ["Seg1.Seg2.Seg3", ["Seg1", "Seg2", "Seg3"]]; const case1 = ["Seg1.Seg2.Seg3", ["Seg1", "Seg2", "Seg3"]] as ParamPath;
case1[1].alias = 0; case1[1].alias = 0;
case1[1].wildcard = 0; case1[1].wildcard = 0;
const case2 = ["Seg1.*.*.Seg4", ["Seg1", "*", "*", "Seg4"]]; const case2 = ["Seg1.*.*.Seg4", ["Seg1", "*", "*", "Seg4"]] as ParamPath;
case2[1].alias = 0; case2[1].alias = 0;
case2[1].wildcard = 6; case2[1].wildcard = 6;
const case3 = [ const case3 = [
"Seg1.Seg2.[Seg6:Seg9].Seg3", "Seg1.Seg2.[Seg6:Seg9].Seg3",
["Seg1", "Seg2", [["Seg6"], "Seg9"], "Seg3"] ["Seg1", "Seg2", [["Seg6"], "Seg9"], "Seg3"]
]; ] as ParamPath;
case3[1].alias = 4; case3[1].alias = 4;
case3[1].wildcard = 0; case3[1].wildcard = 0;
case3[1][2][0].alias = 0; case3[1][2][0].alias = 0;
case3[1][2][0].wildcard = 0; case3[1][2][0].wildcard = 0;
const case4 = ["Seg1.[*:SegAny]", ["Seg1", [["*"], "SegAny"]]]; const case4 = ["Seg1.[*:SegAny]", ["Seg1", [["*"], "SegAny"]]] as ParamPath;
case4[1].alias = 2; case4[1].alias = 2;
case4[1].wildcard = 0; case4[1].wildcard = 0;
case4[1][1][0].alias = 0; case4[1][1][0].alias = 0;
@@ -93,24 +92,33 @@ ava("parsePath", t => {
}); });
ava("addPathMeta", t => { ava("addPathMeta", t => {
const case1 = [["Seg1", "Seg2", "Seg3"], ["Seg1", "Seg2", "Seg3"]]; const case1 = [
["Seg1", "Seg2", "Seg3"],
["Seg1", "Seg2", "Seg3"]
] as ParamPath;
case1[1].alias = 0; case1[1].alias = 0;
case1[1].wildcard = 0; case1[1].wildcard = 0;
const case2 = [["Seg1", "*", "*", "Seg4"], ["Seg1", "*", "*", "Seg4"]]; const case2 = [
["Seg1", "*", "*", "Seg4"],
["Seg1", "*", "*", "Seg4"]
] as ParamPath;
case2[1].alias = 0; case2[1].alias = 0;
case2[1].wildcard = 6; case2[1].wildcard = 6;
const case3 = [ const case3 = [
["Seg1", "Seg2", [["Seg6"], "Seg9"], "Seg3"], ["Seg1", "Seg2", [["Seg6"], "Seg9"], "Seg3"],
["Seg1", "Seg2", [["Seg6"], "Seg9"], "Seg3"] ["Seg1", "Seg2", [["Seg6"], "Seg9"], "Seg3"]
]; ] as ParamPath;
case3[1].alias = 4; case3[1].alias = 4;
case3[1].wildcard = 0; case3[1].wildcard = 0;
case3[1][2][0].alias = 0; case3[1][2][0].alias = 0;
case3[1][2][0].wildcard = 0; case3[1][2][0].wildcard = 0;
const case4 = [["Seg1", [["*"], "SegAny"]], ["Seg1", [["*"], "SegAny"]]]; const case4 = [
["Seg1", [["*"], "SegAny"]],
["Seg1", [["*"], "SegAny"]]
] as ParamPath;
case4[1].alias = 2; case4[1].alias = 2;
case4[1].wildcard = 0; case4[1].wildcard = 0;
case4[1][1][0].alias = 0; case4[1][1][0].alias = 0;
+5 -8
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env -S node -r esm #!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
"use strict";
import { MongoClient } from "mongodb"; import { MongoClient } from "mongodb";
import { get } from "../lib/config"; import { get } from "../lib/config";
@@ -160,11 +158,10 @@ const configData = {
"ui.pageSize": "10" "ui.pageSize": "10"
}; };
async function run() { async function run(): Promise<void> {
const client = await MongoClient.connect( const client = await MongoClient.connect(get("MONGODB_CONNECTION_URL"), {
get("MONGODB_CONNECTION_URL"), useNewUrlParser: true
{ useNewUrlParser: true } });
);
const collection = client.db().collection("config"); const collection = client.db().collection("config");
await collection.deleteMany({}); await collection.deleteMany({});
const objs = Object.entries(configData).map(c => ({ const objs = Object.entries(configData).map(c => ({
+4 -5
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S node -r esm #!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
/** /**
* Copyright 2013-2017 Zaid Abdulla * Copyright 2013-2017 Zaid Abdulla
* *
@@ -17,10 +17,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 fs from "fs"; import * as fs from "fs";
import path from "path"; import * as path from "path";
import { connect, devicesCollection } from "../lib/db"; 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`);
@@ -28,7 +27,7 @@ process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
const dataModel = {}; const dataModel = {};
function recursive(object, p) { function recursive(object, p): void {
if (!p) p = ""; if (!p) p = "";
for (const key in object) { for (const key in object) {
+24
View File
@@ -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"
]
}
-4
View File
@@ -2,9 +2,5 @@
"env": { "env": {
"browser": true, "browser": true,
"node": false "node": false
},
"parser": "babel-eslint",
"parserOptions": {
"allowImportExportEverywhere": true
} }
} }
+6 -5
View File
@@ -1,11 +1,10 @@
"use strict";
import m from "mithril"; import m from "mithril";
import { ClosureComponent, Component } from "mithril";
export default function menu() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const active = { [vnode.attrs.page]: "active" }; const active = { [vnode.attrs["page"]]: "active" };
const tabs = []; const tabs = [];
if (window.authorizer.hasAccess("presets", 1)) { if (window.authorizer.hasAccess("presets", 1)) {
@@ -61,4 +60,6 @@ export default function menu() {
return m("nav#side-menu", m("ul", tabs)); return m("nav#side-menu", m("ul", tabs));
} }
}; };
} };
export default component;
+20 -11
View File
@@ -1,7 +1,4 @@
"use strict"; import m, { RouteResolver } from "mithril";
import m from "mithril";
import layout from "./layout"; import layout from "./layout";
import * as store from "./store"; import * as store from "./store";
import * as loginPage from "./login-page"; import * as loginPage from "./login-page";
@@ -19,6 +16,15 @@ import Authorizer from "../lib//common/authorizer";
import * as notifications from "./notifications"; import * as notifications from "./notifications";
import { contextifyComponent } from "./components"; import { contextifyComponent } from "./components";
declare global {
interface Window {
authorizer: Authorizer;
permissionSets: any;
username: string;
clientConfig: any;
}
}
window.authorizer = new Authorizer(window.permissionSets); window.authorizer = new Authorizer(window.permissionSets);
const adminPages = [ const adminPages = [
@@ -35,7 +41,7 @@ let lastRenderTimestamp = 0;
let pageVisitTimestamp = 0; let pageVisitTimestamp = 0;
let fulfillTimeout = null; let fulfillTimeout = null;
function fulfill() { function fulfill(): void {
clearTimeout(fulfillTimeout); clearTimeout(fulfillTimeout);
fulfillTimeout = setTimeout(() => { fulfillTimeout = setTimeout(() => {
store.fulfill(lastRenderTimestamp, pageVisitTimestamp).then(updated => { store.fulfill(lastRenderTimestamp, pageVisitTimestamp).then(updated => {
@@ -44,16 +50,19 @@ function fulfill() {
}, 100); }, 100);
} }
function pagify(pageName, page) { function pagify(pageName, page): RouteResolver {
const component = { const component: RouteResolver = {
render: () => { render: () => {
lastRenderTimestamp = Date.now(); lastRenderTimestamp = Date.now();
let p; let p;
if (state && state.error) p = m(errorPage.component, state); if (state && state.error) p = m(errorPage.component, state);
else p = m(contextifyComponent(page.component), state); else p = m(contextifyComponent(page.component), state);
fulfill(); fulfill();
return m(layout, { page: pageName }, p); const attrs = {};
} attrs["page"] = pageName;
return m(layout, attrs, p);
},
onmatch: null
}; };
component.onmatch = (args, requestedPath) => { component.onmatch = (args, requestedPath) => {
@@ -87,8 +96,8 @@ function pagify(pageName, page) {
return component; return component;
} }
function redirectAdminPage() { function redirectAdminPage(): RouteResolver {
const component = { const component: RouteResolver = {
onmatch: () => { onmatch: () => {
for (const page of adminPages) { for (const page of adminPages) {
if (window.authorizer.hasAccess(page, 2)) { if (window.authorizer.hasAccess(page, 2)) {
@@ -1,7 +1,16 @@
"use strict"; 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;
class Autocomplete { public constructor(className, callback) {
constructor(className, callback) {
this.callback = callback; this.callback = callback;
this.element = null; this.element = null;
this.hideTimeout = null; this.hideTimeout = null;
@@ -12,11 +21,11 @@ class Autocomplete {
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.style.position = "absolute"; this.container.style.position = "absolute";
this.container.style.display = "block"; this.container.style.display = "block";
this.container.style.opacity = 0; this.container.style.opacity = "0";
this.container.className = className; this.container.className = className;
} }
attach(el) { public attach(el): void {
el.setAttribute("autocomplete", "off"); el.setAttribute("autocomplete", "off");
el.addEventListener("focus", () => { el.addEventListener("focus", () => {
@@ -63,8 +72,8 @@ class Autocomplete {
}); });
} }
hide() { private hide(): void {
this.container.style.opacity = 0; this.container.style.opacity = "0";
this.visible = false; this.visible = false;
this.default = null; this.default = null;
this.selection = null; this.selection = null;
@@ -77,7 +86,7 @@ class Autocomplete {
}, 500); }, 500);
} }
update() { private update(): void {
const el = this.element; const el = this.element;
this.callback(el.value, suggestions => { this.callback(el.value, suggestions => {
@@ -101,7 +110,7 @@ class Autocomplete {
clearTimeout(this.hideTimeout); clearTimeout(this.hideTimeout);
this.hideTimeout = null; this.hideTimeout = null;
} }
this.container.style.opacity = 1; this.container.style.opacity = "1";
this.visible = true; this.visible = true;
} }
@@ -128,7 +137,7 @@ class Autocomplete {
e.addEventListener("mousedown", ev => { e.addEventListener("mousedown", ev => {
ev.preventDefault(); ev.preventDefault();
el.value = suggestion; el.value = suggestion;
if (this.element === el) this.update(el); if (this.element === el) this.update();
}); });
this.container.appendChild(e); this.container.appendChild(e);
} }
@@ -150,5 +159,3 @@ class Autocomplete {
}); });
} }
} }
export default Autocomplete;
+50 -18
View File
@@ -1,6 +1,12 @@
"use strict"; import m, {
Static,
import m from "mithril"; Attributes,
Children,
ComponentTypes,
Lifecycle,
ClosureComponent,
Vnode
} from "mithril";
import parameter from "./components/parameter"; import parameter from "./components/parameter";
import parameterList from "./components/parameter-list"; import parameterList from "./components/parameter-list";
import parameterTable from "./components/parameter-table"; import parameterTable from "./components/parameter-table";
@@ -31,13 +37,33 @@ const comps = {
"long-text": longTextComponent "long-text": longTextComponent
}; };
const contextifiedComponents = new WeakMap(); const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
const vnodeContext = new WeakMap(); const vnodeContext = new WeakMap<Vnode, Attributes>();
function contextFn(context, ...argumentsList) { interface MC extends Static {
const vnode = Reflect.apply(M, undefined, argumentsList); context: {
vnodeContext.set(vnode, context); (ctx: Attributes, selector: string, ...children: Children[]): Vnode<
return 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, { const M = new Proxy(m, {
@@ -52,9 +78,15 @@ const M = new Proxy(m, {
if (prop === "context") return contextFn; if (prop === "context") return contextFn;
else return Reflect.get(target, prop); else return Reflect.get(target, prop);
} }
}); }) as MC;
function applyContext(vnode, parentContext) { 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)) { if (Array.isArray(vnode)) {
for (const c of vnode) applyContext(c, parentContext); for (const c of vnode) applyContext(c, parentContext);
} else if (vnode && typeof vnode === "object" && vnode.tag) { } else if (vnode && typeof vnode === "object" && vnode.tag) {
@@ -66,25 +98,25 @@ function applyContext(vnode, parentContext) {
} }
} }
export function contextifyComponent(component) { export function contextifyComponent(component: ComponentTypes): ComponentTypes {
let c = contextifiedComponents.get(component); let c = contextifiedComponents.get(component);
if (!c) { if (!c) {
if (typeof component !== "function") { if (typeof component !== "function") {
c = Object.assign({}, component); c = Object.assign({}, component);
const view = component.view; const view = component.view;
c.view = vnode => { c.view = function(vnode) {
const context = vnodeContext.get(vnode) || {}; const context = vnodeContext.get(vnode) || {};
const res = view(vnode, M); const res = Reflect.apply(view, this, [vnode]);
applyContext(res, context); applyContext(res, context);
return res; return res;
}; };
} else if (!component.prototype.view) { } else if (!component.prototype || !component.prototype.view) {
c = initialNode => { c = initialNode => {
const state = component(initialNode); const state = (component as ClosureComponent)(initialNode);
const view = state.view; const view = state.view;
state.view = vnode => { state.view = function(vnode) {
const context = vnodeContext.get(vnode) || {}; const context = vnodeContext.get(vnode) || {};
const res = view(vnode, M); const res = Reflect.apply(view, this, [vnode]);
applyContext(res, context); applyContext(res, context);
return res; return res;
}; };
@@ -1,5 +1,4 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as taskQueue from "../task-queue"; import * as taskQueue from "../task-queue";
import { parse } from "../../lib/common/expression-parser"; import { parse } from "../../lib/common/expression-parser";
@@ -7,30 +6,30 @@ import memoize from "../../lib/common/memoize";
const memoizedParse = memoize(parse); const memoizedParse = memoize(parse);
function escapeRegExp(str) { function escapeRegExp(str): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
} }
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const device = vnode.attrs.device; const device = vnode.attrs["device"];
const search = m("input", { const search = m("input", {
type: "text", type: "text",
placeholder: "Search parameters", placeholder: "Search parameters",
oninput: e => { oninput: e => {
vnode.state.searchString = e.target.value; vnode.state["searchString"] = e.target.value;
e.redraw = false; e.redraw = false;
clearTimeout(vnode.state.timeout); clearTimeout(vnode.state["timeout"]);
vnode.state.timeout = setTimeout(m.redraw, 500); vnode.state["timeout"] = setTimeout(m.redraw, 500);
} }
}); });
const instanceRegex = /\.[0-9]+$/; const instanceRegex = /\.[0-9]+$/;
let re; let re;
if (vnode.state.searchString) { if (vnode.state["searchString"]) {
const keywords = vnode.state.searchString.split(" ").filter(s => s); const keywords = vnode.state["searchString"].split(" ").filter(s => s);
if (keywords.length) if (keywords.length)
re = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i"); re = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i");
} }
@@ -43,7 +42,7 @@ export default function component() {
const attrs = {}; const attrs = {};
if (re) { if (re) {
const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k; const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k;
if (!re.test(str)) attrs.style = "display: none;"; if (!re.test(str)) attrs["style"] = "display: none;";
} }
if (p.object === false) { if (p.object === false) {
@@ -132,4 +131,6 @@ export default function component() {
); );
} }
}; };
} };
export default component;
-22
View File
@@ -1,22 +0,0 @@
"use strict";
import { m } from "../components";
import * as store from "../store";
export default function 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;
}
};
}
+28
View File
@@ -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;
@@ -1,14 +1,13 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as taskQueue from "../task-queue"; import * as taskQueue from "../task-queue";
import * as notifications from "../notifications"; import * as notifications from "../notifications";
import * as store from "../store"; import * as store from "../store";
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const device = vnode.attrs.device; const device = vnode.attrs["device"];
const buttons = []; const buttons = [];
@@ -87,4 +86,6 @@ export default function component() {
return m(".actions-bar", buttons); return m(".actions-bar", buttons);
} }
}; };
} };
export default component;
@@ -1,13 +1,12 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as store from "../store"; import * as store from "../store";
import * as notifications from "../notifications"; import * as notifications from "../notifications";
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const device = vnode.attrs.device; const device = vnode.attrs["device"];
const deviceId = device["DeviceID.ID"].value[0]; const deviceId = device["DeviceID.ID"].value[0];
const faults = store.fetch("faults", [ const faults = store.fetch("faults", [
"AND", "AND",
@@ -72,4 +71,6 @@ export default function component() {
return m("table.table", thead, tbody); return m("table.table", thead, tbody);
} }
}; };
} };
export default component;
@@ -1,18 +1,17 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
let deviceId; let deviceId;
if (vnode.attrs.device) if (vnode.attrs["device"])
deviceId = vnode.attrs.device["DeviceID.ID"].value[0]; deviceId = vnode.attrs["device"]["DeviceID.ID"].value[0];
const children = Object.values(vnode.attrs.components).map(c => { const children = Object.values(vnode.attrs["components"]).map(c => {
if (typeof c !== "object") return `${c}`; if (typeof c !== "object") return `${c}`;
const attrs = Object.assign({}, vnode.attrs, c); const attrs = Object.assign({}, vnode.attrs, c);
return m(attrs.type, attrs); return m(attrs["type"], attrs);
}); });
if (deviceId) { if (deviceId) {
return m( return m(
@@ -25,4 +24,6 @@ export default function component() {
} }
} }
}; };
} };
export default component;
@@ -1,18 +1,17 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import config from "../config"; import config from "../config";
import * as store from "../store"; import * as store from "../store";
const CHARTS = config.ui.overview.charts; const CHARTS = config.ui.overview.charts;
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const device = vnode.attrs.device; const device = vnode.attrs["device"];
const chart = CHARTS[vnode.attrs.chart]; const chart = CHARTS[vnode.attrs["chart"]];
for (const slice of Object.values(chart.slices)) { for (const slice of Object.values(chart.slices)) {
const filter = slice.filter; const filter = slice["filter"];
if (store.evaluateExpression(filter, device)) { if (store.evaluateExpression(filter, device)) {
const dot = m( const dot = m(
"svg", "svg",
@@ -26,13 +25,15 @@ export default function component() {
cx: "0.5em", cx: "0.5em",
cy: "0.5em", cy: "0.5em",
r: "0.4em", r: "0.4em",
fill: slice.color fill: slice["color"]
}) })
); );
return m("span.overview-dot", dot, `${slice.label}`); return m("span.overview-dot", dot, `${slice["label"]}`);
} }
} }
return null; return null;
} }
}; };
} };
export default component;
-35
View File
@@ -1,35 +0,0 @@
"use strict";
import { m } from "../components";
export default function 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.style.display = p.dom ? "" : "none";
}
},
m("th", parameter.label),
m("td", p)
);
});
return m("table.parameter-list", rows);
}
};
}
+38
View File
@@ -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;
@@ -1,23 +1,22 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as taskQueue from "../task-queue"; import * as taskQueue from "../task-queue";
import * as store from "../store"; import * as store from "../store";
import * as expressionParser from "../../lib/common/expression-parser"; import * as expressionParser from "../../lib/common/expression-parser";
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
oninit: vnode => { oninit: vnode => {
const obj = vnode.attrs.parameter; const obj = vnode.attrs["parameter"];
if (!Array.isArray(obj) || !obj[0] === "PARAM") if (!Array.isArray(obj) || obj[0] !== "PARAM")
throw new Error("Object must be a parameter path"); throw new Error("Object must be a parameter path");
vnode.state.object = obj[1]; vnode.state["object"] = obj[1];
vnode.state.parameters = Object.values(vnode.attrs.childParameters); vnode.state["parameters"] = Object.values(vnode.attrs["childParameters"]);
}, },
view: vnode => { view: vnode => {
const device = vnode.attrs.device; const device = vnode.attrs["device"];
const object = store.evaluateExpression(vnode.state.object, device); const object = store.evaluateExpression(vnode.state["object"], device);
const parameters = vnode.state.parameters; const parameters = vnode.state["parameters"];
if (!device[object]) return null; if (!device[object]) return null;
@@ -31,13 +30,14 @@ export default function component() {
} }
} }
const headers = Object.values(parameters).map(p => m("th", p.label)); const headers = Object.values(parameters).map(p => m("th", p["label"]));
const thead = m("thead", m("tr", headers)); const thead = m("thead", m("tr", headers));
const rows = []; const rows = [];
for (const i of instances) { for (const i of instances) {
let filter = vnode.attrs.filter != null ? vnode.attrs.filter : true; let filter =
vnode.attrs["filter"] != null ? vnode.attrs["filter"] : true;
filter = expressionParser.map(filter, e => { filter = expressionParser.map(filter, e => {
if (Array.isArray(e) && e[0] === "PARAM") if (Array.isArray(e) && e[0] === "PARAM")
@@ -126,9 +126,11 @@ export default function component() {
} }
let label; let label;
if (vnode.attrs.label) label = m("h2", vnode.attrs.label); if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
return [label, m("table.table", thead, m("tbody", rows))]; return [label, m("table.table", thead, m("tbody", rows))];
} }
}; };
} };
export default component;
@@ -1,5 +1,4 @@
"use strict"; import { ClosureComponent, Component, VnodeDOM } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as taskQueue from "../task-queue"; import * as taskQueue from "../task-queue";
import * as store from "../store"; import * as store from "../store";
@@ -31,14 +30,14 @@ const evaluateParam = memoize((exp, obj, now) => {
return { value, timestamp, parameter }; return { value, timestamp, parameter };
}); });
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const device = vnode.attrs.device; const device = vnode.attrs["device"];
const { value, timestamp, parameter } = evaluateParam( const { value, timestamp, parameter } = evaluateParam(
vnode.attrs.parameter, vnode.attrs["parameter"],
vnode.attrs.device, vnode.attrs["device"],
store.getTimestamp() store.getTimestamp()
); );
@@ -77,7 +76,7 @@ export default function component() {
onmouseover: e => { onmouseover: e => {
e.redraw = false; e.redraw = false;
// Don't update any child element // Don't update any child element
if (e.target === el.dom) { if (e.target === (el as VnodeDOM).dom) {
const now = Date.now(); const now = Date.now();
const localeString = new Date(timestamp).toLocaleString(); const localeString = new Date(timestamp).toLocaleString();
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`; e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
@@ -89,4 +88,6 @@ export default function component() {
); );
} }
}; };
} };
export default component;
-56
View File
@@ -1,56 +0,0 @@
"use strict";
import { m } from "../components";
import * as store from "../store";
const REFRESH_INTERVAL = 3000;
export default function component() {
return {
oninit: vnode => {
vnode.state.timestamp = 0;
vnode.state.ping = null;
vnode.state.host = null;
vnode.state.timeout = null;
},
view: vnode => {
const refresh = () => {
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;
}
};
}
+57
View File
@@ -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;
@@ -1,14 +1,13 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as taskQueue from "../task-queue"; import * as taskQueue from "../task-queue";
import * as store from "../store"; import * as store from "../store";
import * as notifications from "../notifications"; import * as notifications from "../notifications";
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const device = vnode.attrs.device; const device = vnode.attrs["device"];
return m( return m(
"button.primary", "button.primary",
@@ -16,9 +15,9 @@ export default function component() {
title: "Initiate session and refresh basic parameters", title: "Initiate session and refresh basic parameters",
onclick: e => { onclick: e => {
e.target.disabled = true; e.target.disabled = true;
const params = Object.values(vnode.attrs.parameters) const params = Object.values(vnode.attrs["parameters"])
.map(exp => { .map(exp => {
if (!Array.isArray(exp) || !exp[0] === "PARAM") return null; if (!Array.isArray(exp) || exp[0] !== "PARAM") return null;
return store.evaluateExpression(exp[1], device); return store.evaluateExpression(exp[1], device);
}) })
.filter(exp => !!exp); .filter(exp => !!exp);
@@ -68,4 +67,6 @@ export default function component() {
); );
} }
}; };
} };
export default component;
@@ -1,15 +1,14 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as notifications from "../notifications"; import * as notifications from "../notifications";
import * as store from "../store"; import * as store from "../store";
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const device = vnode.attrs.device; const device = vnode.attrs["device"];
let writable = true; let writable = true;
if (vnode.attrs.writable != null) writable = vnode.attrs.writable; if (vnode.attrs["writable"] != null) writable = vnode.attrs["writable"];
const tags = []; const tags = [];
for (const p of Object.keys(device)) for (const p of Object.keys(device))
@@ -87,4 +86,6 @@ export default function component() {
); );
} }
}; };
} };
export default component;
+26 -21
View File
@@ -1,5 +1,3 @@
"use strict";
import { m } from "./components"; import { m } from "./components";
import * as store from "./store"; import * as store from "./store";
import * as notifications from "./notifications"; import * as notifications from "./notifications";
@@ -7,13 +5,18 @@ import putFormComponent from "./put-form-component";
import * as overlay from "./overlay"; import * as overlay from "./overlay";
import * as expressionParser from "../lib/common/expression-parser"; import * as expressionParser from "../lib/common/expression-parser";
import { loadCodeMirror } from "./dynamic-loader"; import { loadCodeMirror } from "./dynamic-loader";
import { ClosureComponent, Component, Children } from "mithril";
const attributes = [ const attributes = [
{ id: "_id", label: "Key" }, { id: "_id", label: "Key" },
{ id: "value", label: "Value" } { id: "value", label: "Value" }
]; ];
function putActionHandler(action, _object, isNew) { interface ValidationErrors {
[prop: string]: string;
}
function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const object = Object.assign({}, _object); const object = Object.assign({}, _object);
if (action === "save") { if (action === "save") {
@@ -78,11 +81,11 @@ const formData = {
attributes: attributes attributes: attributes
}; };
function escapeRegExp(str) { function escapeRegExp(str): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
} }
export function init() { export function init(): Promise<{}> {
if (!window.authorizer.hasAccess("config", 2)) { if (!window.authorizer.hasAccess("config", 2)) {
return Promise.reject( return Promise.reject(
new Error("You are not authorized to view this page") new Error("You are not authorized to view this page")
@@ -97,7 +100,7 @@ export function init() {
}); });
} }
function renderTable(confsResponse, searchString) { function renderTable(confsResponse, searchString): Children {
const confs = confsResponse.value.sort((a, b) => { const confs = confsResponse.value.sort((a, b) => {
return a._id < b._id ? -1 : 1; return a._id < b._id ? -1 : 1;
}); });
@@ -113,14 +116,14 @@ function renderTable(confsResponse, searchString) {
for (const conf of confs) { for (const conf of confs) {
const attrs = {}; const attrs = {};
if (regex && !regex.test(conf._id) && !regex.test(conf.value)) if (regex && !regex.test(conf._id) && !regex.test(conf.value))
attrs.style = "display: none;"; attrs["style"] = "display: none;";
const edit = m( const edit = m(
"button", "button",
{ {
title: "Edit config value", title: "Edit config value",
onclick: () => { onclick: () => {
const cb = () => { const cb = (): Children => {
return m( return m(
putFormComponent, putFormComponent,
Object.assign( Object.assign(
@@ -130,9 +133,9 @@ function renderTable(confsResponse, searchString) {
return new Promise(resolve => { return new Promise(resolve => {
putActionHandler(action, object, false) putActionHandler(action, object, false)
.then(errors => { .then(errors => {
errors = errors ? Object.values(errors) : []; const ErrorList = errors ? Object.values(errors) : [];
if (errors.length) { if (ErrorList.length) {
for (const err of errors) for (const err of ErrorList)
notifications.push("error", err); notifications.push("error", err);
} else { } else {
overlay.close(cb); overlay.close(cb);
@@ -190,7 +193,7 @@ function renderTable(confsResponse, searchString) {
return m("table", m("tbody", rows)); return m("table", m("tbody", rows));
} }
export function component() { export const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
document.title = "Config - GenieACS"; document.title = "Config - GenieACS";
@@ -199,10 +202,10 @@ export function component() {
type: "text", type: "text",
placeholder: "Search config", placeholder: "Search config",
oninput: e => { oninput: e => {
vnode.state.searchString = e.target.value; vnode.state["searchString"] = e.target.value;
e.redraw = false; e.redraw = false;
clearTimeout(vnode.state.timeout); clearTimeout(vnode.state["timeout"]);
vnode.state.timeout = setTimeout(m.redraw, 250); vnode.state["timeout"] = setTimeout(m.redraw, 250);
} }
}); });
@@ -213,7 +216,7 @@ export function component() {
{ {
title: "Create new config", title: "Create new config",
onclick: () => { onclick: () => {
const cb = () => { const cb = (): Children => {
return m( return m(
putFormComponent, putFormComponent,
Object.assign( Object.assign(
@@ -222,9 +225,11 @@ export function component() {
return new Promise(resolve => { return new Promise(resolve => {
putActionHandler(action, object, true) putActionHandler(action, object, true)
.then(errors => { .then(errors => {
errors = errors ? Object.values(errors) : []; const errorList = errors
if (errors.length) { ? Object.values(errors)
for (const err of errors) : [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err); notifications.push("error", err);
} else { } else {
overlay.close(cb); overlay.close(cb);
@@ -259,11 +264,11 @@ export function component() {
m( m(
".parameter-list", ".parameter-list",
{ style: "height: 400px" }, { style: "height: 400px" },
renderTable(confs, vnode.state.searchString) renderTable(confs, vnode.state["searchString"])
), ),
m(".actions-bar", newConfig) m(".actions-bar", newConfig)
) )
]; ];
} }
}; };
} };
-2
View File
@@ -1,3 +1 @@
"use strict";
export default window.clientConfig; export default window.clientConfig;
+7 -8
View File
@@ -1,10 +1,9 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "./components"; import { m } from "./components";
import config from "./config"; import config from "./config";
import * as store from "./store"; import * as store from "./store";
export function init(args) { export function init(args): Promise<{}> {
if (!window.authorizer.hasAccess("devices", 2)) { if (!window.authorizer.hasAccess("devices", 2)) {
return Promise.reject( return Promise.reject(
new Error("You are not authorized to view this page") new Error("You are not authorized to view this page")
@@ -17,12 +16,12 @@ export function init(args) {
}); });
} }
export function component() { export const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
document.title = `${vnode.attrs.deviceId} - Devices - GenieACS`; document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
const dev = store.fetch("devices", vnode.attrs.deviceFilter).value; const dev = store.fetch("devices", vnode.attrs["deviceFilter"]).value;
if (!dev.length) return "Loading"; if (!dev.length) return "Loading";
const conf = config.ui.device; const conf = config.ui.device;
const cmps = []; const cmps = [];
@@ -30,7 +29,7 @@ export function component() {
for (const c of Object.values(conf)) for (const c of Object.values(conf))
cmps.push(m.context({ device: dev[0] }, c["type"], c)); cmps.push(m.context({ device: dev[0] }, c["type"], c));
return [m("h1", vnode.attrs.deviceId), cmps]; return [m("h1", vnode.attrs["deviceId"]), cmps];
} }
}; };
} };
+31 -29
View File
@@ -1,5 +1,4 @@
"use strict"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import config from "./config"; import config from "./config";
import filterComponent from "./filter-component"; import filterComponent from "./filter-component";
@@ -42,7 +41,7 @@ const unpackSmartQuery = memoize(query => {
}); });
}); });
export function init(args) { export function init(args): Promise<{}> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!window.authorizer.hasAccess("devices", 2)) if (!window.authorizer.hasAccess("devices", 2))
return void reject(new Error("You are not authorized to view this page")); return void reject(new Error("You are not authorized to view this page"));
@@ -63,7 +62,7 @@ function renderTable(
downloadUrl, downloadUrl,
sort, sort,
onSortChange onSortChange
) { ): Children {
const devices = devicesResponse.value; const devices = devicesResponse.value;
const selectAll = m("input", { const selectAll = m("input", {
type: "checkbox", type: "checkbox",
@@ -195,7 +194,7 @@ function renderTable(
]); ]);
} }
function renderActions(selected) { function renderActions(selected): Children {
const buttons = []; const buttons = [];
buttons.push( buttons.push(
@@ -336,65 +335,68 @@ function renderActions(selected) {
return m(".actions-bar", buttons); return m(".actions-bar", buttons);
} }
export function component() { export const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
document.title = "Devices - GenieACS"; document.title = "Devices - GenieACS";
function showMore() { function showMore(): void {
vnode.state.showCount = vnode.state["showCount"] =
(vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE;
m.redraw(); m.redraw();
} }
function onFilterChanged(filter) { function onFilterChanged(filter): void {
const ops = { filter }; const ops = { filter };
if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set("/devices", ops); m.route.set("/devices", ops);
} }
function onSortChange(sort) { function onSortChange(sort): void {
const ops = { sort }; const ops = { sort };
if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/devices", ops); m.route.set("/devices", ops);
} }
const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; const sort = vnode.attrs["sort"]
let filter = vnode.attrs.filter ? memoizedJsonParse(vnode.attrs["sort"])
? memoizedParse(vnode.attrs.filter) : {};
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true; : true;
filter = unpackSmartQuery(filter); filter = unpackSmartQuery(filter);
const devs = store.fetch("devices", filter, { const devs = store.fetch("devices", filter, {
limit: vnode.state.showCount || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
sort: sort sort: sort
}); });
const count = store.count("devices", filter); const count = store.count("devices", filter);
const selected = new Set(); const selected = new Set();
if (vnode.state.selected) { if (vnode.state["selected"]) {
for (const d of devs.value) { for (const d of devs.value) {
if (vnode.state.selected.has(d["DeviceID.ID"].value[0])) if (vnode.state["selected"].has(d["DeviceID.ID"].value[0]))
selected.add(d["DeviceID.ID"].value[0]); selected.add(d["DeviceID.ID"].value[0]);
} }
} }
vnode.state.selected = selected; vnode.state["selected"] = selected;
const downloadUrl = getDownloadUrl( const downloadUrl = getDownloadUrl(
vnode.attrs.filter, vnode.attrs["filter"],
vnode.attrs.indexParameters vnode.attrs["indexParameters"]
); );
const attrs = {};
attrs["resource"] = "devices";
attrs["filter"] = vnode.attrs["filter"];
attrs["onChange"] = onFilterChanged;
return [ return [
m("h1", "Listing devices"), m("h1", "Listing devices"),
m(filterComponent, { m(filterComponent, attrs),
resource: "devices",
filter: vnode.attrs.filter,
onChange: onFilterChanged
}),
renderTable( renderTable(
devs, devs,
vnode.attrs.indexParameters, vnode.attrs["indexParameters"],
count.value, count.value,
selected, selected,
showMore, showMore,
@@ -406,4 +408,4 @@ export function component() {
]; ];
} }
}; };
} };
@@ -1,21 +1,24 @@
"use strict"; import m, {
Children,
import m from "mithril"; Child,
ClosureComponent,
Component,
VnodeDOM
} from "mithril";
import * as taskQueue from "./task-queue"; import * as taskQueue from "./task-queue";
import * as store from "./store"; import * as store from "./store";
import * as notifications from "./notifications"; import * as notifications from "./notifications";
function mparam(param) { function mparam(param): Children {
return m("span.parameter", { title: param }, `${param}`); return m("span.parameter", { title: param }, `${param}`);
} }
function mval(val) { function mval(val): Children {
return m("span.value", { title: val }, `${val}`); return m("span.value", { title: val }, `${val}`);
} }
function renderStagingSpv(task, queueFunc, cancelFunc) { function renderStagingSpv(task, queueFunc, cancelFunc): Children {
function keydown(e) { function keydown(e): void {
if (e.keyCode === 13) queueFunc(); if (e.keyCode === 13) queueFunc();
else if (e.keyCode === 27) cancelFunc(); else if (e.keyCode === 27) cancelFunc();
else e.redraw = false; else e.redraw = false;
@@ -33,7 +36,7 @@ function renderStagingSpv(task, queueFunc, cancelFunc) {
}, },
onkeydown: keydown, onkeydown: keydown,
oncreate: vnode => { oncreate: vnode => {
vnode.dom.focus(); (vnode.dom as HTMLSelectElement).focus();
} }
}, },
[ [
@@ -55,11 +58,11 @@ function renderStagingSpv(task, queueFunc, cancelFunc) {
}, },
onkeydown: keydown, onkeydown: keydown,
oncreate: vnode => { oncreate: vnode => {
vnode.dom.focus(); (vnode.dom as HTMLInputElement).focus();
vnode.dom.select(); (vnode.dom as HTMLInputElement).select();
// Need to prevent scrolling on focus because // Need to prevent scrolling on focus because
// we're animating height and using overflow: hidden // we're animating height and using overflow: hidden
vnode.dom.parentNode.parentNode.scrollTop = 0; (vnode.dom.parentNode.parentNode as Element).scrollTop = 0;
} }
}); });
} }
@@ -67,7 +70,7 @@ function renderStagingSpv(task, queueFunc, cancelFunc) {
return [m("span", "Editing ", mparam(task.parameterValues[0][0])), input]; return [m("span", "Editing ", mparam(task.parameterValues[0][0])), input];
} }
function renderStagingDownload(task) { function renderStagingDownload(task): Children {
task.invalid = !task.fileName || !task.fileType; task.invalid = !task.fileName || !task.fileType;
const files = store.fetch("files", true); const files = store.fetch("files", true);
const idParts = task.device.split("-"); const idParts = task.device.split("-");
@@ -138,22 +141,22 @@ function renderStagingDownload(task) {
]; ];
} }
function renderStaging(staging) { function renderStaging(staging): Child[] {
const elements = []; const elements: Child[] = [];
for (const s of staging) { for (const s of staging) {
const queueFunc = () => { const queueFunc = (): void => {
staging.delete(s); staging.delete(s);
taskQueue.queueTask(s); taskQueue.queueTask(s);
}; };
const cancelFunc = () => { const cancelFunc = (): void => {
staging.delete(s); staging.delete(s);
}; };
let elms; let elms;
if (s.name === "setParameterValues") if (s.name === "setParameterValues")
elms = renderStagingSpv(s, queueFunc, cancelFunc); elms = renderStagingSpv(s, queueFunc, cancelFunc);
else if (s.name === "download") else if (s.name === "download") elms = renderStagingDownload(s);
elms = renderStagingDownload(s, queueFunc, cancelFunc);
const queue = m( const queue = m(
"button.primary", "button.primary",
@@ -171,9 +174,9 @@ function renderStaging(staging) {
return elements; return elements;
} }
function renderQueue(queue) { function renderQueue(queue): Child[] {
const details = []; const details: Child[] = [];
const devices = {}; const devices: { [deviceId: string]: any[] } = {};
for (const t of queue) { for (const t of queue) {
devices[t.device] = devices[t.device] || []; devices[t.device] = devices[t.device] || [];
devices[t.device].push(t); devices[t.device].push(t);
@@ -282,8 +285,8 @@ function renderQueue(queue) {
return details; return details;
} }
function renderNotifications(notifs) { function renderNotifications(notifs): Child[] {
const notificationElements = []; const notificationElements: Child[] = [];
for (const n of notifs) { for (const n of notifs) {
notificationElements.push( notificationElements.push(
@@ -293,10 +296,10 @@ function renderNotifications(notifs) {
class: n.type, class: n.type,
style: "position: absolute;opacity: 0", style: "position: absolute;opacity: 0",
oncreate: vnode => { oncreate: vnode => {
vnode.dom.style.opacity = 1; (vnode.dom as HTMLDivElement).style.opacity = "1";
}, },
onbeforeremove: vnode => { onbeforeremove: vnode => {
vnode.dom.style.opacity = 0; (vnode.dom as HTMLDivElement).style.opacity = "0";
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {
resolve(); resolve();
@@ -312,7 +315,7 @@ function renderNotifications(notifs) {
return notificationElements; return notificationElements;
} }
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const queue = taskQueue.getQueue(); const queue = taskQueue.getQueue();
@@ -324,21 +327,26 @@ export default function component() {
const stagingElements = renderStaging(staging); const stagingElements = renderStaging(staging);
const queueElements = renderQueue(queue); const queueElements = renderQueue(queue);
function repositionNotifications() { function repositionNotifications(): void {
let top = 10; let top = 10;
for (const c of notificationElements) { for (const c of notificationElements as VnodeDOM[]) {
c.dom.style.top = top; (c.dom as HTMLDivElement).style.top = `${top}`;
top += c.dom.offsetHeight + 10; top += (c.dom as HTMLDivElement).offsetHeight + 10;
} }
} }
function resizeDrawer() { function resizeDrawer(): void {
let height = let height =
statusElement.dom.offsetTop + statusElement.dom.offsetHeight; statusElement.dom.offsetTop + statusElement.dom.offsetHeight;
if (stagingElements.length) { if (stagingElements.length) {
for (const s of stagingElements) for (const s of stagingElements as VnodeDOM[]) {
height = Math.max(height, s.dom.offsetTop + s.dom.offsetHeight); height = Math.max(
} else if (vnode.state.mouseIn) { height,
(s.dom as HTMLDivElement).offsetTop +
(s.dom as HTMLDivElement).offsetHeight
);
}
} else if (vnode.state["mouseIn"]) {
for (const c of drawerElement.children) for (const c of drawerElement.children)
height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight); height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight);
} }
@@ -347,7 +355,7 @@ export default function component() {
if (stagingElements.length + queueElements.length) { if (stagingElements.length + queueElements.length) {
const statusCount = { queued: 0, pending: 0, fault: 0, stale: 0 }; const statusCount = { queued: 0, pending: 0, fault: 0, stale: 0 };
for (const t of queue) statusCount[t.status] += 1; for (const t of queue) statusCount[t["status"]] += 1;
const actions = m( const actions = m(
".actions", ".actions",
@@ -358,7 +366,7 @@ export default function component() {
disabled: !statusCount.queued, disabled: !statusCount.queued,
onclick: () => { onclick: () => {
const tasks = Array.from(taskQueue.getQueue()).filter( const tasks = Array.from(taskQueue.getQueue()).filter(
t => t.status === "queued" t => t["status"] === "queued"
); );
taskQueue taskQueue
.commit( .commit(
@@ -451,25 +459,26 @@ export default function component() {
key: "drawer", key: "drawer",
style: "opacity: 0;height: 0;", style: "opacity: 0;height: 0;",
oncreate: vnode2 => { oncreate: vnode2 => {
vnode.state.mouseIn = false; vnode.state["mouseIn"] = false;
vnode2.dom.style.opacity = 1; (vnode2.dom as HTMLDivElement).style.opacity = "1";
resizeDrawer(); resizeDrawer();
}, },
onmouseover: e => { onmouseover: e => {
vnode.state.mouseIn = true; vnode.state["mouseIn"] = true;
resizeDrawer(); resizeDrawer();
e.redraw = false; e.redraw = false;
}, },
onmouseleave: e => { onmouseleave: e => {
vnode.state.mouseIn = false; vnode.state["mouseIn"] = false;
resizeDrawer(); resizeDrawer();
e.redraw = false; e.redraw = false;
}, },
onupdate: resizeDrawer, onupdate: resizeDrawer,
onbeforeremove: vnode2 => { onbeforeremove: vnode2 => {
vnode2.dom.onmouseover = vnode2.dom.onmouseleave = null; (vnode2.dom as HTMLDivElement).onmouseover = null;
vnode2.dom.style.opacity = 0; (vnode2.dom as HTMLDivElement).onmouseleave = null;
vnode2.dom.style.height = 0; (vnode2.dom as HTMLDivElement).style.opacity = "0";
(vnode2.dom as HTMLDivElement).style.height = "0";
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(resolve, 500); setTimeout(resolve, 500);
}); });
@@ -496,4 +505,6 @@ export default function component() {
); );
} }
}; };
} };
export default component;
@@ -1,9 +1,7 @@
"use strict"; export let codeMirror;
export let yaml;
let codeMirror; export function loadCodeMirror(): Promise<void> {
let yaml;
function loadCodeMirror() {
if (codeMirror) return Promise.resolve(); if (codeMirror) return Promise.resolve();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -20,7 +18,7 @@ function loadCodeMirror() {
}); });
} }
function loadYaml() { export function loadYaml(): Promise<void> {
if (yaml) return Promise.resolve(); if (yaml) return Promise.resolve();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -32,5 +30,3 @@ function loadYaml() {
.catch(reject); .catch(reject);
}); });
} }
export { loadCodeMirror, codeMirror, loadYaml, yaml };
-12
View File
@@ -1,12 +0,0 @@
"use strict";
import { m } from "./components";
export function component() {
return {
view: function(vnode) {
document.title = "Error! - GenieACS";
return m("p.error", vnode.attrs.error);
}
};
}
+11
View File
@@ -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"]);
}
};
};
+28 -26
View File
@@ -1,5 +1,4 @@
"use strict"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import config from "./config"; import config from "./config";
import filterComponent from "./filter-component"; import filterComponent from "./filter-component";
@@ -46,7 +45,7 @@ const unpackSmartQuery = memoize(query => {
}); });
}); });
export function init(args) { export function init(args): Promise<{}> {
if (!window.authorizer.hasAccess("faults", 2)) { if (!window.authorizer.hasAccess("faults", 2)) {
return Promise.reject( return Promise.reject(
new Error("You are not authorized to view this page") new Error("You are not authorized to view this page")
@@ -72,7 +71,7 @@ function renderTable(
downloadUrl, downloadUrl,
sort, sort,
onSortChange onSortChange
) { ): Children {
const faults = faultsResponse.value; const faults = faultsResponse.value;
const selectAll = m("input", { const selectAll = m("input", {
type: "checkbox", type: "checkbox",
@@ -229,57 +228,60 @@ function renderTable(
]; ];
} }
export function component() { export const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
document.title = "Faults - GenieACS"; document.title = "Faults - GenieACS";
function showMore() { function showMore(): void {
vnode.state.showCount = vnode.state["showCount"] =
(vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE;
m.redraw(); m.redraw();
} }
function onFilterChanged(filter) { function onFilterChanged(filter): void {
const ops = { filter }; const ops = { filter };
if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set("/faults", ops); m.route.set("/faults", ops);
} }
function onSortChange(sort) { function onSortChange(sort): void {
const ops = { sort }; const ops = { sort };
if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set("/faults", ops); m.route.set("/faults", ops);
} }
const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; const sort = vnode.attrs["sort"]
let filter = vnode.attrs.filter ? memoizedJsonParse(vnode.attrs["sort"])
? memoizedParse(vnode.attrs.filter) : {};
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true; : true;
filter = unpackSmartQuery(filter); filter = unpackSmartQuery(filter);
const faults = store.fetch("faults", filter, { const faults = store.fetch("faults", filter, {
limit: vnode.state.showCount || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
sort: sort sort: sort
}); });
const count = store.count("faults", filter); const count = store.count("faults", filter);
const selected = new Set(); const selected = new Set();
if (vnode.state.selected) { if (vnode.state["selected"]) {
for (const f of faults.value) for (const f of faults.value)
if (vnode.state.selected.has(f["_id"])) selected.add(f["_id"]); if (vnode.state["selected"].has(f["_id"])) selected.add(f["_id"]);
} }
vnode.state.selected = selected; vnode.state["selected"] = selected;
const downloadUrl = getDownloadUrl(vnode.attrs.filter); const downloadUrl = getDownloadUrl(vnode.attrs["filter"]);
const attrs = {};
attrs["resource"] = "faults";
attrs["filter"] = vnode.attrs["filter"];
attrs["onChange"] = onFilterChanged;
return [ return [
m("h1", "Listing faults"), m("h1", "Listing faults"),
m(filterComponent, { m(filterComponent, attrs),
resource: "faults",
filter: vnode.attrs.filter,
onChange: onFilterChanged
}),
renderTable( renderTable(
faults, faults,
count.value, count.value,
@@ -292,4 +294,4 @@ export function component() {
]; ];
} }
}; };
} };
+48 -33
View File
@@ -1,5 +1,4 @@
"use strict"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import config from "./config"; import config from "./config";
import filterComponent from "./filter-component"; import filterComponent from "./filter-component";
@@ -16,7 +15,12 @@ const PAGE_SIZE = config.ui.pageSize || 10;
const memoizedParse = memoize(parse); const memoizedParse = memoize(parse);
const memoizedJsonParse = memoize(JSON.parse); const memoizedJsonParse = memoize(JSON.parse);
const attributes = [ const attributes: {
id: string;
label: string;
type?: string;
options?: any;
}[] = [
{ id: "_id", label: "Name" }, { id: "_id", label: "Name" },
{ {
id: "metadata.fileType", id: "metadata.fileType",
@@ -50,7 +54,11 @@ const unpackSmartQuery = memoize(query => {
}); });
}); });
function putActionHandler(action, _object) { interface ValidationErrors {
[prop: string]: string;
}
function putActionHandler(action, _object): Promise<ValidationErrors> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const object = Object.assign({}, _object); const object = Object.assign({}, _object);
if (action === "save") { if (action === "save") {
@@ -109,7 +117,7 @@ const getDownloadUrl = memoize(filter => {
})}`; })}`;
}); });
export function init(args) { export function init(args): Promise<{}> {
if (!window.authorizer.hasAccess("files", 2)) { if (!window.authorizer.hasAccess("files", 2)) {
return Promise.reject( return Promise.reject(
new Error("You are not authorized to view this page") new Error("You are not authorized to view this page")
@@ -129,7 +137,7 @@ function renderTable(
downloadUrl, downloadUrl,
sort, sort,
onSortChange onSortChange
) { ): Children {
const files = filesResponse.value; const files = filesResponse.value;
const selectAll = m("input", { const selectAll = m("input", {
type: "checkbox", type: "checkbox",
@@ -270,7 +278,7 @@ function renderTable(
{ {
title: "Create new file", title: "Create new file",
onclick: () => { onclick: () => {
const cb = () => { const cb = (): Children => {
return m( return m(
putFormComponent, putFormComponent,
Object.assign( Object.assign(
@@ -279,9 +287,11 @@ function renderTable(
return new Promise(resolve => { return new Promise(resolve => {
putActionHandler(action, object) putActionHandler(action, object)
.then(errors => { .then(errors => {
errors = errors ? Object.values(errors) : []; const errorList = errors
if (errors.length) { ? Object.values(errors)
for (const err of errors) : [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err); notifications.push("error", err);
} else { } else {
overlay.close(cb); overlay.close(cb);
@@ -318,58 +328,63 @@ function renderTable(
]; ];
} }
export function component() { export const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
document.title = "Files - GenieACS"; document.title = "Files - GenieACS";
function showMore() { function showMore(): void {
vnode.state.showCount = vnode.state["showCount"] =
(vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE;
m.redraw(); m.redraw();
} }
function onFilterChanged(filter) { function onFilterChanged(filter): void {
const ops = { filter }; const ops = { filter };
if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set(m.route.get(), ops); m.route.set(m.route.get(), ops);
} }
function onSortChange(sort) { function onSortChange(sort): void {
const ops = { sort }; const ops = { sort };
if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set(m.route.get(), ops); m.route.set(m.route.get(), ops);
} }
const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; const sort = vnode.attrs["sort"]
let filter = vnode.attrs.filter ? memoizedJsonParse(vnode.attrs["sort"])
? memoizedParse(vnode.attrs.filter) : {};
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true; : true;
filter = unpackSmartQuery(filter); filter = unpackSmartQuery(filter);
const files = store.fetch("files", filter, { const files = store.fetch("files", filter, {
limit: vnode.state.showCount || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
sort: sort sort: sort
}); });
const count = store.count("files", filter); const count = store.count("files", filter);
const selected = new Set(); const selected = new Set();
if (vnode.state.selected) { if (vnode.state["selected"]) {
for (const file of files.value) for (const file of files.value) {
if (vnode.state.selected.has(file["_id"])) selected.add(file["_id"]); if (vnode.state["selected"].has(file["_id"]))
selected.add(file["_id"]);
}
} }
vnode.state.selected = selected; vnode.state["selected"] = selected;
const downloadUrl = getDownloadUrl(vnode.attrs.filter); const downloadUrl = getDownloadUrl(vnode.attrs["filter"]);
const attrs = {};
attrs["resource"] = "files";
attrs["filter"] = vnode.attrs["filter"];
attrs["onChange"] = onFilterChanged;
return [ return [
m("h1", "Listing files"), m("h1", "Listing files"),
m(filterComponent, { m(filterComponent, attrs),
resource: "files",
filter: vnode.attrs.filter,
onChange: onFilterChanged
}),
renderTable( renderTable(
files, files,
count.value, count.value,
@@ -382,4 +397,4 @@ export function component() {
]; ];
} }
}; };
} };
@@ -1,11 +1,10 @@
"use strict"; import m, { ClosureComponent, Component } from "mithril";
import m from "mithril";
import { parse, stringify, map } from "../lib/common/expression-parser"; import { parse, stringify, map } from "../lib/common/expression-parser";
import memoize from "../lib/common/memoize"; import memoize from "../lib/common/memoize";
import Autocomplete from "./autocomplete-compnent"; import Autocomplete from "./autocomplete-compnent";
import * as smartQuery from "./smart-query"; import * as smartQuery from "./smart-query";
import { filterToMongoQuery } from "../lib/ui/mongodb-functions"; import { filterToMongoQuery } from "../lib/ui/mongodb-functions";
import { Expression } from "../lib/types";
const getAutocomplete = memoize(resource => { const getAutocomplete = memoize(resource => {
const labels = smartQuery.getLabels(resource); const labels = smartQuery.getLabels(resource);
@@ -16,7 +15,7 @@ const getAutocomplete = memoize(resource => {
return autocomplete; return autocomplete;
}); });
function parseFilter(resource, f) { function parseFilter(resource, f): Expression {
let exp; let exp;
if (/^[\s0-9a-zA-Z]+:/.test(f)) { if (/^[\s0-9a-zA-Z]+:/.test(f)) {
const k = f.split(":", 1)[0]; const k = f.split(":", 1)[0];
@@ -38,7 +37,7 @@ function parseFilter(resource, f) {
return exp; return exp;
} }
function stringifyFilter(f) { function stringifyFilter(f: Expression): string {
if (Array.isArray(f) && f[0] === "FUNC" && f[1] === "Q") if (Array.isArray(f) && f[0] === "FUNC" && f[1] === "Q")
return `${f[2]}: ${f[3]}`; return `${f[2]}: ${f[3]}`;
return stringify(f); return stringify(f);
@@ -56,39 +55,39 @@ const splitFilter = memoize(filter => {
return list; return list;
}); });
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
if ( if (
!vnode.state.filterList || !vnode.state["filterList"] ||
vnode.attrs.filter !== vnode.state.filter vnode.attrs["filter"] !== vnode.state["filter"]
) { ) {
vnode.state.filterInvalid = 0; vnode.state["filterInvalid"] = 0;
vnode.state.filter = vnode.attrs.filter; vnode.state["filter"] = vnode.attrs["filter"];
vnode.state.filterList = splitFilter(vnode.attrs.filter); vnode.state["filterList"] = splitFilter(vnode.attrs["filter"]);
} }
function onChange() { function onChange(): void {
vnode.state.filterInvalid = 0; vnode.state["filterInvalid"] = 0;
vnode.state.filterList = vnode.state.filterList.filter(f => f); vnode.state["filterList"] = vnode.state["filterList"].filter(f => f);
let filter = vnode.state.filterList.map((f, idx) => { let filter = vnode.state["filterList"].map((f, idx) => {
try { try {
return parseFilter(vnode.attrs.resource, f); return parseFilter(vnode.attrs["resource"], f);
} catch (err) { } catch (err) {
vnode.state.filterInvalid |= 1 << idx; vnode.state["filterInvalid"] |= 1 << idx;
} }
return null; return null;
}); });
vnode.state.filterList.push(""); vnode.state["filterList"].push("");
if (!vnode.state.filterInvalid) { if (!vnode.state["filterInvalid"]) {
delete vnode.state.filter; delete vnode.state["filter"];
if (filter.length === 0) { if (filter.length === 0) {
vnode.attrs.onChange(""); vnode.attrs["onChange"]("");
} else { } else {
if (filter.length > 1) filter = ["AND"].concat(filter); if (filter.length > 1) filter = ["AND"].concat(filter);
else filter = filter[0]; else filter = filter[0];
vnode.attrs.onChange(stringify(filter)); vnode.attrs["onChange"](stringify(filter));
} }
} }
} }
@@ -96,18 +95,20 @@ export default function component() {
return m( return m(
"div.filter", "div.filter",
[m("b", "Filter")].concat( [m("b", "Filter")].concat(
vnode.state.filterList.map((fltr, idx) => { vnode.state["filterList"].map((fltr, idx) => {
return m("input", { return m("input", {
type: "text", type: "text",
class: `${(vnode.state.filterInvalid >> idx) & 1 ? "error" : ""}`, class: `${
(vnode.state["filterInvalid"] >> idx) & 1 ? "error" : ""
}`,
value: fltr, value: fltr,
onchange: e => { onchange: e => {
vnode.state.filterList = vnode.state.filterList.slice(); vnode.state["filterList"] = vnode.state["filterList"].slice();
vnode.state.filterList[idx] = e.target.value.trim(); vnode.state["filterList"][idx] = e.target.value.trim();
onChange(); onChange();
}, },
oncreate: vn => { oncreate: vn => {
getAutocomplete(vnode.attrs.resource).attach(vn.dom); getAutocomplete(vnode.attrs["resource"]).attach(vn.dom);
} }
}); });
}) })
@@ -115,4 +116,6 @@ export default function component() {
); );
} }
}; };
} };
export default component;
+17 -9
View File
@@ -1,6 +1,4 @@
"use strict"; import m, { ClosureComponent, Component } from "mithril";
import m from "mithril";
import menu from "./menu"; import menu from "./menu";
import drawerComponent from "./drawer-component"; import drawerComponent from "./drawer-component";
import userMenu from "./user-menu"; import userMenu from "./user-menu";
@@ -15,29 +13,39 @@ const adminPages = [
"config" "config"
]; ];
export default function layout() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
let sideMenu, group; let sideMenu, group;
if (adminPages.includes(vnode.attrs.page)) {
if (adminPages.includes(vnode.attrs["page"])) {
group = "admin"; group = "admin";
sideMenu = m(adminMenu, { page: vnode.attrs.page }); const attrs = {};
attrs["page"] = vnode.attrs["page"];
sideMenu = m(adminMenu, attrs);
} }
const attrs = {};
attrs["page"] = group || vnode.attrs["page"];
return [ return [
m("#header", [ m("#header", [
m("img.logo", { src: "logo.svg" }), m("img.logo", { src: "logo.svg" }),
m(userMenu), m(userMenu),
m(menu, { page: group || vnode.attrs.page }), m(menu, attrs),
m(drawerComponent) m(drawerComponent)
]), ]),
m( m(
"#content-wrapper", "#content-wrapper",
sideMenu, sideMenu,
m("#content", { class: `page-${vnode.attrs.page}` }, [vnode.children]) m("#content", { class: `page-${vnode.attrs["page"]}` }, [
vnode.children
])
), ),
overlay.render() overlay.render()
]; ];
} }
}; };
} };
export default component;
+11 -12
View File
@@ -1,17 +1,16 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "./components"; import { m } from "./components";
import * as store from "./store"; import * as store from "./store";
import * as notifications from "./notifications"; import * as notifications from "./notifications";
export function init(args) { export function init(args): Promise<{}> {
return Promise.resolve(args); return Promise.resolve(args);
} }
export function component() { export const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
if (window.username) m.route.set(vnode.attrs.continue || "/"); if (window.username) m.route.set(vnode.attrs["continue"] || "/");
document.title = "Login - GenieACS"; document.title = "Login - GenieACS";
return [ return [
@@ -24,11 +23,11 @@ export function component() {
m("input", { m("input", {
name: "username", name: "username",
type: "text", type: "text",
value: vnode.state.username, value: vnode.state["username"],
oncreate: vnode2 => { oncreate: vnode2 => {
vnode2.dom.focus(); (vnode2.dom as HTMLInputElement).focus();
}, },
oninput: m.withAttr("value", v => (vnode.state.username = v)) oninput: m.withAttr("value", v => (vnode.state["username"] = v))
}) })
), ),
m( m(
@@ -37,8 +36,8 @@ export function component() {
m("input", { m("input", {
name: "password", name: "password",
type: "password", type: "password",
value: vnode.state.password, value: vnode.state["password"],
oninput: m.withAttr("value", v => (vnode.state.password = v)) oninput: m.withAttr("value", v => (vnode.state["password"] = v))
}) })
), ),
m( m(
@@ -50,7 +49,7 @@ export function component() {
onclick: e => { onclick: e => {
e.target.disabled = true; e.target.disabled = true;
store store
.logIn(vnode.state.username, vnode.state.password) .logIn(vnode.state["username"], vnode.state["password"])
.then(() => { .then(() => {
location.reload(); location.reload();
}) })
@@ -68,4 +67,4 @@ export function component() {
]; ];
} }
}; };
} };
@@ -1,15 +1,13 @@
"use strict"; import m, { ClosureComponent, Component } from "mithril";
import m from "mithril";
import * as overlay from "./overlay"; import * as overlay from "./overlay";
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const text = vnode.attrs.text; const text = vnode.attrs["text"];
const element = vnode.attrs.element || "span"; const element = vnode.attrs["element"] || "span";
function overflowed(_vnode) { function overflowed(_vnode): void {
if (_vnode.dom.classList.contains("long-text-overflowed")) return; if (_vnode.dom.classList.contains("long-text-overflowed")) return;
_vnode.dom.classList.add("long-text-overflowed"); _vnode.dom.classList.add("long-text-overflowed");
_vnode.dom.onclick = e => { _vnode.dom.onclick = e => {
@@ -20,8 +18,8 @@ export default function component() {
rows: 24, rows: 24,
readonly: "", readonly: "",
oncreate: vnode2 => { oncreate: vnode2 => {
vnode2.dom.focus(); (vnode2.dom as HTMLTextAreaElement).focus();
vnode2.dom.select(); (vnode2.dom as HTMLTextAreaElement).select();
} }
}); });
}); });
@@ -40,8 +38,10 @@ export default function component() {
}, },
onupdate: vnode2 => { onupdate: vnode2 => {
if (vnode2.dom.clientWidth === vnode2.dom.scrollWidth) { if (vnode2.dom.clientWidth === vnode2.dom.scrollWidth) {
vnode2.dom.classList.remove("long-text-overflowed"); (vnode2.dom as HTMLElement).classList.remove(
vnode2.dom.onclick = null; "long-text-overflowed"
);
(vnode2.dom as HTMLElement).onclick = null;
} else { } else {
overflowed(vnode2); overflowed(vnode2);
} }
@@ -53,4 +53,6 @@ export default function component() {
); );
} }
}; };
} };
export default component;
+6 -6
View File
@@ -1,11 +1,9 @@
"use strict"; import m, { ClosureComponent, Component } from "mithril";
import m from "mithril"; const component: ClosureComponent = (): Component => {
export default function menu() {
return { return {
view: vnode => { view: vnode => {
const active = { [vnode.attrs.page]: "active" }; const active = { [vnode.attrs["page"]]: "active" };
const tabs = []; const tabs = [];
if (window.authorizer.hasAccess("devices", 1)) { if (window.authorizer.hasAccess("devices", 1)) {
@@ -60,4 +58,6 @@ export default function menu() {
return m("nav", m("ul", tabs)); return m("nav", m("ul", tabs));
} }
}; };
} };
export default component;
+9 -5
View File
@@ -1,10 +1,14 @@
"use strict";
import m from "mithril"; import m from "mithril";
const notifications = new Set(); interface Notification {
type: string;
message: string;
timestamp: number;
}
export function push(type, message) { const notifications = new Set<Notification>();
export function push(type, message): void {
const n = { type: type, message: message, timestamp: Date.now() }; const n = { type: type, message: message, timestamp: Date.now() };
notifications.add(n); notifications.add(n);
m.redraw(); m.redraw();
@@ -14,6 +18,6 @@ export function push(type, message) {
}, 4000); }, 4000);
} }
export function getNotifications() { export function getNotifications(): Set<Notification> {
return notifications; return notifications;
} }
+9 -11
View File
@@ -1,14 +1,14 @@
"use strict"; import m, { Children } from "mithril";
import m from "mithril"; type OverlayCallback = () => Children;
let overlayCallback = null; let overlayCallback: OverlayCallback = null;
function open(callback) { export function open(callback: OverlayCallback): void {
overlayCallback = callback; overlayCallback = callback;
} }
function close(callback) { export function close(callback: OverlayCallback): boolean {
if (callback === overlayCallback) { if (callback === overlayCallback) {
overlayCallback = null; overlayCallback = null;
return true; return true;
@@ -17,7 +17,7 @@ function close(callback) {
return false; return false;
} }
function render() { export function render(): Children {
if (overlayCallback) { if (overlayCallback) {
return m( return m(
".overlay-wrapper", ".overlay-wrapper",
@@ -28,11 +28,11 @@ function render() {
}, },
style: "opacity: 0", style: "opacity: 0",
oncreate: vnode => { oncreate: vnode => {
vnode.dom.focus(); (vnode.dom as HTMLDivElement).focus();
vnode.dom.style.opacity = 1; (vnode.dom as HTMLDivElement).style.opacity = "1";
}, },
onbeforeremove: vnode => { onbeforeremove: vnode => {
vnode.dom.style.opacity = 0; (vnode.dom as HTMLDivElement).style.opacity = "0";
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {
resolve(); resolve();
@@ -62,5 +62,3 @@ document.addEventListener("keydown", e => {
window.addEventListener("popstate", () => { window.addEventListener("popstate", () => {
if (close(overlayCallback)) m.redraw(); if (close(overlayCallback)) m.redraw();
}); });
export { open, close, render };
+17 -16
View File
@@ -1,5 +1,4 @@
"use strict"; import { ClosureComponent, Component } from "mithril";
import { m } from "./components"; import { m } from "./components";
import config from "./config"; import config from "./config";
import * as store from "./store"; import * as store from "./store";
@@ -8,25 +7,25 @@ import pieChartComponent from "./pie-chart-component";
const GROUPS = config.ui.overview.groups; const GROUPS = config.ui.overview.groups;
const CHARTS = {}; const CHARTS = {};
for (const group of Object.values(GROUPS)) { for (const group of Object.values(GROUPS)) {
for (const chartName of Object.values(group["charts"])) for (const chartName of Object.values(group["charts"]) as string[])
CHARTS[chartName] = config.ui.overview.charts[chartName]; CHARTS[chartName] = config.ui.overview.charts[chartName];
} }
function queryCharts(charts) { function queryCharts(charts): {} {
charts = Object.assign({}, charts); charts = Object.assign({}, charts);
for (let [chartName, chart] of Object.entries(charts)) { for (let [chartName, chart] of Object.entries(charts)) {
charts[chartName] = chart = Object.assign({}, chart); charts[chartName] = chart = Object.assign({}, chart);
chart.slices = Object.assign({}, chart.slices); chart["slices"] = Object.assign({}, chart["slices"]);
for (let [sliceName, slice] of Object.entries(chart.slices)) { for (let [sliceName, slice] of Object.entries(chart["slices"])) {
const filter = slice.filter; const filter = slice["filter"];
chart.slices[sliceName] = slice = Object.assign({}, slice); chart["slices"][sliceName] = slice = Object.assign({}, slice);
slice.count = store.count("devices", filter); slice["count"] = store.count("devices", filter);
} }
} }
return charts; return charts;
} }
export function init() { export function init(): Promise<{}> {
if (!window.authorizer.hasAccess("devices", 1)) { if (!window.authorizer.hasAccess("devices", 1)) {
return Promise.reject( return Promise.reject(
new Error("You are not authorized to view this page") new Error("You are not authorized to view this page")
@@ -36,21 +35,23 @@ export function init() {
return Promise.resolve({ charts: queryCharts(CHARTS) }); return Promise.resolve({ charts: queryCharts(CHARTS) });
} }
export function component() { export const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
document.title = "Overview - GenieACS"; document.title = "Overview - GenieACS";
const children = []; const children = [];
for (const group of Object.values(GROUPS)) { for (const group of Object.values(GROUPS)) {
if (group.label) children.push(m("h1", group.label)); if (group["label"]) children.push(m("h1", group["label"]));
const groupChildren = []; const groupChildren = [];
for (const chartName of Object.values(group.charts)) { for (const chartName of Object.values(group["charts"]) as string[]) {
const chart = vnode.attrs.charts[chartName]; const chart = vnode.attrs["charts"][chartName];
const chartChildren = []; const chartChildren = [];
if (chart.label) chartChildren.push(m("h2", chart.label)); if (chart.label) chartChildren.push(m("h2", chart.label));
chartChildren.push(m(pieChartComponent, { chart: chart })); const attrs = {};
attrs["chart"] = chart;
chartChildren.push(m(pieChartComponent, attrs));
groupChildren.push(m(".overview-chart", chartChildren)); groupChildren.push(m(".overview-chart", chartChildren));
} }
@@ -61,4 +62,4 @@ export function component() {
return children; return children;
} }
}; };
} };
@@ -1,15 +1,14 @@
"use strict"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import { stringify } from "../lib/common/expression-parser"; import { stringify } from "../lib/common/expression-parser";
import memoize from "../lib/common/memoize"; import memoize from "../lib/common/memoize";
const memoizedStringify = memoize(stringify); const memoizedStringify = memoize(stringify);
const drawChart = function(chartData) { function drawChart(chartData): Children {
const slices = chartData.slices; const slices = chartData.slices;
const total = Array.from(Object.values(chartData.slices)).reduce( const total: number = Array.from(Object.values(chartData.slices)).reduce(
(a, s) => a + (s.count.value || 0), (a: number, s) => a + (s["count"]["value"] || 0),
0 0
); );
const legend = []; const legend = [];
@@ -21,21 +20,21 @@ const drawChart = function(chartData) {
let endX, endY; let endX, endY;
for (const slice of Object.values(slices)) { for (const slice of Object.values(slices)) {
const percent = total > 0 ? (slice.count.value || 0) / total : 0; const percent = total > 0 ? (slice["count"]["value"] || 0) / total : 0;
legend.push( legend.push(
m(".legend-line", [ m(".legend-line", [
m("span.color", { m("span.color", {
style: `background-color: ${slice.color} !important;` style: `background-color: ${slice["color"]} !important;`
}), }),
`${slice.label}: `, `${slice["label"]}: `,
m( m(
"a", "a",
{ {
href: `/#!/devices/?${m.buildQueryString({ href: `/#!/devices/?${m.buildQueryString({
filter: memoizedStringify(slice.filter) filter: memoizedStringify(slice["filter"])
})}` })}`
}, },
slice.count.value || 0 slice["count"]["value"] || 0
), ),
` (${(percent * 100).toFixed(2)}%)` ` (${(percent * 100).toFixed(2)}%)`
]) ])
@@ -58,7 +57,7 @@ const drawChart = function(chartData) {
paths.push( paths.push(
m("path", { m("path", {
d: sketch, d: sketch,
fill: slice.color fill: slice["color"]
}) })
); );
@@ -72,7 +71,7 @@ const drawChart = function(chartData) {
"a", "a",
{ {
"xlink:href": `/#!/devices/?${m.buildQueryString({ "xlink:href": `/#!/devices/?${m.buildQueryString({
filter: memoizedStringify(slice.filter) filter: memoizedStringify(slice["filter"])
})}` })}`
}, },
[ [
@@ -113,12 +112,14 @@ const drawChart = function(chartData) {
), ),
m(".legend", legend) m(".legend", legend)
]); ]);
}; }
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
return drawChart(vnode.attrs.chart); return drawChart(vnode.attrs["chart"]);
} }
}; };
} };
export default component;
+47 -37
View File
@@ -1,5 +1,4 @@
"use strict"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import config from "./config"; import config from "./config";
import filterComponent from "./filter-component"; import filterComponent from "./filter-component";
@@ -35,7 +34,11 @@ const unpackSmartQuery = memoize(query => {
}); });
}); });
function putActionHandler(action, _object, isNew) { interface ValidationErrors {
[prop: string]: string;
}
function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const object = Object.assign({}, _object); const object = Object.assign({}, _object);
if (action === "save") { if (action === "save") {
@@ -45,7 +48,7 @@ function putActionHandler(action, _object, isNew) {
const errors = {}; const errors = {};
if (!id) errors["_id"] = "ID can not be empty"; if (!id) errors["_id"] = "ID can not be empty";
if (!object.provision) errors.provision = "Provision not selected"; if (!object.provision) errors["provision"] = "Provision not selected";
if (Object.keys(errors).length) return void resolve(errors); if (Object.keys(errors).length) return void resolve(errors);
@@ -107,7 +110,7 @@ const getDownloadUrl = memoize(filter => {
})}`; })}`;
}); });
export function init(args) { export function init(args): Promise<{}> {
if (!window.authorizer.hasAccess("presets", 2)) { if (!window.authorizer.hasAccess("presets", 2)) {
return Promise.reject( return Promise.reject(
new Error("You are not authorized to view this page") new Error("You are not authorized to view this page")
@@ -127,7 +130,7 @@ function renderTable(
downloadUrl, downloadUrl,
sort, sort,
onSortChange onSortChange
) { ): Children {
const presets = presetsResponse.value; const presets = presetsResponse.value;
const selectAll = m("input", { const selectAll = m("input", {
type: "checkbox", type: "checkbox",
@@ -215,7 +218,7 @@ function renderTable(
"a", "a",
{ {
onclick: () => { onclick: () => {
const cb = () => { const cb = (): Children => {
return m( return m(
putFormComponent, putFormComponent,
Object.assign( Object.assign(
@@ -225,9 +228,11 @@ function renderTable(
return new Promise(resolve => { return new Promise(resolve => {
putActionHandler(action, object, false) putActionHandler(action, object, false)
.then(errors => { .then(errors => {
errors = errors ? Object.values(errors) : []; const errorList = errors
if (errors.length) { ? Object.values(errors)
for (const err of errors) : [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err); notifications.push("error", err);
} else { } else {
overlay.close(cb); overlay.close(cb);
@@ -334,7 +339,7 @@ function renderTable(
{ {
title: "Create new preset", title: "Create new preset",
onclick: () => { onclick: () => {
const cb = () => { const cb = (): Children => {
return m( return m(
putFormComponent, putFormComponent,
Object.assign( Object.assign(
@@ -343,9 +348,11 @@ function renderTable(
return new Promise(resolve => { return new Promise(resolve => {
putActionHandler(action, object, true) putActionHandler(action, object, true)
.then(errors => { .then(errors => {
errors = errors ? Object.values(errors) : []; const errorList = errors
if (errors.length) { ? Object.values(errors)
for (const err of errors) : [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err); notifications.push("error", err);
} else { } else {
overlay.close(cb); overlay.close(cb);
@@ -382,37 +389,39 @@ function renderTable(
]; ];
} }
export function component() { export const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
document.title = "Presets - GenieACS"; document.title = "Presets - GenieACS";
function showMore() { function showMore(): void {
vnode.state.showCount = vnode.state["showCount"] =
(vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE;
m.redraw(); m.redraw();
} }
function onFilterChanged(filter) { function onFilterChanged(filter): void {
const ops = { filter }; const ops = { filter };
if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set(m.route.get(), ops); m.route.set(m.route.get(), ops);
} }
function onSortChange(sort) { function onSortChange(sort): void {
const ops = { sort }; const ops = { sort };
if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set(m.route.get(), ops); m.route.set(m.route.get(), ops);
} }
const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; const sort = vnode.attrs["sort"]
let filter = vnode.attrs.filter ? memoizedJsonParse(vnode.attrs["sort"])
? memoizedParse(vnode.attrs.filter) : {};
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true; : true;
filter = unpackSmartQuery(filter); filter = unpackSmartQuery(filter);
const presets = store.fetch("presets", filter, { const presets = store.fetch("presets", filter, {
limit: vnode.state.showCount || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
sort: sort sort: sort
}); });
const count = store.count("presets", filter); const count = store.count("presets", filter);
@@ -435,27 +444,28 @@ export function component() {
for (const p of provisions.value) provisionIds.add(p["_id"]); for (const p of provisions.value) provisionIds.add(p["_id"]);
provisionAttr.options = Array.from(provisionIds); provisionAttr["options"] = Array.from(provisionIds);
} }
const selected = new Set(); const selected = new Set();
if (vnode.state.selected) { if (vnode.state["selected"]) {
for (const preset of presets.value) { for (const preset of presets.value) {
if (vnode.state.selected.has(preset["_id"])) if (vnode.state["selected"].has(preset["_id"]))
selected.add(preset["_id"]); selected.add(preset["_id"]);
} }
} }
vnode.state.selected = selected; vnode.state["selected"] = selected;
const downloadUrl = getDownloadUrl(vnode.attrs.filter); const downloadUrl = getDownloadUrl(vnode.attrs["filter"]);
const attrs = {};
attrs["resource"] = "presets";
attrs["filter"] = vnode.attrs["filter"];
attrs["onChange"] = onFilterChanged;
return [ return [
m("h1", "Listing presets"), m("h1", "Listing presets"),
m(filterComponent, { m(filterComponent, attrs),
resource: "presets",
filter: vnode.attrs.filter,
onChange: onFilterChanged
}),
renderTable( renderTable(
presets, presets,
count.value, count.value,
@@ -468,4 +478,4 @@ export function component() {
]; ];
} }
}; };
} };
+45 -35
View File
@@ -1,5 +1,4 @@
"use strict"; import { ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import config from "./config"; import config from "./config";
import filterComponent from "./filter-component"; import filterComponent from "./filter-component";
@@ -30,7 +29,11 @@ const unpackSmartQuery = memoize(query => {
}); });
}); });
function putActionHandler(action, _object, isNew) { interface ValidationErrors {
[prop: string]: string;
}
function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const object = Object.assign({}, _object); const object = Object.assign({}, _object);
if (action === "save") { if (action === "save") {
@@ -97,7 +100,7 @@ const getDownloadUrl = memoize(filter => {
})}`; })}`;
}); });
export function init(args) { export function init(args): Promise<{}> {
if (!window.authorizer.hasAccess("provisions", 2)) { if (!window.authorizer.hasAccess("provisions", 2)) {
return Promise.reject( return Promise.reject(
new Error("You are not authorized to view this page") new Error("You are not authorized to view this page")
@@ -124,7 +127,7 @@ function renderTable(
downloadUrl, downloadUrl,
sort, sort,
onSortChange onSortChange
) { ): Children {
const provisions = provisionsResponse.value; const provisions = provisionsResponse.value;
const selectAll = m("input", { const selectAll = m("input", {
type: "checkbox", type: "checkbox",
@@ -197,7 +200,7 @@ function renderTable(
"a", "a",
{ {
onclick: () => { onclick: () => {
const cb = () => { const cb = (): Children => {
return m( return m(
putFormComponent, putFormComponent,
Object.assign( Object.assign(
@@ -207,9 +210,11 @@ function renderTable(
return new Promise(resolve => { return new Promise(resolve => {
putActionHandler(action, object, false) putActionHandler(action, object, false)
.then(errors => { .then(errors => {
errors = errors ? Object.values(errors) : []; const errorList = errors
if (errors.length) { ? Object.values(errors)
for (const err of errors) : [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err); notifications.push("error", err);
} else { } else {
overlay.close(cb); overlay.close(cb);
@@ -322,7 +327,7 @@ function renderTable(
{ {
title: "Create new provision", title: "Create new provision",
onclick: () => { onclick: () => {
const cb = () => { const cb = (): Children => {
return m( return m(
putFormComponent, putFormComponent,
Object.assign( Object.assign(
@@ -331,9 +336,11 @@ function renderTable(
return new Promise(resolve => { return new Promise(resolve => {
putActionHandler(action, object, true) putActionHandler(action, object, true)
.then(errors => { .then(errors => {
errors = errors ? Object.values(errors) : []; const errorList = errors
if (errors.length) { ? Object.values(errors)
for (const err of errors) : [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err); notifications.push("error", err);
} else { } else {
overlay.close(cb); overlay.close(cb);
@@ -370,60 +377,63 @@ function renderTable(
]; ];
} }
export function component() { export const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
document.title = "Provisions - GenieACS"; document.title = "Provisions - GenieACS";
function showMore() { function showMore(): void {
vnode.state.showCount = vnode.state["showCount"] =
(vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; (vnode.state["showCount"] || PAGE_SIZE) + PAGE_SIZE;
m.redraw(); m.redraw();
} }
function onFilterChanged(filter) { function onFilterChanged(filter): void {
const ops = { filter }; const ops = { filter };
if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
m.route.set(m.route.get(), ops); m.route.set(m.route.get(), ops);
} }
function onSortChange(sort) { function onSortChange(sort): void {
const ops = { sort }; const ops = { sort };
if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
m.route.set(m.route.get(), ops); m.route.set(m.route.get(), ops);
} }
const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; const sort = vnode.attrs["sort"]
let filter = vnode.attrs.filter ? memoizedJsonParse(vnode.attrs["sort"])
? memoizedParse(vnode.attrs.filter) : {};
let filter = vnode.attrs["filter"]
? memoizedParse(vnode.attrs["filter"])
: true; : true;
filter = unpackSmartQuery(filter); filter = unpackSmartQuery(filter);
const provisions = store.fetch("provisions", filter, { const provisions = store.fetch("provisions", filter, {
limit: vnode.state.showCount || PAGE_SIZE, limit: vnode.state["showCount"] || PAGE_SIZE,
sort: sort sort: sort
}); });
const count = store.count("provisions", filter); const count = store.count("provisions", filter);
const selected = new Set(); const selected = new Set();
if (vnode.state.selected) { if (vnode.state["selected"]) {
for (const provision of provisions.value) { for (const provision of provisions.value) {
if (vnode.state.selected.has(provision["_id"])) if (vnode.state["selected"].has(provision["_id"]))
selected.add(provision["_id"]); selected.add(provision["_id"]);
} }
} }
vnode.state.selected = selected; vnode.state["selected"] = selected;
const downloadUrl = getDownloadUrl(vnode.attrs.filter); const downloadUrl = getDownloadUrl(vnode.attrs["filter"]);
const attrs = {};
attrs["resource"] = "provisions";
attrs["filter"] = vnode.attrs["filter"];
attrs["onChange"] = onFilterChanged;
return [ return [
m("h1", "Listing provisions"), m("h1", "Listing provisions"),
m(filterComponent, { m(filterComponent, attrs),
resource: "provisions",
filter: vnode.attrs.filter,
onChange: onFilterChanged
}),
renderTable( renderTable(
provisions, provisions,
count.value, count.value,
@@ -436,4 +446,4 @@ export function component() {
]; ];
} }
}; };
} };
@@ -1,5 +1,4 @@
"use strict"; import { VnodeDOM, ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import { codeMirror } from "./dynamic-loader"; import { codeMirror } from "./dynamic-loader";
@@ -10,7 +9,7 @@ const singular = {
files: "file" files: "file"
}; };
function createField(current, attr, focus) { function createField(current, attr, focus): Children {
if (attr.type === "combo") { if (attr.type === "combo") {
const options = [m("option", { value: "" }, "--Please choose--")]; const options = [m("option", { value: "" }, "--Please choose--")];
for (const op of attr.options) options.push(m("option", { value: op }, op)); for (const op of attr.options) options.push(m("option", { value: op }, op));
@@ -25,7 +24,7 @@ function createField(current, attr, focus) {
value: selected, value: selected,
oncreate: focus oncreate: focus
? _vnode => { ? _vnode => {
_vnode.dom.focus(); (_vnode.dom as HTMLSelectElement).focus();
} }
: null, : null,
onchange: e => { onchange: e => {
@@ -45,10 +44,14 @@ function createField(current, attr, focus) {
lineNumbers: true, lineNumbers: true,
extraKeys: { extraKeys: {
"Ctrl-Enter": () => { "Ctrl-Enter": () => {
_vnode.dom.form.querySelector("button[type=submit]").click(); ((_vnode.dom as HTMLTextAreaElement).form.querySelector(
"button[type=submit]"
) as HTMLButtonElement).click();
}, },
"Cmd-Enter": () => { "Cmd-Enter": () => {
_vnode.dom.form.querySelector("button[type=submit]").click(); ((_vnode.dom as HTMLTextAreaElement).form.querySelector(
"button[type=submit]"
) as HTMLButtonElement).click();
} }
} }
}); });
@@ -66,7 +69,7 @@ function createField(current, attr, focus) {
name: attr.id, name: attr.id,
oncreate: focus oncreate: focus
? _vnode => { ? _vnode => {
_vnode.dom.focus(); (_vnode.dom as HTMLInputElement).focus();
} }
: null, : null,
onchange: e => { onchange: e => {
@@ -83,7 +86,7 @@ function createField(current, attr, focus) {
value: current.object[attr.id], value: current.object[attr.id],
oncreate: focus oncreate: focus
? _vnode => { ? _vnode => {
_vnode.dom.focus(); (_vnode.dom as HTMLInputElement).focus();
} }
: null, : null,
oninput: e => { oninput: e => {
@@ -93,21 +96,21 @@ function createField(current, attr, focus) {
}); });
} }
export default function component() { const component: ClosureComponent = (): Component => {
return { return {
view: vnode => { view: vnode => {
const actionHandler = vnode.attrs.actionHandler; const actionHandler = vnode.attrs["actionHandler"];
const attributes = vnode.attrs.attributes; const attributes = vnode.attrs["attributes"];
const resource = vnode.attrs.resource; const resource = vnode.attrs["resource"];
const base = vnode.attrs.base || {}; const base = vnode.attrs["base"] || {};
if (!vnode.state.current) { if (!vnode.state["current"]) {
vnode.state.current = { vnode.state["current"] = {
isNew: !base["_id"], isNew: !base["_id"],
object: Object.assign({}, base) object: Object.assign({}, base)
}; };
} }
const current = vnode.state.current; const current = vnode.state["current"];
const form = []; const form = [];
let focused = false; let focused = false;
@@ -126,27 +129,29 @@ export default function component() {
); );
} }
const submit = m("button.primary", { type: "submit" }, "Save"); const submit = m(
"button.primary",
{ type: "submit" },
"Save"
) as VnodeDOM;
const buttons = [submit]; const buttons = [submit];
if (!current.isNew) { if (!current.isNew) {
buttons.push( buttons.push(m(
m( "button.primary",
"button.primary", {
{ type: "button",
type: "button", title: `Delete ${singular[resource] || resource}`,
title: `Delete ${singular[resource] || resource}`, onclick: e => {
onclick: e => { e.redraw = false;
e.redraw = false; e.target.disabled = true;
e.target.disabled = true; actionHandler("delete", current.object).then(() => {
actionHandler("delete", current.object).then(() => { e.target.disabled = false;
e.target.disabled = false; });
}); }
} },
}, "Delete"
"Delete" ) as VnodeDOM);
)
);
} }
form.push(m(".actions-bar", buttons)); form.push(m(".actions-bar", buttons));
@@ -165,12 +170,12 @@ export default function component() {
// const onsubmit = e.target.onsubmit; // const onsubmit = e.target.onsubmit;
e.preventDefault(); e.preventDefault();
// e.target.onsubmit = null; // e.target.onsubmit = null;
submit.dom.disabled = true; (submit.dom as HTMLFormElement).disabled = true;
// submit.dom.textContent = "Loading ..."; // submit.dom.textContent = "Loading ...";
actionHandler("save", current.object).then(() => { actionHandler("save", current.object).then(() => {
// submit.dom.textContent = "Save"; // submit.dom.textContent = "Save";
// e.target.onsubmit = onsubmit; // e.target.onsubmit = onsubmit;
submit.dom.disabled = false; (submit.dom as HTMLFormElement).disabled = false;
}); });
} }
}, },
@@ -181,4 +186,6 @@ export default function component() {
return m("div.put-form", children); return m("div.put-form", children);
} }
}; };
} };
export default component;
+13 -12
View File
@@ -1,6 +1,5 @@
"use strict";
import config from "./config"; import config from "./config";
import { Expression } from "../lib/types";
const resources = { const resources = {
devices: {}, devices: {},
@@ -34,19 +33,23 @@ const resources = {
} }
}; };
for (const v of Object.values(config.ui.filters)) { for (const v of Object.values(config.ui.filters as {
label: string;
parameter: string;
type: string;
}[])) {
resources.devices[v.label] = { resources.devices[v.label] = {
parameter: v.parameter, parameter: v.parameter,
type: (v.type || "").split(",").map(s => s.trim()) type: (v.type || "").split(",").map(s => s.trim())
}; };
} }
function getLabels(resource) { export function getLabels(resource): string[] {
if (!resources[resource]) return []; if (!resources[resource]) return [];
return Object.keys(resources[resource]); return Object.keys(resources[resource]);
} }
function queryNumber(param, value) { function queryNumber(param, value): Expression {
let op = "="; let op = "=";
for (const o of ["<>", "=", "<=", "<", ">=", ">"]) { for (const o of ["<>", "=", "<=", "<", ">=", ">"]) {
if (value.startsWith(o)) { if (value.startsWith(o)) {
@@ -62,7 +65,7 @@ function queryNumber(param, value) {
return [op, param, v]; return [op, param, v];
} }
function queryTimestamp(param, value) { function queryTimestamp(param, value): Expression {
let op = "="; let op = "=";
for (const o of ["<>", "=", "<=", "<", ">=", ">"]) { for (const o of ["<>", "=", "<=", "<", ">=", ">"]) {
if (value.startsWith(o)) { if (value.startsWith(o)) {
@@ -78,11 +81,11 @@ function queryTimestamp(param, value) {
return [op, param, v]; return [op, param, v];
} }
function queryString(param, value) { function queryString(param, value): Expression {
return ["LIKE", ["FUNC", "LOWER", param], value.toLowerCase()]; return ["LIKE", ["FUNC", "LOWER", param], value.toLowerCase()];
} }
function queryMac(param, value) { function queryMac(param, value): Expression {
value = value.replace(/[^a-f0-9]/gi, "").toLowerCase(); value = value.replace(/[^a-f0-9]/gi, "").toLowerCase();
if (!value) return null; if (!value) return null;
if (value.length === 12) { if (value.length === 12) {
@@ -104,11 +107,11 @@ function queryMac(param, value) {
]; ];
} }
function unpack(resource, label, value) { export function unpack(resource, label, value): Expression {
if (!resources[resource]) return null; if (!resources[resource]) return null;
const type = resources[resource][label].type; const type = resources[resource][label].type;
value = value.trim(); value = value.trim();
const res = ["OR"]; const res: Expression = ["OR"];
if (type.length === 0 || type.includes("number")) { if (type.length === 0 || type.includes("number")) {
const q = queryNumber(resources[resource][label].parameter, value); const q = queryNumber(resources[resource][label].parameter, value);
@@ -134,5 +137,3 @@ function unpack(resource, label, value) {
else if (res.length === 2) return res[1]; else if (res.length === 2) return res[1];
else return res; else return res;
} }
export { getLabels, unpack };
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more