From c3501debd554ff195cc743d56b49ce43a9afad51 Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Mon, 22 Apr 2019 12:00:04 -0700 Subject: [PATCH] Async/await let's go! --- .eslintrc.json | 2 +- bin/genieacs-cwmp | 14 +- bin/genieacs-ext | 4 +- bin/genieacs-fs | 12 +- bin/genieacs-nbi | 14 +- bin/genieacs-ui | 12 +- build/build.ts | 1 + lib/api-functions.ts | 305 ++++----- lib/cache.ts | 178 ++--- lib/cwmp.ts | 1417 ++++++++++++++++++--------------------- lib/db.ts | 816 +++++++++++----------- lib/extensions.ts | 135 ++-- lib/local-cache.ts | 683 +++++++++---------- lib/nbi.ts | 365 ++++++---- lib/sandbox.ts | 138 ++-- lib/session.ts | 1133 +++++++++++++++---------------- lib/soap.ts | 20 +- lib/types.ts | 28 +- lib/ui/api-functions.ts | 245 +++---- lib/ui/api.ts | 11 +- 20 files changed, 2617 insertions(+), 2916 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 7414408..a578335 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,10 +14,10 @@ "handle-callback-err": "error", "prefer-arrow-callback": "error", "curly": ["error", "multi-or-nest", "consistent"], - "consistent-return": ["error", {"treatUndefinedAsUnspecified": true}], "no-buffer-constructor": "error", "prefer-const": ["error", {"destructuring": "all"}], "eqeqeq": ["error", "always", {"null": "ignore"}], + "no-return-await": "error", "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-use-before-define": ["error", { "functions": false }] } diff --git a/bin/genieacs-cwmp b/bin/genieacs-cwmp index 8c5f01b..f3356b3 100755 --- a/bin/genieacs-cwmp +++ b/bin/genieacs-cwmp @@ -27,7 +27,7 @@ function exitWorkerGracefully(): void { } function exitWorkerUngracefully(): void { - extensions.killAll(() => { + extensions.killAll().then(() => { process.exit(1); }); } @@ -97,10 +97,8 @@ if (!cluster.worker) { .catch(exitWorkerUngracefully); }); - db.connect(err => { - if (err) throw err; - cache.connect(err => { - if (err) throw err; + Promise.all([db.connect(), cache.connect()]) + .then(() => { server.start( SERVICE_PORT, SERVICE_ADDRESS, @@ -108,6 +106,10 @@ if (!cluster.worker) { cwmp.listener, cwmp.onConnection ); + }) + .catch(err => { + setTimeout(() => { + throw err; + }); }); - }); } diff --git a/bin/genieacs-ext b/bin/genieacs-ext index a634fcc..b8ac176 100755 --- a/bin/genieacs-ext +++ b/bin/genieacs-ext @@ -59,7 +59,7 @@ function errorToFault(err: Error): Fault { process.on("uncaughtException", err => { const fault = errorToFault(err); jobs.forEach(jobId => { - process.send([jobId, fault]); + process.send([jobId, fault, null]); }); jobs.clear(); process.disconnect(); @@ -81,7 +81,7 @@ process.on("message", message => { code: "ext", message: `No such function '${funcName}' in extension '${fileName}'` }; - process.send([message[0], fault]); + process.send([message[0], fault, null]); return; } diff --git a/bin/genieacs-fs b/bin/genieacs-fs index 1a268b4..d3a34a1 100755 --- a/bin/genieacs-fs +++ b/bin/genieacs-fs @@ -93,11 +93,13 @@ if (!cluster.worker) { .catch(exitWorkerUngracefully); }); - db.connect(err => { - if (err) throw err; - cache.connect(err => { - if (err) throw err; + Promise.all([db.connect(), cache.connect()]) + .then(() => { server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener); + }) + .catch(err => { + setTimeout(() => { + throw err; + }); }); - }); } diff --git a/bin/genieacs-nbi b/bin/genieacs-nbi index e737d1b..f86e410 100755 --- a/bin/genieacs-nbi +++ b/bin/genieacs-nbi @@ -27,7 +27,7 @@ function exitWorkerGracefully(): void { } function exitWorkerUngracefully(): void { - extensions.killAll(() => { + extensions.killAll().then(() => { process.exit(1); }); } @@ -97,11 +97,13 @@ if (!cluster.worker) { .catch(exitWorkerUngracefully); }); - db.connect(err => { - if (err) throw err; - cache.connect(err => { - if (err) throw err; + Promise.all([db.connect(), cache.connect()]) + .then(() => { server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener); + }) + .catch(err => { + setTimeout(() => { + throw err; + }); }); - }); } diff --git a/bin/genieacs-ui b/bin/genieacs-ui index 88225de..c3f9b85 100755 --- a/bin/genieacs-ui +++ b/bin/genieacs-ui @@ -105,11 +105,13 @@ if (!cluster.worker) { listener(req, res); }; - db2.connect(err => { - if (err) throw err; - cache.connect(err => { - if (err) throw err; + Promise.all([db2.connect(), cache.connect()]) + .then(() => { server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener); + }) + .catch(err => { + setTimeout(() => { + throw err; + }); }); - }); } diff --git a/build/build.ts b/build/build.ts index 832f68f..829d8bd 100644 --- a/build/build.ts +++ b/build/build.ts @@ -31,6 +31,7 @@ const externals = [ "https", "zlib", "crypto", + "util", "mongodb", "libxmljs", "vm", diff --git a/lib/api-functions.ts b/lib/api-functions.ts index 0e19bf7..882a93a 100644 --- a/lib/api-functions.ts +++ b/lib/api-functions.ts @@ -29,9 +29,9 @@ import { httpConnectionRequest, udpConnectionRequest } from "./connection-request"; -import { Expression } from "./types"; +import { Expression, Task } from "./types"; -export function connectionRequest(deviceId, callback): void { +export async function connectionRequest(deviceId): Promise { const options = { projection: { _deviceId: 1, @@ -46,133 +46,119 @@ export function connectionRequest(deviceId, callback): void { } }; - db.devicesCollection.findOne({ _id: deviceId }, options, (err, device) => { - if (err) return void callback(err); - if (!device) return void callback(new Error("No such device")); + const device = await db.devicesCollection.findOne({ _id: deviceId }, options); + if (!device) throw new Error("No such device"); - let managementServer, - connectionRequestUrl, + let managementServer, + connectionRequestUrl, + udpConnectionRequestAddress, + username, + password; + if (device.Device) + // TR-181 data model + managementServer = device.Device.ManagementServer; + // TR-098 data model + else managementServer = device.InternetGatewayDevice.ManagementServer; + + if (managementServer.ConnectionRequestURL) + connectionRequestUrl = managementServer.ConnectionRequestURL._value; + if (managementServer.UDPConnectionRequestAddress) { + udpConnectionRequestAddress = + managementServer.UDPConnectionRequestAddress._value; + } + if (managementServer.ConnectionRequestUsername) + username = managementServer.ConnectionRequestUsername._value; + if (managementServer.ConnectionRequestPassword) + password = managementServer.ConnectionRequestPassword._value; + + const context = { + id: device["_id"], + serialNumber: device["_deviceId"]["SerialNumber"], + productClass: device["_deviceId"]["ProductClass"], + oui: device["_deviceId"]["OUI"], + username: username || "", + password: password || "" + }; + + const snapshot = await getCurrentSnapshot(); + const now = Date.now(); + const UDP_CONNECTION_REQUEST_PORT = +getConfig( + snapshot, + "cwmp.udpConnectionRequestAuth", + context, + now + ); + const CONNECTION_REQUEST_TIMEOUT = +getConfig( + snapshot, + "cwmp.connectionRequestTimeout", + context, + now + ); + const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig( + snapshot, + "cwmp.connectionRequestAllowBasicAuth", + context, + now + ); + let authExp: Expression = getConfigExpression( + snapshot, + "cwmp.connectionRequestAuth" + ); + + if (authExp === undefined) { + authExp = [ + "FUNC", + "AUTH", + ["PARAM", "username"], + ["PARAM", "password"] + ] as Expression; + } + + let udpProm; + if (udpConnectionRequestAddress) { + udpProm = udpConnectionRequest( udpConnectionRequestAddress, - username, - password; - if (device.Device) - // TR-181 data model - managementServer = device.Device.ManagementServer; - // TR-098 data model - else managementServer = device.InternetGatewayDevice.ManagementServer; - - if (managementServer.ConnectionRequestURL) - connectionRequestUrl = managementServer.ConnectionRequestURL._value; - if (managementServer.UDPConnectionRequestAddress) { - udpConnectionRequestAddress = - managementServer.UDPConnectionRequestAddress._value; - } - if (managementServer.ConnectionRequestUsername) - username = managementServer.ConnectionRequestUsername._value; - if (managementServer.ConnectionRequestPassword) - password = managementServer.ConnectionRequestPassword._value; - - const context = { - id: device["_id"], - serialNumber: device["_deviceId"]["SerialNumber"], - productClass: device["_deviceId"]["ProductClass"], - oui: device["_deviceId"]["OUI"], - username: username || "", - password: password || "" - }; - - getCurrentSnapshot() - .then(snapshot => { - const now = Date.now(); - const UDP_CONNECTION_REQUEST_PORT = +getConfig( - snapshot, - "cwmp.udpConnectionRequestAuth", - context, - now - ); - const CONNECTION_REQUEST_TIMEOUT = +getConfig( - snapshot, - "cwmp.connectionRequestTimeout", - context, - now - ); - const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig( - snapshot, - "cwmp.connectionRequestAllowBasicAuth", - context, - now - ); - let authExp: Expression = getConfigExpression( - snapshot, - "cwmp.connectionRequestAuth" - ); - - if (authExp === undefined) { - authExp = [ - "FUNC", - "AUTH", - ["PARAM", "username"], - ["PARAM", "password"] - ] as Expression; - } - - let udpProm; - if (udpConnectionRequestAddress) { - udpProm = udpConnectionRequest( - udpConnectionRequestAddress, - authExp, - db.configCollection, - UDP_CONNECTION_REQUEST_PORT - ); - } - - httpConnectionRequest( - connectionRequestUrl, - authExp, - context, - CONNECTION_REQUEST_ALLOW_BASIC_AUTH, - CONNECTION_REQUEST_TIMEOUT - ) - .then(callback) - .catch(err => { - if (udpProm) return void udpProm.then(callback).catch(callback); - callback(err); - }); - }) - .catch(callback); - }); -} - -export function watchTask(deviceId, taskId, timeout, callback): void { - setTimeout(() => { - db.tasksCollection.findOne( - { _id: taskId }, - { projection: { _id: 1 } }, - (err, task) => { - if (err) return void callback(err); - - if (!task) return void callback(null, "completed"); - - const q = { _id: `${deviceId}:task_${taskId}` }; - db.faultsCollection.findOne( - q, - { projection: { _id: 1 } }, - (err, fault) => { - if (err) return void callback(err); - - if (fault) return void callback(null, "fault"); - - if ((timeout -= 500) <= 0) return void callback(null, "timeout"); - - watchTask(deviceId, taskId, timeout, callback); - } - ); - } + authExp, + db.configCollection, + UDP_CONNECTION_REQUEST_PORT ); - }, 500); + } + + try { + await httpConnectionRequest( + connectionRequestUrl, + authExp, + context, + CONNECTION_REQUEST_ALLOW_BASIC_AUTH, + CONNECTION_REQUEST_TIMEOUT + ); + } catch (err) { + if (!udpProm) throw err; + await udpProm; + } } -function sanitizeTask(task, callback): void { +export async function watchTask(deviceId, taskId, timeout): Promise { + await new Promise(resolve => setTimeout(resolve, 500)); + + const task = await db.tasksCollection.findOne( + { _id: taskId }, + { projection: { _id: 1 } } + ); + if (!task) return "completed"; + + const q = { _id: `${deviceId}:task_${taskId}` }; + const fault = await db.faultsCollection.findOne(q, { + projection: { _id: 1 } + }); + if (fault) return "fault"; + + if ((timeout -= 500) <= 0) return "timeout"; + + return watchTask(deviceId, taskId, timeout); +} + +function sanitizeTask(task): void { task.timestamp = new Date(task.timestamp || Date.now()); if (task.expiry) { if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry)) @@ -180,55 +166,40 @@ function sanitizeTask(task, callback): void { else task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000); } - callback(task); + return task; } -export function insertTasks(tasks, callback): void { +export async function insertTasks(tasks): Promise { if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE) tasks = [tasks]; - else if (!tasks || tasks.length === 0) - return void callback(null, tasks || []); + else if (!tasks || tasks.length === 0) return tasks || []; - let counter = tasks.length; for (const task of tasks) { - sanitizeTask(task, t => { - if (t.uniqueKey) { - db.tasksCollection.deleteOne( - { device: t.device, uniqueKey: t.uniqueKey }, - () => {} - ); - } - - if (--counter === 0) db.tasksCollection.insertMany(tasks, callback); - }); + sanitizeTask(task); + if (task.uniqueKey) { + await db.tasksCollection.deleteOne({ + device: task.device, + uniqueKey: task.uniqueKey + }); + } } + await db.tasksCollection.insertMany(tasks); + return tasks; } -export function deleteDevice(deviceId, callback): void { - db.tasksCollection.deleteMany({ device: deviceId }, err => { - if (err) return void callback(err); - db.devicesCollection.deleteOne({ _id: deviceId }, err => { - if (err) return void callback(err); - db.faultsCollection.deleteMany( - { - _id: { - $regex: `^${common.escapeRegExp(deviceId)}\\:` - } - }, - err => { - if (err) return void callback(err); - db.operationsCollection.deleteMany( - { - _id: { - $regex: `^${common.escapeRegExp(deviceId)}\\:` - } - }, - err => { - if (err) return void callback(err); - cache.del(`${deviceId}_tasks_faults_operations`, callback); - } - ); - } - ); - }); - }); +export async function deleteDevice(deviceId): Promise { + await Promise.all([ + db.tasksCollection.deleteMany({ device: deviceId }), + db.devicesCollection.deleteOne({ _id: deviceId }), + db.faultsCollection.deleteMany({ + _id: { + $regex: `^${common.escapeRegExp(deviceId)}\\:` + } + }), + db.operationsCollection.deleteMany({ + _id: { + $regex: `^${common.escapeRegExp(deviceId)}\\:` + } + }), + cache.del(`${deviceId}_tasks_faults_operations`) + ]); } diff --git a/lib/cache.ts b/lib/cache.ts index c9ec7c7..d64e4e7 100644 --- a/lib/cache.ts +++ b/lib/cache.ts @@ -17,162 +17,124 @@ * along with GenieACS. If not, see . */ -import { MongoClient } from "mongodb"; +import { MongoClient, Collection } from "mongodb"; import * as config from "./config"; const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL"); -let mongoClient; -let mongoCollection; +let mongoClient: MongoClient; +let mongoCollection: Collection; let mongoTimeOffset = 0; -export function connect(callback): void { +export async function connect(): Promise { const MONGODB_CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL"); - - MongoClient.connect( - MONGODB_CONNECTION_URL, - { useNewUrlParser: true }, - (err, client) => { - if (err) return void callback(err); - - const db = client.db(); - mongoClient = client; - mongoCollection = db.collection("cache"); - mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }); - - const now = Date.now(); - db.command({ hostInfo: 1 }, (err, res) => { - if (err) return void callback(err); - mongoTimeOffset = res.system.currentTime.getTime() - now; - callback(); - }); - } - ); + mongoClient = await MongoClient.connect(MONGODB_CONNECTION_URL, { + useNewUrlParser: true + }); + const db = mongoClient.db(); + mongoCollection = db.collection("cache"); + await mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }); + const now = Date.now(); + const res = await db.command({ hostInfo: 1 }); + mongoTimeOffset = res.system.currentTime.getTime() - now; } -export function disconnect(): void { - if (mongoClient) mongoClient.close(); +export async function disconnect(): Promise { + if (mongoClient) await mongoClient.close(); } -export function get(key, callback): void { +export async function get(key): Promise { const expire = new Date(Date.now() - mongoTimeOffset); if (Array.isArray(key)) { - mongoCollection.find({ _id: { $in: key } }).toArray((err, res) => { - if (err) return void callback(err); + const res = await mongoCollection.find({ _id: { $in: key } }).toArray(); - const indices = {}; - key.forEach((v, i) => { - indices[v] = i; - }); - - const values = []; - res.forEach(r => { - if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"]; - }); - callback(null, values); + const indices = {}; + key.forEach((v, i) => { + indices[v] = i; }); + + const values = []; + res.forEach(r => { + if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"]; + }); + + return values; } else { - mongoCollection.findOne({ _id: { $in: [key] } }, (err, res) => { - if (err || !res) return void callback(err); - - if (res["expire"] > expire) return void callback(null, res["value"]); - - callback(); - }); + const res = await mongoCollection.findOne({ _id: { $in: [key] } }); + if (res && res["expire"] > expire) return res["value"]; + return res; } } -export function del(key, callback): void { +export async function del(key): Promise { if (Array.isArray(key)) - mongoCollection.deleteMany({ _id: { $in: key } }, callback); - else mongoCollection.deleteOne({ _id: key }, callback); + await mongoCollection.deleteMany({ _id: { $in: key } }); + else await mongoCollection.deleteOne({ _id: key }); } -export function set( +export async function set( key: string, value: string | number, - callback: (err?: Error) => void -): void; -export function set( - key: string, - value: string | number, - ttl: number, - callback: (err?: Error) => void -): void; -export function set( - key: string, - value: string | number, - ttl: number | ((err?: Error) => void), - callback?: (err?: Error) => void -): void { - if (!callback && typeof ttl === "function") { - callback = ttl; - ttl = null; - } - - if (!ttl) ttl = MAX_CACHE_TTL; - - const expire = new Date( - Date.now() - mongoTimeOffset + (ttl as number) * 1000 - ); - mongoCollection.replaceOne( + ttl: number = MAX_CACHE_TTL +): Promise { + const expire = new Date(Date.now() - mongoTimeOffset + ttl * 1000); + await mongoCollection.replaceOne( { _id: key }, { _id: key, value: value, expire: expire }, - { upsert: true }, - callback + { upsert: true } ); } -export function pop(key, callback): void { - mongoCollection.findOneAndDelete({ _id: key }, (err, res) => { - if (err || !res["value"]) return void callback(err); +export async function pop(key): Promise { + const res = await mongoCollection.findOneAndDelete({ _id: key }); - if (res["value"]["expire"] > new Date(Date.now() - mongoTimeOffset)) - return void callback(null, res["value"]["value"]); + if ( + res && + res["value"] && + +res["value"]["expire"] - (Date.now() - mongoTimeOffset) + ) + return res["value"]["value"]; - callback(); - }); + return null; } -export function lock(lockName, ttl, callback): void { +export async function lock(lockName, ttl): Promise { const token = Math.random() .toString(36) .slice(2); - function unlockOrExtend(extendTtl): void { + async function unlockOrExtend(extendTtl): Promise { if (!extendTtl) { - mongoCollection.deleteOne({ _id: lockName, value: token }, (err, res) => { - if (err || res["result"]["n"] !== 1) - throw err || new Error("Lock expired"); + const res = await mongoCollection.deleteOne({ + _id: lockName, + value: token }); + if (res["result"]["n"] !== 1) throw new Error("Lock expired"); } else { const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000); - mongoCollection.updateOne( + const res = await mongoCollection.updateOne( { _id: lockName, value: token }, - { expire: expire }, - (err, res) => { - if (err || res["result"]["n"] !== 1) - throw err || new Error("Lock expired"); - } + { expire: expire } ); + if (res["result"]["n"] !== 1) throw new Error("Lock expired"); } } const expireTest = new Date(Date.now() - mongoTimeOffset); const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000); - mongoCollection.updateOne( - { _id: lockName, expire: { $lte: expireTest } }, - { $set: { value: token, expire: expireSet } }, - { upsert: true }, - err => { - if (err && err.code === 11000) { - return setTimeout(() => { - lock(lockName, ttl, callback); - }, 200); - } - - return callback(err, unlockOrExtend); + try { + await mongoCollection.updateOne( + { _id: lockName, expire: { $lte: expireTest } }, + { $set: { value: token, expire: expireSet } }, + { upsert: true } + ); + } catch (err) { + if (err && err.code === 11000) { + await new Promise(resolve => setTimeout(resolve, 200)); + return lock(lockName, ttl); } - ); + } + + return unlockOrExtend; } diff --git a/lib/cwmp.ts b/lib/cwmp.ts index b9f8b73..6ff3fa2 100644 --- a/lib/cwmp.ts +++ b/lib/cwmp.ts @@ -41,8 +41,16 @@ import { SessionFault, Operation, Fault, - Expression + Expression, + Task, + SoapMessage } from "./types"; +import { IncomingMessage, ServerResponse } from "http"; +import { Readable } from "stream"; +import { promisify } from "util"; + +const gzipPromisified = promisify(zlib.gzip); +const deflatePromisified = promisify(zlib.deflate); const MAX_CYCLES = 4; const MAX_CONCURRENT_REQUESTS = +config.get("MAX_CONCURRENT_REQUESTS"); @@ -56,16 +64,6 @@ const stats = { initiatedSessions: 0 }; -function throwError(err, httpResponse?): never { - if (httpResponse) { - currentSessions.delete(httpResponse.connection); - httpResponse.writeHead(500, { Connection: "close" }); - httpResponse.end(`${err.name}: ${err.message}`); - stats.concurrentRequests -= 1; - } - throw err; -} - function authenticate(sessionContext: SessionContext): boolean { const authExpression: Expression = localCache.getConfigExpression( sessionContext.cacheSnapshot, @@ -106,11 +104,11 @@ function authenticate(sessionContext: SessionContext): boolean { return false; } -function writeResponse( +async function writeResponse( sessionContext: SessionContext, res, close = false -): void { +): Promise { // Close connection after last request in session if (close) res.headers["Connection"] = "close"; @@ -122,38 +120,12 @@ function writeResponse( res.data + "\n\n"; fs.appendFile(`./debug/${sessionContext.deviceId}.dump`, dump, err => { - if (err) throwError(err); + if (err) throw err; }); } - function finish(): void { - stats.concurrentRequests -= 1; - if (sessionContext.httpRequest.connection.destroyed) { - logger.accessError({ - sessionContext: sessionContext, - message: "Connection dropped" - }); - } else if (close) { - endSession(sessionContext, (err, isNew) => { - if (err) return void throwError(err); + let data = res.data; - if (isNew) { - logger.accessInfo({ - sessionContext: sessionContext, - message: "New device registered" - }); - } - }); - } else { - sessionContext.lastActivity = Date.now(); - currentSessions.set( - sessionContext.httpRequest.connection, - sessionContext - ); - } - } - - let compress; // Respond using the same content-encoding as the request if ( sessionContext.httpRequest.headers["content-encoding"] && @@ -162,28 +134,34 @@ function writeResponse( switch (sessionContext.httpRequest.headers["content-encoding"]) { case "gzip": res.headers["Content-Encoding"] = "gzip"; - compress = zlib.gzip; + data = await gzipPromisified(data); break; case "deflate": res.headers["Content-Encoding"] = "deflate"; - compress = zlib.deflate; + data = await deflatePromisified(data); } } - if (compress) { - compress(res.data, (err, data) => { - if (err) return void throwError(err, sessionContext.httpResponse); + res.headers["Content-Length"] = data.length; + sessionContext.httpResponse.writeHead(res.code, res.headers); + sessionContext.httpResponse.end(data); - res.headers["Content-Length"] = data.length; - sessionContext.httpResponse.writeHead(res.code, res.headers); - sessionContext.httpResponse.end(data); - finish(); + if (sessionContext.httpRequest.connection.destroyed) { + logger.accessError({ + sessionContext: sessionContext, + message: "Connection dropped" }); + } else if (close) { + const isNew = await endSession(sessionContext); + if (isNew) { + logger.accessInfo({ + sessionContext: sessionContext, + message: "New device registered" + }); + } } else { - res.headers["Content-Length"] = res.data.length; - sessionContext.httpResponse.writeHead(res.code, res.headers); - sessionContext.httpResponse.end(res.data); - finish(); + sessionContext.lastActivity = Date.now(); + currentSessions.set(sessionContext.httpRequest.connection, sessionContext); } } @@ -257,69 +235,64 @@ function recordFault( session.clearProvisions(sessionContext); } -function inform(sessionContext, rpc): void { - session.inform(sessionContext, rpc.cpeRequest, (err, acsResponse) => { - if (err) return void throwError(err, sessionContext.httpResponse); - sessionContext.state = 1; +async function inform(sessionContext: SessionContext, rpc): Promise { + const acsResponse = await session.inform(sessionContext, rpc.cpeRequest); + sessionContext.state = 1; - const res = soap.response({ - id: rpc.id, - acsResponse: acsResponse, - cwmpVersion: sessionContext.cwmpVersion - }); - - const cookiesPath = localCache.getConfig( - sessionContext.cacheSnapshot, - "cwmp.cookiesPath", - sessionContext.configContext - ); - - if (cookiesPath) { - res.headers["Set-Cookie"] = `session=${ - sessionContext.sessionId - }; Path=${cookiesPath}`; - } else { - res.headers["Set-Cookie"] = `session=${sessionContext.sessionId}`; - } - - writeResponse(sessionContext, res); + const res = soap.response({ + id: rpc.id, + acsResponse: acsResponse, + cwmpVersion: sessionContext.cwmpVersion }); + + const cookiesPath = localCache.getConfig( + sessionContext.cacheSnapshot, + "cwmp.cookiesPath", + sessionContext.configContext + ); + + if (cookiesPath) { + res.headers["Set-Cookie"] = `session=${ + sessionContext.sessionId + }; Path=${cookiesPath}`; + } else { + res.headers["Set-Cookie"] = `session=${sessionContext.sessionId}`; + } + + return writeResponse(sessionContext, res); } -function transferComplete(sessionContext, rpc): void { - session.transferComplete( +async function transferComplete(sessionContext, rpc): Promise { + const { acsResponse, operation, fault } = await session.transferComplete( sessionContext, - rpc.cpeRequest, - (err, acsResponse, operation, fault) => { - if (err) return void throwError(err, sessionContext.httpResponse); - - if (!operation) { - logger.accessWarn({ - sessionContext: sessionContext, - message: "Unrecognized command key", - rpc: rpc - }); - } - - if (fault) { - Object.assign(sessionContext.retries, operation.retries); - recordFault( - sessionContext, - fault, - operation.provisions, - operation.channels - ); - } - - const res = soap.response({ - id: rpc.id, - acsResponse: acsResponse, - cwmpVersion: sessionContext.cwmpVersion - }); - - writeResponse(sessionContext, res); - } + rpc.cpeRequest ); + + if (!operation) { + logger.accessWarn({ + sessionContext: sessionContext, + message: "Unrecognized command key", + rpc: rpc + }); + } + + if (fault) { + Object.assign(sessionContext.retries, operation.retries); + recordFault( + sessionContext, + fault, + operation.provisions, + operation.channels + ); + } + + const res = soap.response({ + id: rpc.id, + acsResponse: acsResponse, + cwmpVersion: sessionContext.cwmpVersion + }); + + return writeResponse(sessionContext, res); } // Append provisions and remove duplicates @@ -354,7 +327,7 @@ function appendProvisions(original, toAppend): boolean { return modified; } -function applyPresets(sessionContext: SessionContext): void { +async function applyPresets(sessionContext: SessionContext): Promise { const deviceData = sessionContext.deviceData; const presets = localCache.getPresets(sessionContext.cacheSnapshot); @@ -440,358 +413,309 @@ function applyPresets(sessionContext: SessionContext): void { defer: true })); - session.rpcRequest( + const { fault: flt, rpcId: reqId, rpc: acsReq } = await session.rpcRequest( sessionContext, - declarations, - (err, flt, reqId, acsReq) => { - if (err) return void throwError(err, sessionContext.httpResponse); - - if (flt) { - recordFault(sessionContext, flt); - session.clearProvisions(sessionContext); - return void applyPresets(sessionContext); - } - - if (acsReq) return void sendAcsRequest(sessionContext, reqId, acsReq); - - session.clearProvisions(sessionContext); - - const parameterValues = {}; - for (const [k, v] of Object.entries(parameters)) { - const unpacked = device.unpack(deviceData, v); - if (!unpacked.length) continue; - const attrs = deviceData.attributes.get(unpacked[0]); - if (attrs && attrs.value && attrs.value[1]) - parameterValues[k] = attrs.value[1][0]; - } - - if (whiteList != null) - session.addProvisions(sessionContext, whiteList, whiteListProvisions); - - const appendProvisionsToFaults = {}; - const now = Date.now(); - for (const p of filteredPresets) { - if (evaluate(p.precondition, parameterValues, now)) { - if (blackList[p.channel] === 2) { - appendProvisionsToFaults[p.channel] = ( - appendProvisionsToFaults[p.channel] || [] - ).concat(p.provisions); - } else { - session.addProvisions(sessionContext, p.channel, p.provisions); - } - } - } - - for (const [channel, provisions] of Object.entries( - appendProvisionsToFaults - )) { - if ( - appendProvisions( - sessionContext.faults[channel].provisions, - provisions - ) - ) { - if (!sessionContext.faultsTouched) sessionContext.faultsTouched = {}; - sessionContext.faultsTouched[channel] = true; - } - } - - // Don't increment when processing a single channel (e.g. after fault) - if (whiteList == null) - sessionContext.presetCycles = (sessionContext.presetCycles || 0) + 1; - - if (sessionContext.presetCycles > MAX_CYCLES) { - const fault = { - code: "preset_loop", - message: "The presets are stuck in an endless configuration loop", - timestamp: sessionContext.timestamp - }; - recordFault(sessionContext, fault); - // No need to save retryNow - for (const f of Object.values(sessionContext.faults)) delete f.retryNow; - session.clearProvisions(sessionContext); - return void sendAcsRequest(sessionContext); - } - - deviceData.timestamps.dirty = 0; - deviceData.attributes.dirty = 0; - session.rpcRequest(sessionContext, null, (err, fault, id, acsRequest) => { - if (err) return void throwError(err, sessionContext.httpResponse); - - if (fault) { - recordFault(sessionContext, fault); - session.clearProvisions(sessionContext); - return void applyPresets(sessionContext); - } - - if (!acsRequest) { - for (const channel of Object.keys(sessionContext.channels)) { - if (sessionContext.faults[channel]) { - delete sessionContext.faults[channel]; - if (!sessionContext.faultsTouched) - sessionContext.faultsTouched = {}; - sessionContext.faultsTouched[channel] = true; - } - } - - if (whiteList != null) return void applyPresets(sessionContext); - - if ( - sessionContext.deviceData.timestamps.dirty > 1 || - sessionContext.deviceData.attributes.dirty > 1 - ) - return void applyPresets(sessionContext); - } - - sendAcsRequest(sessionContext, id, acsRequest); - }); - } + declarations ); -} - -function nextRpc(sessionContext: SessionContext): void { - session.rpcRequest(sessionContext, null, (err, fault, id, acsRequest) => { - if (err) return void throwError(err, sessionContext.httpResponse); - - if (fault) { - recordFault(sessionContext, fault); - session.clearProvisions(sessionContext); - return void nextRpc(sessionContext); - } - - if (acsRequest) return void sendAcsRequest(sessionContext, id, acsRequest); - - for (const [channel, flags] of Object.entries(sessionContext.channels)) { - if (flags && sessionContext.faults[channel]) { - delete sessionContext.faults[channel]; - if (!sessionContext.faultsTouched) sessionContext.faultsTouched = {}; - - sessionContext.faultsTouched[channel] = true; - } - if (channel.startsWith("task_")) { - const taskId = channel.slice(5); - if (!sessionContext.doneTasks) sessionContext.doneTasks = []; - sessionContext.doneTasks.push(taskId); - - for (let j = 0; j < sessionContext.tasks.length; ++j) { - if (sessionContext.tasks[j]._id === taskId) { - sessionContext.tasks.splice(j, 1); - break; - } - } - } - } + if (flt) { + recordFault(sessionContext, flt); session.clearProvisions(sessionContext); + return applyPresets(sessionContext); + } - // Clear expired tasks - sessionContext.tasks = sessionContext.tasks.filter(task => { - if (!(task.expiry <= sessionContext.timestamp)) return true; + if (acsReq) return sendAcsRequest(sessionContext, reqId, acsReq); - logger.accessInfo({ - sessionContext: sessionContext, - message: "Task expired", - task: task - }); + session.clearProvisions(sessionContext); - if (!sessionContext.doneTasks) sessionContext.doneTasks = []; - sessionContext.doneTasks.push(task._id); + const parameterValues = {}; + for (const [k, v] of Object.entries(parameters)) { + const unpacked = device.unpack(deviceData, v); + if (!unpacked.length) continue; + const attrs = deviceData.attributes.get(unpacked[0]); + if (attrs && attrs.value && attrs.value[1]) + parameterValues[k] = attrs.value[1][0]; + } - const channel = `task_${task._id}`; + if (whiteList != null) + session.addProvisions(sessionContext, whiteList, whiteListProvisions); + + const appendProvisionsToFaults = {}; + const now = Date.now(); + for (const p of filteredPresets) { + if (evaluate(p.precondition, parameterValues, now)) { + if (blackList[p.channel] === 2) { + appendProvisionsToFaults[p.channel] = ( + appendProvisionsToFaults[p.channel] || [] + ).concat(p.provisions); + } else { + session.addProvisions(sessionContext, p.channel, p.provisions); + } + } + } + + for (const [channel, provisions] of Object.entries( + appendProvisionsToFaults + )) { + if ( + appendProvisions(sessionContext.faults[channel].provisions, provisions) + ) { + if (!sessionContext.faultsTouched) sessionContext.faultsTouched = {}; + sessionContext.faultsTouched[channel] = true; + } + } + + // Don't increment when processing a single channel (e.g. after fault) + if (whiteList == null) + sessionContext.presetCycles = (sessionContext.presetCycles || 0) + 1; + + if (sessionContext.presetCycles > MAX_CYCLES) { + const fault = { + code: "preset_loop", + message: "The presets are stuck in an endless configuration loop", + timestamp: sessionContext.timestamp + }; + recordFault(sessionContext, fault); + // No need to save retryNow + for (const f of Object.values(sessionContext.faults)) delete f.retryNow; + session.clearProvisions(sessionContext); + return sendAcsRequest(sessionContext); + } + + deviceData.timestamps.dirty = 0; + deviceData.attributes.dirty = 0; + const { fault: fault, rpcId: id, rpc: acsRequest } = await session.rpcRequest( + sessionContext, + null + ); + + if (fault) { + recordFault(sessionContext, fault); + session.clearProvisions(sessionContext); + return applyPresets(sessionContext); + } + + if (!acsRequest) { + for (const channel of Object.keys(sessionContext.channels)) { if (sessionContext.faults[channel]) { delete sessionContext.faults[channel]; if (!sessionContext.faultsTouched) sessionContext.faultsTouched = {}; sessionContext.faultsTouched[channel] = true; } - - return false; - }); - - const task = sessionContext.tasks.find( - t => !sessionContext.faults[`task_${t._id}`] - ); - - if (!task) return void applyPresets(sessionContext); - - let alias; - - switch (task.name) { - case "getParameterValues": - // Set channel in case params array is empty - sessionContext.channels[`task_${task._id}`] = 0; - for (const p of task.parameterNames) { - session.addProvisions(sessionContext, `task_${task._id}`, [ - ["refresh", p] - ]); - } - - break; - case "setParameterValues": - // Set channel in case params array is empty - sessionContext.channels[`task_${task._id}`] = 0; - for (const p of task.parameterValues) { - session.addProvisions(sessionContext, `task_${task._id}`, [ - ["value", p[0], p[1]] - ]); - } - - break; - case "refreshObject": - session.addProvisions(sessionContext, `task_${task._id}`, [ - ["refresh", task.objectName] - ]); - break; - case "reboot": - session.addProvisions(sessionContext, `task_${task._id}`, [["reboot"]]); - break; - case "factoryReset": - session.addProvisions(sessionContext, `task_${task._id}`, [["reset"]]); - break; - case "download": - session.addProvisions(sessionContext, `task_${task._id}`, [ - ["download", task.fileType, task.fileName, task.targetFileName] - ]); - break; - case "addObject": - alias = (task.parameterValues || []) - .map(p => `${p[0]}:${JSON.stringify(p[1])}`) - .join(","); - session.addProvisions(sessionContext, `task_${task._id}`, [ - ["instances", `${task.objectName}.[${alias}]`, "+1"] - ]); - break; - case "deleteObject": - session.addProvisions(sessionContext, `task_${task._id}`, [ - ["instances", task.objectName, 0] - ]); - break; - default: - return void throwError( - new Error("Task name not recognized"), - sessionContext.httpResponse - ); } - nextRpc(sessionContext); - }); + + if (whiteList != null) return applyPresets(sessionContext); + + if ( + sessionContext.deviceData.timestamps.dirty > 1 || + sessionContext.deviceData.attributes.dirty > 1 + ) + return applyPresets(sessionContext); + } + + return sendAcsRequest(sessionContext, id, acsRequest); } -function endSession(sessionContext: SessionContext, callback): void { - let saveCache = sessionContext.cacheUntil != null; - let counter = 3; +async function nextRpc(sessionContext: SessionContext): Promise { + const { fault: fault, rpcId: id, rpc: acsRequest } = await session.rpcRequest( + sessionContext, + null + ); - counter += 2; - db.saveDevice( - sessionContext.deviceId, - sessionContext.deviceData, - sessionContext.new, - sessionContext.timestamp, - err => { - if (err) { - if (counter & 1) callback(err); - return void (counter = 0); - } - if ((counter -= 2) === 1) callback(null, sessionContext.new); + if (fault) { + recordFault(sessionContext, fault); + session.clearProvisions(sessionContext); + return nextRpc(sessionContext); + } + + if (acsRequest) return sendAcsRequest(sessionContext, id, acsRequest); + + for (const [channel, flags] of Object.entries(sessionContext.channels)) { + if (flags && sessionContext.faults[channel]) { + delete sessionContext.faults[channel]; + if (!sessionContext.faultsTouched) sessionContext.faultsTouched = {}; + + sessionContext.faultsTouched[channel] = true; } + if (channel.startsWith("task_")) { + const taskId = channel.slice(5); + if (!sessionContext.doneTasks) sessionContext.doneTasks = []; + sessionContext.doneTasks.push(taskId); + + for (let j = 0; j < sessionContext.tasks.length; ++j) { + if (sessionContext.tasks[j]._id === taskId) { + sessionContext.tasks.splice(j, 1); + break; + } + } + } + } + + session.clearProvisions(sessionContext); + + // Clear expired tasks + sessionContext.tasks = sessionContext.tasks.filter(task => { + if (!(task.expiry <= sessionContext.timestamp)) return true; + + logger.accessInfo({ + sessionContext: sessionContext, + message: "Task expired", + task: task + }); + + if (!sessionContext.doneTasks) sessionContext.doneTasks = []; + sessionContext.doneTasks.push(task._id); + + const channel = `task_${task._id}`; + if (sessionContext.faults[channel]) { + delete sessionContext.faults[channel]; + if (!sessionContext.faultsTouched) sessionContext.faultsTouched = {}; + sessionContext.faultsTouched[channel] = true; + } + + return false; + }); + + const task = sessionContext.tasks.find( + t => !sessionContext.faults[`task_${t._id}`] + ); + + if (!task) return applyPresets(sessionContext); + + let alias; + + switch (task.name) { + case "getParameterValues": + // Set channel in case params array is empty + sessionContext.channels[`task_${task._id}`] = 0; + for (const p of task.parameterNames) { + session.addProvisions(sessionContext, `task_${task._id}`, [ + ["refresh", p] + ]); + } + + break; + case "setParameterValues": + // Set channel in case params array is empty + sessionContext.channels[`task_${task._id}`] = 0; + for (const p of task.parameterValues) { + session.addProvisions(sessionContext, `task_${task._id}`, [ + ["value", p[0], p[1]] + ]); + } + + break; + case "refreshObject": + session.addProvisions(sessionContext, `task_${task._id}`, [ + ["refresh", task.objectName] + ]); + break; + case "reboot": + session.addProvisions(sessionContext, `task_${task._id}`, [["reboot"]]); + break; + case "factoryReset": + session.addProvisions(sessionContext, `task_${task._id}`, [["reset"]]); + break; + case "download": + session.addProvisions(sessionContext, `task_${task._id}`, [ + ["download", task.fileType, task.fileName, task.targetFileName] + ]); + break; + case "addObject": + alias = (task.parameterValues || []) + .map(p => `${p[0]}:${JSON.stringify(p[1])}`) + .join(","); + session.addProvisions(sessionContext, `task_${task._id}`, [ + ["instances", `${task.objectName}.[${alias}]`, "+1"] + ]); + break; + case "deleteObject": + session.addProvisions(sessionContext, `task_${task._id}`, [ + ["instances", task.objectName, 0] + ]); + break; + default: + throw new Error("Task name not recognized"); + } + + return nextRpc(sessionContext); +} + +async function endSession(sessionContext: SessionContext): Promise { + let saveCache = sessionContext.cacheUntil != null; + + const promises = []; + + promises.push( + db.saveDevice( + sessionContext.deviceId, + sessionContext.deviceData, + sessionContext.new, + sessionContext.timestamp + ) ); if (sessionContext.operationsTouched) { for (const k of Object.keys(sessionContext.operationsTouched)) { - counter += 2; saveCache = true; if (sessionContext.operations[k]) { - db.saveOperation( - sessionContext.deviceId, - k, - sessionContext.operations[k], - err => { - if (err) { - if (counter & 1) callback(err); - return void (counter = 0); - } - if ((counter -= 2) === 1) callback(null, sessionContext.new); - } + promises.push( + db.saveOperation( + sessionContext.deviceId, + k, + sessionContext.operations[k] + ) ); } else { - db.deleteOperation(sessionContext.deviceId, k, err => { - if (err) { - if (counter & 1) callback(err); - return void (counter = 0); - } - if ((counter -= 2) === 1) callback(null, sessionContext.new); - }); + promises.push(db.deleteOperation(sessionContext.deviceId, k)); } } } if (sessionContext.doneTasks && sessionContext.doneTasks.length) { - counter += 2; saveCache = true; - db.clearTasks(sessionContext.deviceId, sessionContext.doneTasks, err => { - if (err) { - if (counter & 1) callback(err); - return void (counter = 0); - } - if ((counter -= 2) === 1) callback(null, sessionContext.new); - }); + promises.push( + db.clearTasks(sessionContext.deviceId, sessionContext.doneTasks) + ); } if (sessionContext.faultsTouched) { for (const k of Object.keys(sessionContext.faultsTouched)) { - counter += 2; saveCache = true; if (sessionContext.faults[k]) { sessionContext.faults[k].retries = sessionContext.retries[k]; - db.saveFault( - sessionContext.deviceId, - k, - sessionContext.faults[k], - err => { - if (err) { - if (counter & 1) callback(err); - return void (counter = 0); - } - if ((counter -= 2) === 1) callback(null, sessionContext.new); - } + promises.push( + db.saveFault(sessionContext.deviceId, k, sessionContext.faults[k]) ); } else { - db.deleteFault(sessionContext.deviceId, k, err => { - if (err) { - if (counter & 1) callback(err); - return void (counter = 0); - } - if ((counter -= 2) === 1) callback(null, sessionContext.new); - }); + promises.push(db.deleteFault(sessionContext.deviceId, k)); } } } if (saveCache) { - counter += 2; - cacheDueTasksAndFaultsAndOperations( - sessionContext.deviceId, - sessionContext.tasks, - sessionContext.faults, - sessionContext.operations, - sessionContext.cacheUntil, - err => { - if (err) { - if (counter & 1) callback(err); - return void (counter = 0); - } - if ((counter -= 2) === 1) callback(null, sessionContext.new); - } + promises.push( + cacheDueTasksAndFaultsAndOperations( + sessionContext.deviceId, + sessionContext.tasks, + sessionContext.faults, + sessionContext.operations, + sessionContext.cacheUntil + ) ); } - if ((counter -= 2) === 1) callback(null, sessionContext.new); + + await Promise.all(promises); + return sessionContext.new; } -function sendAcsRequest( +async function sendAcsRequest( sessionContext: SessionContext, id?: string, acsRequest?: AcsRequest -): void { +): Promise { if (!acsRequest) - return void writeResponse(sessionContext, soap.response(null), true); + return writeResponse(sessionContext, soap.response(null), true); if (acsRequest.name === "Download") { const downloadRequest = acsRequest as SetAcsRequest; @@ -824,28 +748,23 @@ function sendAcsRequest( }); const res = soap.response(rpc); - writeResponse(sessionContext, res); + return writeResponse(sessionContext, res); } -function getSession( - connection, - sessionId, - callback: (err?: Error, sessionContext?: SessionContext) => void -): void { +async function getSession(connection, sessionId): Promise { const sessionContext = currentSessions.get(connection); if (sessionContext) { currentSessions.delete(connection); - return void callback(null, sessionContext); + return sessionContext; } - if (!sessionId) return void callback(); + if (!sessionId) return null; - setTimeout(() => { - cache.pop(`session_${sessionId}`, (err, sessionContextString) => { - if (err || !sessionContextString) return void callback(err); - session.deserialize(sessionContextString, callback); - }); - }, 100); + await new Promise(resolve => setTimeout(resolve, 100)); + + const sessionContextString = await cache.pop(`session_${sessionId}`); + if (!sessionContextString) return null; + return session.deserialize(sessionContextString); } // When socket closes, store active sessions in cache @@ -854,7 +773,7 @@ export function onConnection(socket): void { // closed, unless we read it at least once (caching?) socket.remoteAddress; - socket.on("close", () => { + socket.on("close", async () => { const sessionContext = currentSessions.get(socket); if (!sessionContext) return; currentSessions.delete(socket); @@ -871,36 +790,24 @@ export function onConnection(socket): void { sessionContext.lastActivity + sessionContext.timeout * 1000 - now; if (timeout <= 0) return void logger.accessError(timeoutMsg); - setTimeout(() => { - cache.get( - `session_${sessionContext.sessionId}`, - (err, sessionContextString) => { - if (err) return void throwError(err); - - if (!sessionContextString) return; - - session.deserialize(sessionContextString, (err, _sessionContext) => { - if (err) return void throwError(err); - if (_sessionContext.lastActivity === lastActivity) - logger.accessError(timeoutMsg); - }); - } + setTimeout(async () => { + const sessionContextString = await cache.get( + `session_${sessionContext.sessionId}` ); + if (!sessionContextString) return; + const _sessionContext = await session.deserialize(sessionContextString); + if (_sessionContext.lastActivity === lastActivity) + logger.accessError(timeoutMsg); }, timeout + 1000).unref(); if (sessionContext.state === 0) return; - session.serialize(sessionContext, (err, sessionContextString) => { - if (err) return void throwError(err); - cache.set( - `session_${sessionContext.sessionId}`, - sessionContextString, - Math.ceil(timeout / 1000) + 3, - err => { - if (err) return void throwError(err); - } - ); - }); + const sessionContextString = await session.serialize(sessionContext); + await cache.set( + `session_${sessionContext.sessionId}`, + sessionContextString, + Math.ceil(timeout / 1000) + 3 + ); }); } @@ -920,89 +827,46 @@ setInterval(() => { stats.initiatedSessions = 0; }, 10000).unref(); -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( +async function getDueTasksAndFaultsAndOperations( deviceId, - timestamp, - callback: GetDueTasksAndFaultsAndOperationsCallback -): void { - cache.get(`${deviceId}_tasks_faults_operations`, (err, res) => { - if (err) return void callback(err); + timestamp +): Promise<{ + tasks: Task[]; + faults: { [channel: string]: SessionFault }; + operations: { [commandKey: string]: Operation }; + ttl: number; +}> { + const res = await cache.get(`${deviceId}_tasks_faults_operations`); + if (res) { + const resParsed = JSON.parse(res); + return { + tasks: resParsed.tasks || [], + faults: resParsed.faults || {}, + operations: resParsed.operations || {}, + ttl: 0 + }; + } - if (res) { - res = JSON.parse(res); - return void callback( - null, - res.tasks || [], - res.faults || {}, - res.operations || {}, - 0 - ); - } - - let faults, tasks, operations, cacheUntil; - - db.getFaults(deviceId, (err, _faults) => { - if (err) { - if (callback) callback(err); - return void (callback = null); - } - faults = _faults; - if (tasks && operations) - callback(null, tasks, faults, operations, cacheUntil); - }); - - db.getDueTasks(deviceId, timestamp, (err, dueTasks, nextTimestamp) => { - if (err) { - if (callback) callback(err); - return void (callback = null); - } - tasks = dueTasks; - cacheUntil = nextTimestamp || 0; - if (faults && operations) - callback(null, tasks, faults, operations, cacheUntil); - }); - - db.getOperations(deviceId, (err, _operations) => { - if (err) { - if (callback) callback(err); - return void (callback = null); - } - operations = _operations; - if (faults && tasks) - callback(null, tasks, faults, operations, cacheUntil); - }); - - if (faults && tasks && operations) - callback(null, tasks, faults, operations, cacheUntil); - }); + const res2 = await Promise.all([ + db.getDueTasks(deviceId, timestamp), + db.getFaults(deviceId), + db.getOperations(deviceId) + ]); + return { + tasks: res2[0][0], + faults: res2[1], + operations: res2[2], + ttl: res2[0][1] || 0 + }; } -function cacheDueTasksAndFaultsAndOperations( +async function cacheDueTasksAndFaultsAndOperations( deviceId, tasks, faults, operations, - cacheUntil, - callback -): void { + cacheUntil +): Promise { const v = { tasks: null, faults: null, @@ -1016,15 +880,14 @@ function cacheDueTasksAndFaultsAndOperations( if (cacheUntil) ttl = Math.trunc((Date.now() - cacheUntil) / 1000); else ttl = config.get("MAX_CACHE_TTL", deviceId); - cache.set( + await cache.set( `${deviceId}_tasks_faults_operations`, JSON.stringify(v), - ttl, - callback + ttl ); } -function reportBadState(sessionContext): void { +function reportBadState(sessionContext: SessionContext): void { logger.accessError({ message: "Bad session state", sessionContext: sessionContext @@ -1032,10 +895,12 @@ function reportBadState(sessionContext): void { currentSessions.delete(sessionContext.httpResponse.connection); sessionContext.httpResponse.writeHead(400, { Connection: "close" }); sessionContext.httpResponse.end("Bad session state"); - stats.concurrentRequests -= 1; } -function processRequest(sessionContext, rpc): void { +async function processRequest( + sessionContext: SessionContext, + rpc: SoapMessage +): Promise { if (rpc.cpeRequest) { if (rpc.cpeRequest.name === "Inform") { if (sessionContext.state !== 0) @@ -1046,7 +911,7 @@ function processRequest(sessionContext, rpc): void { message: "Inform", rpc: rpc }); - inform(sessionContext, rpc); + return inform(sessionContext, rpc); } else if (rpc.cpeRequest.name === "TransferComplete") { if (sessionContext.state !== 1) return void reportBadState(sessionContext); @@ -1056,10 +921,9 @@ function processRequest(sessionContext, rpc): void { message: "CPE request", rpc: rpc }); - transferComplete(sessionContext, rpc); + return transferComplete(sessionContext, rpc); } else if (rpc.cpeRequest.name === "GetRPCMethods") { - if (sessionContext.state !== 1) - return void reportBadState(sessionContext); + if (sessionContext.state !== 1) return reportBadState(sessionContext); logger.accessInfo({ sessionContext: sessionContext, @@ -1074,23 +938,18 @@ function processRequest(sessionContext, rpc): void { }, cwmpVersion: sessionContext.cwmpVersion }); - writeResponse(sessionContext, res); + return writeResponse(sessionContext, res); } else { if (sessionContext.state !== 1) return void reportBadState(sessionContext); - return void throwError( - new Error("ACS method not supported"), - sessionContext.httpResponse - ); + throw new Error("ACS method not supported"); } } else if (rpc.cpeResponse) { if (sessionContext.state !== 2) return void reportBadState(sessionContext); - session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse, err => { - if (err) return void throwError(err, sessionContext.httpResponse); - nextRpc(sessionContext); - }); + await session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse); + return nextRpc(sessionContext); } else if (rpc.cpeFault) { if (sessionContext.state !== 2) return void reportBadState(sessionContext); @@ -1100,40 +959,61 @@ function processRequest(sessionContext, rpc): void { rpc: rpc }); - session.rpcFault(sessionContext, rpc.id, rpc.cpeFault, (err, fault) => { - if (err) return void throwError(err, sessionContext.httpResponse); - if (fault) { - recordFault(sessionContext, fault); - session.clearProvisions(sessionContext); - } - nextRpc(sessionContext); - }); + const fault = await session.rpcFault(sessionContext, rpc.id, rpc.cpeFault); + if (fault) { + recordFault(sessionContext, fault); + session.clearProvisions(sessionContext); + } + return nextRpc(sessionContext); } else { // CPE sent empty response if (sessionContext.state !== 1) return void reportBadState(sessionContext); sessionContext.state = 2; - session.timeoutOperations(sessionContext, (err, faults, operations) => { - if (err) return void throwError(err, sessionContext.httpResponse); + const { faults, operations } = await session.timeoutOperations( + sessionContext + ); - for (const i of faults) { - for (const [k, v] of Object.entries(operations[i].retries)) - sessionContext.retries[k] = v; + for (const [i, f] of faults.entries()) { + for (const [k, v] of Object.entries(operations[i].retries)) + sessionContext.retries[k] = v; - recordFault( - sessionContext, - faults[i], - operations[i].provisions, - operations[i].channels - ); - } + recordFault( + sessionContext, + f, + operations[i].provisions, + operations[i].channels + ); + } - nextRpc(sessionContext); - }); + return nextRpc(sessionContext); } } -export function listener(httpRequest, httpResponse): void { +export function listener( + httpRequest: IncomingMessage, + httpResponse: ServerResponse +): void { + stats.concurrentRequests += 1; + listenerAsync(httpRequest, httpResponse) + .then(() => { + stats.concurrentRequests -= 1; + }) + .catch(err => { + currentSessions.delete(httpResponse.connection); + httpResponse.writeHead(500, { Connection: "close" }); + httpResponse.end(`${err.name}: ${err.message}`); + stats.concurrentRequests -= 1; + setTimeout(() => { + throw err; + }); + }); +} + +async function listenerAsync( + httpRequest: IncomingMessage, + httpResponse: ServerResponse +): Promise { stats.totalRequests += 1; if (httpRequest.method !== "POST") { @@ -1163,7 +1043,7 @@ export function listener(httpRequest, httpResponse): void { return; } - let stream = httpRequest; + let stream: Readable = httpRequest; if (httpRequest.headers["content-encoding"]) { switch (httpRequest.headers["content-encoding"]) { case "gzip": @@ -1179,217 +1059,206 @@ export function listener(httpRequest, httpResponse): void { } } - stats.concurrentRequests += 1; - httpRequest.on("aborted", () => { - stats.concurrentRequests -= 1; - // In some cases event end can be emitted after aborted event - httpRequest.removeAllListeners("end"); - }); + const body = await new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + let bytes = 0; - const chunks = []; - let bytes = 0; - stream.on("data", chunk => { - chunks.push(chunk); - bytes += chunk.length; - }); + stream.on("data", chunk => { + chunks.push(chunk); + bytes += chunk.length; + }); - stream.on("end", () => { - const body = Buffer.allocUnsafe(bytes); - let offset = 0; - for (const chunk of chunks) { - chunk.copy(body, offset, 0, chunk.length); - offset += chunk.length; - } - - function parsedRpc(sessionContext, rpc, parseWarnings): void { - for (const w of parseWarnings) { - w.sessionContext = sessionContext; - w.rpc = rpc; - logger.accessWarn(w); + stream.on("end", () => { + const _body = Buffer.allocUnsafe(bytes); + let offset = 0; + for (const chunk of chunks) { + chunk.copy(_body, offset, 0, chunk.length); + offset += chunk.length; } + resolve(_body); + }); - if (sessionContext.debug) { - const dump = - `# REQUEST ${new Date(Date.now())}\n` + - JSON.stringify(httpRequest.headers) + - "\n" + - body + - "\n\n"; - fs.appendFile(`./debug/${sessionContext.deviceId}.dump`, dump, err => { - if (err) return void throwError(err); - }); - } + stream.on("error", reject); - processRequest(sessionContext, rpc); - } - - getSession(httpRequest.connection, sessionId, (err, sessionContext) => { - if (err) return void throwError(err, httpResponse); - - if (sessionContext) { - sessionContext.httpRequest = httpRequest; - sessionContext.httpResponse = httpResponse; - if ( - sessionContext.sessionId !== sessionId || - sessionContext.lastActivity + sessionContext.timeout * 1000 < - Date.now() - ) { - logger.accessError({ - message: "Invalid session", - sessionContext: sessionContext - }); - - httpResponse.writeHead(400, { Connection: "close" }); - httpResponse.end("Invalid session"); - stats.concurrentRequests -= 1; - return; - } - } else if (stats.concurrentRequests > MAX_CONCURRENT_REQUESTS) { - // Check again just in case device included old session ID - // from the previous session - httpResponse.writeHead(503, { "Retry-after": 60, Connection: "close" }); - httpResponse.end("503 Service Unavailable"); - stats.droppedRequests += 1; - stats.concurrentRequests -= 1; - return; - } - - const parseWarnings = []; - let rpc; - try { - rpc = soap.request( - body, - sessionContext ? sessionContext.cwmpVersion : null, - parseWarnings - ); - } catch (error) { - logger.accessError({ - message: "XML parse error", - parseError: error.message.trim(), - sessionContext: sessionContext || { - httpRequest: httpRequest, - httpResponse: httpResponse - } - }); - httpResponse.writeHead(400, { Connection: "close" }); - httpResponse.end(error.message); - stats.concurrentRequests -= 1; - return; - } - - if (sessionContext) { - if (sessionContext.state === 0 && !authenticate(sessionContext)) { - logger.accessError({ - message: "Authentication failure", - sessionContext: sessionContext - }); - - httpResponse.writeHead(401); - httpResponse.end("Unauthorized"); - currentSessions.set(httpRequest.connection, sessionContext); - stats.concurrentRequests -= 1; - return; - } - return void parsedRpc(sessionContext, rpc, parseWarnings); - } - - if (!(rpc.cpeRequest && rpc.cpeRequest.name === "Inform")) { - logger.accessError({ - message: "Invalid session", - sessionContext: sessionContext || { - httpRequest: httpRequest, - httpResponse: httpResponse - } - }); - httpResponse.writeHead(400, { Connection: "close" }); - httpResponse.end("Invalid session"); - stats.concurrentRequests -= 1; - return; - } - - stats.initiatedSessions += 1; - const deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId); - - localCache.getCurrentSnapshot((err, cacheSnapshot) => { - if (err) return void throwError(err, httpResponse); - - const configContext = { - id: deviceId, - serialNumber: rpc.cpeRequest.deviceId["SerialNumber"], - productClass: rpc.cpeRequest.deviceId["ProductClass"], - oui: rpc.cpeRequest.deviceId["OUI"] - }; - - const sessionTimeout = - rpc.sessionTimeout || - localCache.getConfig( - cacheSnapshot, - "cwmp.sessionTimeout", - configContext - ); - - session.init( - deviceId, - rpc.cwmpVersion, - sessionTimeout, - (err, _sessionContext) => { - if (err) return void throwError(err, httpResponse); - - _sessionContext.cacheSnapshot = cacheSnapshot; - _sessionContext.configContext = configContext; - _sessionContext.debug = !!localCache.getConfig( - cacheSnapshot, - "cwmp.debug", - configContext - ); - - _sessionContext.httpRequest = httpRequest; - _sessionContext.httpResponse = httpResponse; - _sessionContext.sessionId = crypto.randomBytes(8).toString("hex"); - httpRequest.connection.setTimeout(_sessionContext.timeout * 1000); - - getDueTasksAndFaultsAndOperations( - deviceId, - _sessionContext.timestamp, - (err, dueTasks, faults, operations, cacheUntil) => { - if (err) return void throwError(err, httpResponse); - - _sessionContext.tasks = dueTasks; - _sessionContext.operations = operations; - _sessionContext.cacheUntil = cacheUntil; - _sessionContext.faults = faults; - _sessionContext.retries = {}; - for (const [k, v] of Object.entries(_sessionContext.faults)) { - if (v.expiry >= _sessionContext.timestamp) { - // Delete expired faults - delete _sessionContext.faults[k]; - if (!_sessionContext.faultsTouched) - _sessionContext.faultsTouched = {}; - _sessionContext.faultsTouched[k] = true; - } else { - _sessionContext.retries[k] = v.retries; - } - } - - if (!authenticate(_sessionContext)) { - logger.accessError({ - message: "Authentication failure", - sessionContext: _sessionContext - }); - - httpResponse.writeHead(401); - httpResponse.end("Unauthorized"); - currentSessions.set(httpRequest.connection, _sessionContext); - stats.concurrentRequests -= 1; - return; - } - - parsedRpc(_sessionContext, rpc, parseWarnings); - } - ); - } - ); - }); + httpRequest.on("aborted", () => { + resolve(null); }); }); + + // Request aborted + if (!body) return; + + async function parsedRpc(sessionContext, rpc, parseWarnings): Promise { + for (const w of parseWarnings) { + w.sessionContext = sessionContext; + w.rpc = rpc; + logger.accessWarn(w); + } + + if (sessionContext.debug) { + const dump = + `# REQUEST ${new Date(Date.now())}\n` + + JSON.stringify(httpRequest.headers) + + "\n" + + body + + "\n\n"; + fs.appendFile(`./debug/${sessionContext.deviceId}.dump`, dump, err => { + if (err) throw err; + }); + } + + return processRequest(sessionContext, rpc); + } + + const sessionContext = await getSession(httpRequest.connection, sessionId); + + if (sessionContext) { + sessionContext.httpRequest = httpRequest; + sessionContext.httpResponse = httpResponse; + if ( + sessionContext.sessionId !== sessionId || + sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now() + ) { + logger.accessError({ + message: "Invalid session", + sessionContext: sessionContext + }); + + httpResponse.writeHead(400, { Connection: "close" }); + httpResponse.end("Invalid session"); + return; + } + } else if (stats.concurrentRequests > MAX_CONCURRENT_REQUESTS) { + // Check again just in case device included old session ID + // from the previous session + httpResponse.writeHead(503, { "Retry-after": 60, Connection: "close" }); + httpResponse.end("503 Service Unavailable"); + stats.droppedRequests += 1; + return; + } + + const parseWarnings = []; + let rpc; + try { + rpc = soap.request( + body, + sessionContext ? sessionContext.cwmpVersion : null, + parseWarnings + ); + } catch (error) { + logger.accessError({ + message: "XML parse error", + parseError: error.message.trim(), + sessionContext: sessionContext || { + httpRequest: httpRequest, + httpResponse: httpResponse + } + }); + httpResponse.writeHead(400, { Connection: "close" }); + httpResponse.end(error.message); + return; + } + + if (sessionContext) { + if (sessionContext.state === 0 && !authenticate(sessionContext)) { + logger.accessError({ + message: "Authentication failure", + sessionContext: sessionContext + }); + + httpResponse.writeHead(401); + httpResponse.end("Unauthorized"); + currentSessions.set(httpRequest.connection, sessionContext); + return; + } + return parsedRpc(sessionContext, rpc, parseWarnings); + } + + if (!(rpc.cpeRequest && rpc.cpeRequest.name === "Inform")) { + logger.accessError({ + message: "Invalid session", + sessionContext: sessionContext || { + httpRequest: httpRequest, + httpResponse: httpResponse + } + }); + httpResponse.writeHead(400, { Connection: "close" }); + httpResponse.end("Invalid session"); + return; + } + + stats.initiatedSessions += 1; + const deviceId = common.generateDeviceId(rpc.cpeRequest.deviceId); + + const cacheSnapshot = await localCache.getCurrentSnapshot(); + + const configContext = { + id: deviceId, + serialNumber: rpc.cpeRequest.deviceId["SerialNumber"], + productClass: rpc.cpeRequest.deviceId["ProductClass"], + oui: rpc.cpeRequest.deviceId["OUI"] + }; + + const sessionTimeout = + rpc.sessionTimeout || + localCache.getConfig(cacheSnapshot, "cwmp.sessionTimeout", configContext); + + const _sessionContext = session.init( + deviceId, + rpc.cwmpVersion, + sessionTimeout + ); + + _sessionContext.cacheSnapshot = cacheSnapshot; + _sessionContext.configContext = configContext; + _sessionContext.debug = !!localCache.getConfig( + cacheSnapshot, + "cwmp.debug", + configContext + ); + + _sessionContext.httpRequest = httpRequest; + _sessionContext.httpResponse = httpResponse; + _sessionContext.sessionId = crypto.randomBytes(8).toString("hex"); + httpRequest.connection.setTimeout(_sessionContext.timeout * 1000); + + const { + tasks: dueTasks, + faults, + operations, + ttl: cacheUntil + } = await getDueTasksAndFaultsAndOperations( + deviceId, + _sessionContext.timestamp + ); + + _sessionContext.tasks = dueTasks; + _sessionContext.operations = operations; + _sessionContext.cacheUntil = cacheUntil; + _sessionContext.faults = faults; + _sessionContext.retries = {}; + for (const [k, v] of Object.entries(_sessionContext.faults)) { + if (v.expiry >= _sessionContext.timestamp) { + // Delete expired faults + delete _sessionContext.faults[k]; + if (!_sessionContext.faultsTouched) _sessionContext.faultsTouched = {}; + _sessionContext.faultsTouched[k] = true; + } else { + _sessionContext.retries[k] = v.retries; + } + } + + if (!authenticate(_sessionContext)) { + logger.accessError({ + message: "Authentication failure", + sessionContext: _sessionContext + }); + + httpResponse.writeHead(401); + httpResponse.end("Unauthorized"); + currentSessions.set(httpRequest.connection, _sessionContext); + return; + } + + parsedRpc(_sessionContext, rpc, parseWarnings); } diff --git a/lib/db.ts b/lib/db.ts index 01acf48..eef5d76 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -17,59 +17,60 @@ * along with GenieACS. If not, see . */ -import { MongoClient, ObjectID } from "mongodb"; +import { MongoClient, ObjectID, Collection, FindOneOptions } from "mongodb"; import { get } from "./config"; import { escapeRegExp } from "./common"; import { parse } from "./common/expression-parser"; -import { DeviceData, Attributes } from "./types"; +import { + DeviceData, + Attributes, + SessionFault, + Task, + Operation, + Expression +} from "./types"; import Path from "./common/path"; -export let tasksCollection, - devicesCollection, - presetsCollection, - objectsCollection, - provisionsCollection, - virtualParametersCollection, - faultsCollection, - filesCollection, - operationsCollection, - permissionsCollection, - usersCollection, - configCollection; +export let tasksCollection: Collection, + devicesCollection: Collection, + presetsCollection: Collection, + objectsCollection: Collection, + provisionsCollection: Collection, + virtualParametersCollection: Collection, + faultsCollection: Collection, + filesCollection: Collection, + operationsCollection: Collection, + permissionsCollection: Collection, + usersCollection: Collection, + configCollection: Collection; -export let client; +export let client: MongoClient; -export function connect(callback): void { - MongoClient.connect( - "" + get("MONGODB_CONNECTION_URL"), - { useNewUrlParser: true }, - (err, _client) => { - if (err) return void callback(err); - client = _client; - const db = client.db(); +export async function connect(): Promise { + client = await MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), { + useNewUrlParser: true + }); - tasksCollection = db.collection("tasks"); - tasksCollection.createIndex({ device: 1, timestamp: 1 }); + const db = client.db(); - devicesCollection = db.collection("devices"); - presetsCollection = db.collection("presets"); - objectsCollection = db.collection("objects"); - filesCollection = db.collection("fs.files"); - provisionsCollection = db.collection("provisions"); - virtualParametersCollection = db.collection("virtualParameters"); - faultsCollection = db.collection("faults"); - operationsCollection = db.collection("operations"); - permissionsCollection = db.collection("permissions"); - usersCollection = db.collection("users"); - configCollection = db.collection("config"); + tasksCollection = db.collection("tasks"); + await tasksCollection.createIndex({ device: 1, timestamp: 1 }); - callback(); - } - ); + devicesCollection = db.collection("devices"); + presetsCollection = db.collection("presets"); + objectsCollection = db.collection("objects"); + filesCollection = db.collection("fs.files"); + provisionsCollection = db.collection("provisions"); + virtualParametersCollection = db.collection("virtualParameters"); + faultsCollection = db.collection("faults"); + operationsCollection = db.collection("operations"); + permissionsCollection = db.collection("permissions"); + usersCollection = db.collection("users"); + configCollection = db.collection("config"); } -export function disconnect(): void { - if (client) client.close(); +export async function disconnect(): Promise { + if (client) await client.close(); } // Optimize projection by removing overlaps @@ -93,12 +94,11 @@ function optimizeProjection(obj: {}): {} { return obj; } -export function fetchDevice( +export async function fetchDevice( id: string, timestamp: number, - patterns: [Path, number][], - callback -): void { + patterns: [Path, number][] +): Promise<[[Path, number, Attributes?][], [Path, number][]]> { const MAX_DEPTH = +get("MAX_DEPTH", id); if (!patterns || !patterns.length) @@ -273,259 +273,257 @@ export function fetchDevice( } } - let options; + let options: FindOneOptions; if (!projection[""]) { if (!Object.keys(projection).length) options = { projection: { _id: 1 } }; else options = { projection: projection }; } - devicesCollection.findOne({ _id: id }, options, (err, device) => { - if (err || !device) return void callback(err); + const device = await devicesCollection.findOne({ _id: id }, options); + if (!device) return [null, null]; - function storeParams( - obj, - path: string, - pathLength: number, - ts, - descendantsFetched, - projTree - ): void { - let thisFetched = false; - if (descendantsFetched) { - thisFetched = true; - } else if (projection[path.slice(0, -1)]) { - descendantsFetched = true; - if ( - pathLength && - projection[ - path.slice(0, path.lastIndexOf(".", path.length - 2) + 1) + - "_timestamp" - ] - ) { - thisFetched = true; - loaded.push([ - Path.parse(path.slice(0, -1)), - ((1 << (pathLength - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1) - ]); - } else { - loaded.push([ - Path.parse(path.slice(0, -1)), - ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1) - ]); - } - } else if (projection[path + "_writable"]) { - loaded.push([Path.parse(path.slice(0, -1)), 1 << (pathLength - 1)]); + function storeParams( + obj, + path: string, + pathLength: number, + ts, + descendantsFetched, + projTree + ): void { + let thisFetched = false; + if (descendantsFetched) { + thisFetched = true; + } else if (projection[path.slice(0, -1)]) { + descendantsFetched = true; + if ( + pathLength && + projection[ + path.slice(0, path.lastIndexOf(".", path.length - 2) + 1) + + "_timestamp" + ] + ) { thisFetched = true; + loaded.push([ + Path.parse(path.slice(0, -1)), + ((1 << (pathLength - 1)) - 1) ^ ((1 << MAX_DEPTH) - 1) + ]); + } else { + loaded.push([ + Path.parse(path.slice(0, -1)), + ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1) + ]); } + } else if (projection[path + "_writable"]) { + loaded.push([Path.parse(path.slice(0, -1)), 1 << (pathLength - 1)]); + thisFetched = true; + } - if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"]; + if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"]; - // For compatibility with v1.0 database - if (obj["_instance"] && obj["_object"] == null) obj["_object"] = true; + // For compatibility with v1.0 database + if (obj["_instance"] && obj["_object"] == null) obj["_object"] = true; - if (thisFetched) { - const attrs: Attributes = {}; - let t = obj["_timestamp"] || 1; - if (ts > t) t = ts; + if (thisFetched) { + const attrs: Attributes = {}; + let t = obj["_timestamp"] || 1; + if (ts > t) t = ts; - if (obj["_value"] != null) { - attrs.value = [obj["_timestamp"] || 1, [obj["_value"], obj["_type"]]]; - if (obj["_type"] === "xsd:dateTime") - attrs.value[1][0] = +attrs.value[1][0]; + if (obj["_value"] != null) { + attrs.value = [obj["_timestamp"] || 1, [obj["_value"], obj["_type"]]]; + if (obj["_type"] === "xsd:dateTime") + attrs.value[1][0] = +attrs.value[1][0]; - obj["_object"] = false; - } - if (obj["_writable"] != null) - attrs.writable = [ts || 1, obj["_writable"] ? 1 : 0]; - - if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0]; - - res.push([Path.parse(path.slice(0, -1)), t, attrs]); + obj["_object"] = false; } + if (obj["_writable"] != null) + attrs.writable = [ts || 1, obj["_writable"] ? 1 : 0]; - for (const [k, v] of Object.entries(obj)) { - if (!k.startsWith("_")) { - obj["_object"] = true; - storeParams( - v, - path + k + ".", - pathLength + 1, - obj["_timestamp"], - descendantsFetched, - projTree ? projTree[k] : null - ); - if (projTree) delete projTree[k]; - } - } + if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0]; - if (!descendantsFetched) { - if (projTree) { - for (const k of Object.keys(projTree)) { - const p = Path.parse(path + k); - loaded.push([p, ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1)]); - if ((obj["_object"] || !pathLength) && obj["_timestamp"]) - res.push([p, obj["_timestamp"]]); - } - } - } else if ((obj["_object"] || !pathLength) && obj["_timestamp"]) { - res.push([Path.parse(path + "*"), obj["_timestamp"]]); + res.push([Path.parse(path.slice(0, -1)), t, attrs]); + } + + for (const [k, v] of Object.entries(obj)) { + if (!k.startsWith("_")) { + obj["_object"] = true; + storeParams( + v, + path + k + ".", + pathLength + 1, + obj["_timestamp"], + descendantsFetched, + projTree ? projTree[k] : null + ); + if (projTree) delete projTree[k]; } } - for (const [k, v] of Object.entries(device)) { - switch (k) { - case "_lastInform": + if (!descendantsFetched) { + if (projTree) { + for (const k of Object.keys(projTree)) { + const p = Path.parse(path + k); + loaded.push([p, ((1 << pathLength) - 1) ^ ((1 << MAX_DEPTH) - 1)]); + if ((obj["_object"] || !pathLength) && obj["_timestamp"]) + res.push([p, obj["_timestamp"]]); + } + } + } else if ((obj["_object"] || !pathLength) && obj["_timestamp"]) { + res.push([Path.parse(path + "*"), obj["_timestamp"]]); + } + } + + for (const [k, v] of Object.entries(device)) { + switch (k) { + case "_lastInform": + res.push([ + Path.parse("Events.Inform"), + +v, + { + object: [+v, 0], + writable: [+v, 0], + value: [+v, [+v, "xsd:dateTime"]] + } + ]); + delete device[k]; + break; + case "_lastBoot": + res.push([ + Path.parse("Events.1_BOOT"), + +v, + { + object: [+v, 0], + writable: [+v, 0], + value: [+v, [+v, "xsd:dateTime"]] + } + ]); + delete device[k]; + break; + case "_lastBootstrap": + res.push([ + Path.parse("Events.0_BOOTSTRAP"), + +v, + { + object: [+v, 0], + writable: [+v, 0], + value: [+v, [+v, "xsd:dateTime"]] + } + ]); + delete device[k]; + break; + case "_registered": + // Use current timestamp for registered event attribute timestamps + res.push([ + Path.parse("Events.Registered"), + timestamp, + { + object: [timestamp, 0], + writable: [timestamp, 0], + value: [timestamp, [+v, "xsd:dateTime"]] + } + ]); + delete device[k]; + break; + case "_id": + if (projection[""] || projection["_id"]) { res.push([ - Path.parse("Events.Inform"), - +v, - { - object: [+v, 0], - writable: [+v, 0], - value: [+v, [+v, "xsd:dateTime"]] - } - ]); - delete device[k]; - break; - case "_lastBoot": - res.push([ - Path.parse("Events.1_BOOT"), - +v, - { - object: [+v, 0], - writable: [+v, 0], - value: [+v, [+v, "xsd:dateTime"]] - } - ]); - delete device[k]; - break; - case "_lastBootstrap": - res.push([ - Path.parse("Events.0_BOOTSTRAP"), - +v, - { - object: [+v, 0], - writable: [+v, 0], - value: [+v, [+v, "xsd:dateTime"]] - } - ]); - delete device[k]; - break; - case "_registered": - // Use current timestamp for registered event attribute timestamps - res.push([ - Path.parse("Events.Registered"), + Path.parse("DeviceID.ID"), timestamp, { object: [timestamp, 0], writable: [timestamp, 0], - value: [timestamp, [+v, "xsd:dateTime"]] + value: [timestamp, [v as string, "xsd:string"]] } ]); - delete device[k]; - break; - case "_id": - if (projection[""] || projection["_id"]) { - res.push([ - Path.parse("DeviceID.ID"), - timestamp, - { - object: [timestamp, 0], - writable: [timestamp, 0], - value: [timestamp, [v as string, "xsd:string"]] - } - ]); - } + } - delete device[k]; - break; - case "_tags": - if ((v as string[]).length) { - res.push([ - Path.parse("Tags"), - timestamp, - { object: [timestamp, 1], writable: [timestamp, 0] } - ]); - } + delete device[k]; + break; + case "_tags": + if ((v as string[]).length) { + res.push([ + Path.parse("Tags"), + timestamp, + { object: [timestamp, 1], writable: [timestamp, 0] } + ]); + } - for (let t of v as string[]) { - t = t.replace(/[^a-zA-Z0-9-]+/g, "_"); - res.push([ - Path.parse("Tags." + t), - timestamp, - { - object: [timestamp, 0], - writable: [timestamp, 1], - value: [timestamp, [true, "xsd:boolean"]] - } - ]); - } - delete device[k]; - break; - case "_deviceId": - if (v["_Manufacturer"] != null) { - res.push([ - Path.parse("DeviceID.Manufacturer"), - timestamp, - { - object: [timestamp, 0], - writable: [timestamp, 0], - value: [timestamp, [v["_Manufacturer"], "xsd:string"]] - } - ]); - } + for (let t of v as string[]) { + t = t.replace(/[^a-zA-Z0-9-]+/g, "_"); + res.push([ + Path.parse("Tags." + t), + timestamp, + { + object: [timestamp, 0], + writable: [timestamp, 1], + value: [timestamp, [true, "xsd:boolean"]] + } + ]); + } + delete device[k]; + break; + case "_deviceId": + if (v["_Manufacturer"] != null) { + res.push([ + Path.parse("DeviceID.Manufacturer"), + timestamp, + { + object: [timestamp, 0], + writable: [timestamp, 0], + value: [timestamp, [v["_Manufacturer"], "xsd:string"]] + } + ]); + } - if (v["_OUI"] != null) { - res.push([ - Path.parse("DeviceID.OUI"), - timestamp, - { - object: [timestamp, 0], - writable: [timestamp, 0], - value: [timestamp, [v["_OUI"], "xsd:string"]] - } - ]); - } + if (v["_OUI"] != null) { + res.push([ + Path.parse("DeviceID.OUI"), + timestamp, + { + object: [timestamp, 0], + writable: [timestamp, 0], + value: [timestamp, [v["_OUI"], "xsd:string"]] + } + ]); + } - if (v["_ProductClass"] != null) { - res.push([ - Path.parse("DeviceID.ProductClass"), - timestamp, - { - object: [timestamp, 0], - writable: [timestamp, 0], - value: [timestamp, [v["_ProductClass"], "xsd:string"]] - } - ]); - } + if (v["_ProductClass"] != null) { + res.push([ + Path.parse("DeviceID.ProductClass"), + timestamp, + { + object: [timestamp, 0], + writable: [timestamp, 0], + value: [timestamp, [v["_ProductClass"], "xsd:string"]] + } + ]); + } - if (v["_SerialNumber"] != null) { - res.push([ - Path.parse("DeviceID.SerialNumber"), - timestamp, - { - object: [timestamp, 0], - writable: [timestamp, 0], - value: [timestamp, [v["_SerialNumber"], "xsd:string"]] - } - ]); - } + if (v["_SerialNumber"] != null) { + res.push([ + Path.parse("DeviceID.SerialNumber"), + timestamp, + { + object: [timestamp, 0], + writable: [timestamp, 0], + value: [timestamp, [v["_SerialNumber"], "xsd:string"]] + } + ]); + } - delete device[k]; - } + delete device[k]; } + } - storeParams(device, "", 0, 0, false, projectionTree); - callback(null, res, loaded); - }); + storeParams(device, "", 0, 0, false, projectionTree); + return [res, loaded]; } -export function saveDevice( +export async function saveDevice( deviceId: string, deviceData: DeviceData, isNew: boolean, - sessionTimestamp: number, - callback -): void { + sessionTimestamp: number +): Promise { const update = { $set: {}, $unset: {}, $addToSet: {}, $pull: {} }; for (const diff of deviceData.timestamps.diff()) { @@ -737,7 +735,7 @@ export function saveDevice( if (!Object.keys(v).length) delete update[k]; } - if (!Object.keys(update).length) return void callback(); + if (!Object.keys(update).length) return; // Mongo doesn't allow $addToSet and $pull at the same time let update2; @@ -746,52 +744,41 @@ export function saveDevice( delete update["$pull"]; } - devicesCollection.updateOne( - { _id: deviceId }, - update, - { upsert: isNew }, - (err, result) => { - if (err || result.result.n !== 1) { - return void callback( - err || new Error(`Device ${deviceId} not found in database`) - ); - } + const result = await devicesCollection.updateOne({ _id: deviceId }, update, { + upsert: isNew + }); - if (update2) { - return void devicesCollection.updateOne( - { _id: deviceId }, - update2, - callback - ); - } + if (result.result.n !== 1) + throw new Error(`Device ${deviceId} not found in database`); - callback(); - } - ); + if (update2) { + await devicesCollection.updateOne({ _id: deviceId }, update2); + return; + } } -export function getFaults(deviceId, callback): void { - faultsCollection +export async function getFaults( + deviceId +): Promise<{ [channel: string]: SessionFault }> { + const res = await faultsCollection .find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } }) - .toArray((err, res) => { - if (err) return callback(err); + .toArray(); - const faults = {}; - for (const r of res) { - const channel = r._id.slice(deviceId.length + 1); - delete r._id; - delete r.channel; - delete r.device; - r.timestamp = +r.timestamp; - r.provisions = JSON.parse(r.provisions); - faults[channel] = r; - } + const faults: { [channel: string]: SessionFault } = {}; + for (const r of res) { + const channel = r._id.slice(deviceId.length + 1); + delete r._id; + delete r.channel; + delete r.device; + r.timestamp = +r.timestamp; + r.provisions = JSON.parse(r.provisions); + faults[channel] = r; + } - return callback(err, faults); - }); + return faults; } -export function saveFault(deviceId, channel, fault, callback): void { +export async function saveFault(deviceId, channel, fault): Promise { const id = `${deviceId}:${channel}`; fault = Object.assign({}, fault); fault._id = id; @@ -799,87 +786,79 @@ export function saveFault(deviceId, channel, fault, callback): void { fault.channel = channel; fault.timestamp = new Date(fault.timestamp); fault.provisions = JSON.stringify(fault.provisions); - faultsCollection.replaceOne({ _id: id }, fault, { upsert: true }, callback); + await faultsCollection.replaceOne({ _id: id }, fault, { upsert: true }); } -export function deleteFault(deviceId, channel, callback): void { - faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` }, callback); +export async function deleteFault(deviceId, channel): Promise { + await faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` }); } -export function getDueTasks(deviceId, timestamp, callback): void { +export async function getDueTasks( + deviceId, + timestamp +): Promise<[Task[], number]> { const cur = tasksCollection.find({ device: deviceId }).sort(["timestamp"]); - const tasks = []; + const tasks = [] as Task[]; - let f; - cur.next( - (f = (err, task) => { - if (err) return void callback(err); + for await (const task of cur) { + if (task.timestamp) task.timestamp = +task.timestamp; + if (task.expiry) task.expiry = +task.expiry; + if (task.timestamp >= timestamp) return [tasks, +task.timestamp]; + task._id = String(task._id); - if (!task) return void callback(null, tasks, null); + tasks.push(task); - if (task.timestamp) task.timestamp = +task.timestamp; - if (task.expiry) task.expiry = +task.expiry; - if (task.timestamp >= timestamp) - return void callback(null, tasks, +task.timestamp); - task._id = String(task._id); + // For API compatibility + if (task.name === "download" && task.file) { + let q; + if (ObjectID.isValid(task.file)) + q = { _id: { $in: [task.file, new ObjectID(task.file)] } }; + else q = { _id: task.file }; - tasks.push(task); + const res = await filesCollection.find(q).toArray(); - // For API compatibility - if (task.name === "download" && task.file) { - let q; - if (ObjectID.isValid(task.file)) - q = { _id: { $in: [task.file, new ObjectID(task.file)] } }; - else q = { _id: task.file }; + if (res[0]) { + if (!task.fileType) task.fileType = res[0].metadata.fileType; - filesCollection.find(q).toArray((err, res) => { - if (err) return void callback(err); - - if (res[0]) { - if (!task.fileType) task.fileType = res[0].metadata.fileType; - - if (!task.fileName) - task.fileName = res[0].filename || res[0]._id.toString(); - } - - cur.next(f); - }); - } else { - cur.next(f); + if (!task.fileName) + task.fileName = res[0].filename || res[0]._id.toString(); } - }) - ); + } + } + return [tasks, null]; } -export function clearTasks(deviceId, taskIds, callback): void { - tasksCollection.deleteMany( - { _id: { $in: taskIds.map(id => new ObjectID(id)) } }, - callback - ); +export async function clearTasks(deviceId, taskIds): Promise { + await tasksCollection.deleteMany({ + _id: { $in: taskIds.map(id => new ObjectID(id)) } + }); } -export function getOperations(deviceId, callback): void { - operationsCollection +export async function getOperations( + deviceId +): Promise<{ [commandKey: string]: Operation }> { + const res = await operationsCollection .find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } }) - .toArray((err, res) => { - if (err) return void callback(err); + .toArray(); - const operations = {}; - for (const r of res) { - const commandKey = r._id.slice(deviceId.length + 1); - delete r._id; - r.timestamp = +r.timestamp; - if (r.args) r.args = JSON.parse(r.args); - r.provisions = JSON.parse(r.provisions); - r.retries = JSON.parse(r.retries); - operations[commandKey] = r; - } - - callback(err, operations); - }); + const operations: { [commandKey: string]: Operation } = {}; + for (const r of res) { + const commandKey = r._id.slice(deviceId.length + 1); + delete r._id; + r.timestamp = +r.timestamp; + if (r.args) r.args = JSON.parse(r.args); + r.provisions = JSON.parse(r.provisions); + r.retries = JSON.parse(r.retries); + operations[commandKey] = r; + } + return operations; } -export function saveOperation(deviceId, commandKey, operation, callback): void { +export async function saveOperation( + deviceId, + commandKey, + operation +): Promise { const id = `${deviceId}:${commandKey}`; operation = Object.assign({}, operation); operation._id = id; @@ -887,75 +866,64 @@ export function saveOperation(deviceId, commandKey, operation, callback): void { operation.provisions = JSON.stringify(operation.provisions); operation.retries = JSON.stringify(operation.retries); operation.args = JSON.stringify(operation.args); - operationsCollection.replaceOne( - { _id: id }, - operation, - { upsert: true }, - callback - ); -} - -export function deleteOperation(deviceId, commandKey, callback): void { - operationsCollection.deleteOne( - { _id: `${deviceId}:${commandKey}` }, - callback - ); -} - -export function getPresets(callback): void { - presetsCollection.find().toArray(callback); -} - -export function getObjects(callback): void { - objectsCollection.find().toArray(callback); -} - -export function getProvisions(callback): void { - provisionsCollection.find().toArray(callback); -} - -export function getVirtualParameters(callback): void { - virtualParametersCollection.find().toArray(callback); -} - -export function getFiles(callback): void { - filesCollection.find().toArray(callback); -} - -export function getConfig(callback): void { - configCollection.find().toArray((err, res) => { - if (err) return void callback(err); - - callback( - null, - res.map(c => ({ - id: c["_id"], - value: parse(c["value"]) - })) - ); + await operationsCollection.replaceOne({ _id: id }, operation, { + upsert: true }); } -export function getPermissions( - callback: ( - err?: Error, - permissions?: { - role: string; - resource: string; - access: number; - filter: string; - validate: string; - }[] - ) => void -): void { - permissionsCollection.find().toArray(callback); +export async function deleteOperation(deviceId, commandKey): Promise { + await operationsCollection.deleteOne({ _id: `${deviceId}:${commandKey}` }); } -export function getUsers( - callback: ( - err?: Error, - users?: { _id: string; password: string; salt: string; roles: string }[] - ) => void -): void { - usersCollection.find().toArray(callback); +export async function getPresets(): Promise<{}[]> { + return presetsCollection.find().toArray(); +} + +export async function getObjects(): Promise<{}[]> { + return objectsCollection.find().toArray(); +} + +export async function getProvisions(): Promise<{}[]> { + return provisionsCollection.find().toArray(); +} + +export async function getVirtualParameters(): Promise<{}[]> { + return virtualParametersCollection.find().toArray(); +} + +export function getFiles(): Promise<{}[]> { + return filesCollection.find().toArray(); +} + +export async function getConfig(): Promise< + { id: string; value: Expression }[] +> { + const res = await configCollection.find().toArray(); + return res.map(c => ({ + id: c["_id"], + value: parse(c["value"]) + })); +} + +interface Permission { + role: string; + resource: string; + access: number; + filter: string; + validate: string; +} + +export async function getPermissions(): Promise { + return permissionsCollection.find().toArray(); +} + +interface User { + _id: string; + password: string; + salt: string; + roles: string; +} + +export async function getUsers(): Promise { + return usersCollection.find().toArray(); } diff --git a/lib/extensions.ts b/lib/extensions.ts index 6ce729e..7a5790e 100644 --- a/lib/extensions.ts +++ b/lib/extensions.ts @@ -17,92 +17,97 @@ * along with GenieACS. If not, see . */ -import { spawn } from "child_process"; +import { spawn, ChildProcess } from "child_process"; import * as crypto from "crypto"; import * as config from "./config"; +import { Fault } from "./types"; const TIMEOUT = +config.get("EXT_TIMEOUT"); -const processes = {}; +const processes: { [script: string]: ChildProcess } = {}; const jobs = new Map(); function messageHandler(message): void { const func = jobs.get(message[0]); if (func) { jobs.delete(message[0]); - func(null, message[1], message[2]); + func({ fault: message[1], value: message[2] }); } } -export function run(args, callback): boolean { - const scriptName = args[0]; +export function run(args): Promise<{ fault: Fault; value: any }> { + return new Promise(resolve => { + const scriptName = args[0]; - const id = crypto.randomBytes(8).toString("hex"); - jobs.set(id, callback); + const id = crypto.randomBytes(8).toString("hex"); + jobs.set(id, resolve); - if (!processes[scriptName]) { - const p = spawn("bin/genieacs-ext", [scriptName], { - stdio: ["inherit", "inherit", "inherit", "ipc"] - }); - processes[scriptName] = p; + if (!processes[scriptName]) { + const p = spawn("bin/genieacs-ext", [scriptName], { + stdio: ["inherit", "inherit", "inherit", "ipc"] + }); + processes[scriptName] = p; - p.on("error", err => { - if (processes[scriptName] === p) { - if (jobs.delete(id)) - callback(null, { code: err.name, message: err.message }); + p.on("error", err => { + if (processes[scriptName] === p) { + if (jobs.delete(id)) { + resolve({ + fault: { code: err.name, message: err.message }, + value: null + }); + } - kill(processes[scriptName]); - delete processes[scriptName]; - } - }); + kill(processes[scriptName]); + delete processes[scriptName]; + } + }); - p.on("disconnect", () => { - if (processes[scriptName] === p) { - kill(processes[scriptName]); - delete processes[scriptName]; - } - }); + p.on("disconnect", () => { + if (processes[scriptName] === p) delete processes[scriptName]; + }); - p.on("message", messageHandler); - } - - setTimeout(() => { - if (jobs.delete(id)) - callback(null, { code: "timeout", message: "Extension timed out" }); - }, TIMEOUT); - - if (!processes[scriptName].connected) return false; - - return processes[scriptName].send([id, args.slice(1)]); -} - -function kill(process, callback?: () => void): void { - if (process.signalCode !== null || process.exitCode !== null) return; - - const timeToKill = Date.now() + 5000; - - process.kill(); - - const t = setInterval(() => { - if (process.signalCode !== null || process.exitCode !== null) { - clearInterval(t); - if (callback) callback(); - } else if (Date.now() > timeToKill) { - process.kill("SIGKILL"); - clearInterval(t); - if (callback) callback(); + p.on("message", messageHandler); } - }, 100); + + setTimeout(() => { + if (jobs.delete(id)) { + resolve({ + fault: { code: "timeout", message: "Extension timed out" }, + value: null + }); + } + }, TIMEOUT); + + if (!processes[scriptName].connected) return false; + + return processes[scriptName].send([id, args.slice(1)]); + }); } -export function killAll(callback?): void { - let counter = 1; - for (const n in processes) { - ++counter; - kill(processes[n], () => { - if (--counter === 0 && callback) callback(); - }); - delete processes[n]; - } - if (--counter === 0 && callback) callback(); +function kill(process: ChildProcess): Promise { + return new Promise(resolve => { + const timeToKill = Date.now() + 5000; + + process.kill(); + + const t = setInterval(() => { + if (!process.connected) { + clearInterval(t); + resolve(); + } else if (Date.now() > timeToKill) { + process.kill("SIGKILL"); + clearInterval(t); + resolve(); + } + }, 100); + }); +} + +export async function killAll(): Promise { + await Promise.all( + Object.entries(processes).map(([k, p]) => { + delete processes[k]; + return kill(p); + }) + ); } diff --git a/lib/local-cache.ts b/lib/local-cache.ts index b84185d..da32ae7 100644 --- a/lib/local-cache.ts +++ b/lib/local-cache.ts @@ -108,391 +108,326 @@ function flattenObject(src, prefix = "", dst = {}): {} { return dst; } -function refresh(callback): void { +async function fetchPresets(): Promise { + const res = await db.getPresets(); + let objects = await db.getObjects(); + + objects = objects.map(obj => { + // Flatten object + obj = flattenObject(obj); + + // If no keys are defined, consider all parameters as keys to keep the + // same behavior from v1.0 + if (!obj["_keys"] || !obj["_keys"].length) + obj["_keys"] = Object.keys(obj).filter(k => !k.startsWith("_")); + + return obj; + }); + + res.sort((a, b) => { + if (a["weight"] === b["weight"]) + return a["_id"] > b["_id"] ? 1 : a["_id"] < b["_id"] ? -1 : 0; + else return a["weight"] - b["weight"]; + }); + + const presets = [] as Preset[]; + for (const preset of res) { + let schedule: { md5: string; duration: number; schedule: any } = null; + if (preset["schedule"]) { + const parts = preset["schedule"].trim().split(/\s+/); + schedule = { + md5: crypto + .createHash("md5") + .update(preset["schedule"]) + .digest("hex"), + duration: null, + schedule: null + }; + + try { + schedule.duration = +parts.shift() * 1000; + schedule.schedule = scheduling.parseCron(parts.join(" ")); + } catch (err) { + logger.warn({ + message: "Invalid preset schedule", + preset: preset["_id"], + schedule: preset["schedule"] + }); + schedule.schedule = false; + } + } + + const events = preset["events"] || {}; + let precondition; + try { + precondition = parse(preset["precondition"]); + } catch (error) { + precondition = mongoQueryToFilter(JSON.parse(preset["precondition"])); + } + + const _provisions = preset["provisions"] || []; + + // Generate provisions from the old configuration format + for (const c of preset["configurations"]) { + switch (c.type) { + case "age": + _provisions.push(["refresh", c.name, c.age]); + break; + + case "value": + _provisions.push(["value", c.name, c.value]); + break; + + case "add_tag": + _provisions.push(["tag", c.tag, true]); + break; + + case "delete_tag": + _provisions.push(["tag", c.tag, false]); + break; + + case "provision": + _provisions.push([c.name].concat(c.args || [])); + break; + + case "add_object": + for (const obj of objects) { + if (obj["_id"] === c.object) { + const alias = obj["_keys"] + .map(k => `${k}:${JSON.stringify(obj[k])}`) + .join(","); + const p = `${c.name}.[${alias}]`; + _provisions.push(["instances", p, 1]); + + for (const k in obj) { + if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1)) + _provisions.push(["value", `${p}.${k}`, obj[k]]); + } + } + } + + break; + + case "delete_object": + for (const obj of objects) { + if (obj["_id"] === c.object) { + const alias = obj["_keys"] + .map(k => `${k}:${JSON.stringify(obj[k])}`) + .join(","); + const p = `${c.name}.[${alias}]`; + _provisions.push(["instances", p, 0]); + } + } + + break; + + default: + throw new Error(`Unknown configuration type ${c.type}`); + } + } + + presets.push({ + name: preset["_id"], + channel: (preset["channel"] as string) || "default", + schedule: schedule, + events: events, + precondition: precondition, + provisions: _provisions + }); + } + + return presets; +} + +async function fetchProvisions(): Promise<{}> { + const res = await db.getProvisions(); + + const provisions = {}; + for (const r of res) { + provisions[r["_id"]] = {}; + provisions[r["_id"]].md5 = crypto + .createHash("md5") + .update(r["script"]) + .digest("hex"); + provisions[r["_id"]].script = new vm.Script( + `"use strict";(function(){\n${r["script"]}\n})();`, + { filename: r["_id"], lineOffset: -1, timeout: 50 } + ); + } + + return provisions; +} + +async function fetchVirtualParameters(): Promise<{}> { + const res = await db.getVirtualParameters(); + + const virtualParameters = {}; + for (const r of res) { + virtualParameters[r["_id"]] = {}; + virtualParameters[r["_id"]].md5 = crypto + .createHash("md5") + .update(r["script"]) + .digest("hex"); + virtualParameters[r["_id"]].script = new vm.Script( + `"use strict";(function(){\n${r["script"]}\n})();`, + { filename: r["_id"], lineOffset: -1, timeout: 50 } + ); + } + + return virtualParameters; +} + +async function fetchPermissions(): Promise { + const perms = await db.getPermissions(); + const permissions: Permissions = {}; + + for (const p of perms) { + if (!permissions[p.role]) permissions[p.role] = {}; + if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {}; + + permissions[p.role][p.access][p.resource] = { + access: p.access, + filter: parse(p.filter || "true") + }; + if (p.validate) + permissions[p.role][p.access][p.resource].validate = parse(p.validate); + } + + return permissions; +} + +async function fetchFiles(): Promise<{}> { + const res = await db.getFiles(); + const files = {}; + + for (const r of res) { + const id = r["filename"] || r["_id"].toString(); + files[id] = {}; + files[id].length = r["length"]; + files[id].md5 = r["md5"]; + files[id].contentType = r["contentType"]; + } + + return files; +} + +async function fetchUsers(): Promise<{}> { + const _users = await db.getUsers(); + const users = {}; + + for (const user of _users) { + users[user._id] = { + password: user.password, + salt: user.salt, + roles: user.roles.split(",").map(s => s.trim()) + }; + } + + return users; +} + +async function fetchConfig(): Promise<[{}, {}]> { + const conf = await db.getConfig(); + + conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)); + + const ui = { + filters: {}, + device: {}, + index: {}, + overview: { charts: {}, groups: {} } + }; + const _config = {}; + + for (const c of conf) { + // Evaluate expressions to simplify them + const val = expression.evaluate(c.value); + _config[c.id] = val; + if (c.id.startsWith("ui.")) { + const keys = c.id.split("."); + // remove the first key(ui) + keys.shift(); + let ref = ui; + while (keys.length > 1) { + const k = keys.shift(); + if (typeof ref[k] !== "object") ref[k] = {}; + ref = ref[k]; + } + ref[keys[0]] = val; + } + } + + if (!Object.keys(ui["index"]).length) { + ui["index"] = { + "0": { + label: "ID", + parameter: ["PARAM", "DeviceID.ID"] + } + }; + } + + return [_config, ui]; +} + +async function refresh(): Promise { if (!nextRefresh) { - return void setTimeout(() => { - refresh(callback); - }, 20); + await new Promise(resolve => setTimeout(resolve, 20)); + await refresh(); + return; } nextRefresh = 0; const now = Date.now(); - cache.get("presets_hash", (err, dbHash) => { - if (err) return void callback(err); + const dbHash = await cache.get("presets_hash"); - if (currentSnapshot && dbHash === currentSnapshot) { - nextRefresh = now + (REFRESH - (now % REFRESH)); - return void callback(); - } - - cache.lock("presets_hash_lock", 3, (err, unlockOrExtend) => { - if (err) return void callback(err); - - const promises = []; - - promises.push( - new Promise((resolve, reject) => { - db.getPresets((err, res) => { - if (err) return void reject(err); - - db.getObjects((err, objects) => { - if (err) return void reject(err); - - objects = objects.map(obj => { - // Flatten object - obj = flattenObject(obj); - - // If no keys are defined, consider all parameters as keys to keep the - // same behavior from v1.0 - if (!obj["_keys"] || !obj["_keys"].length) { - obj["_keys"] = Object.keys(obj).filter( - k => !k.startsWith("_") - ); - } - - return obj; - }); - - res.sort((a, b) => { - if (a.weight === b.weight) return a._id > b._id; - else return a.weight - b.weight; - }); - - const presets = [] as Preset[]; - for (const preset of res) { - let schedule = null; - if (preset.schedule) { - const parts = preset.schedule.trim().split(/\s+/); - schedule = { - md5: crypto - .createHash("md5") - .update(preset.schedule) - .digest("hex") - }; - - try { - schedule.duration = +parts.shift() * 1000; - schedule.schedule = scheduling.parseCron(parts.join(" ")); - } catch (err) { - logger.warn({ - message: "Invalid preset schedule", - preset: preset._id, - schedule: preset.schedule - }); - schedule.schedule = false; - } - } - - const events = preset.events || {}; - let precondition; - try { - precondition = parse(preset.precondition); - } catch (error) { - precondition = mongoQueryToFilter( - JSON.parse(preset.precondition) - ); - } - - const _provisions = preset.provisions || []; - - // Generate provisions from the old configuration format - for (const c of preset.configurations) { - switch (c.type) { - case "age": - _provisions.push(["refresh", c.name, c.age]); - break; - - case "value": - _provisions.push(["value", c.name, c.value]); - break; - - case "add_tag": - _provisions.push(["tag", c.tag, true]); - break; - - case "delete_tag": - _provisions.push(["tag", c.tag, false]); - break; - - case "provision": - _provisions.push([c.name].concat(c.args || [])); - break; - - case "add_object": - for (const obj of objects) { - if (obj["_id"] === c.object) { - const alias = obj["_keys"] - .map(k => `${k}:${JSON.stringify(obj[k])}`) - .join(","); - const p = `${c.name}.[${alias}]`; - _provisions.push(["instances", p, 1]); - - for (const k in obj) { - if ( - !k.startsWith("_") && - !(obj["_keys"].indexOf(k) !== -1) - ) - _provisions.push(["value", `${p}.${k}`, obj[k]]); - } - } - } - - break; - - case "delete_object": - for (const obj of objects) { - if (obj["_id"] === c.object) { - const alias = obj["_keys"] - .map(k => `${k}:${JSON.stringify(obj[k])}`) - .join(","); - const p = `${c.name}.[${alias}]`; - _provisions.push(["instances", p, 0]); - } - } - - break; - - default: - return void reject( - new Error(`Unknown configuration type ${c.type}`) - ); - } - } - - presets.push({ - name: preset._id, - channel: preset.channel || "default", - schedule: schedule, - events: events, - precondition: precondition, - provisions: _provisions - }); - } - - resolve(presets); - }); - }); - }) - ); - - promises.push( - new Promise((resolve, reject) => { - db.getProvisions((err, res) => { - if (err) return void reject(err); - - const provisions = {}; - for (const r of res) { - provisions[r._id] = {}; - provisions[r._id].md5 = crypto - .createHash("md5") - .update(r.script) - .digest("hex"); - provisions[r._id].script = new vm.Script( - `"use strict";(function(){\n${r.script}\n})();`, - { filename: r._id, lineOffset: -1, timeout: 50 } - ); - } - - resolve(provisions); - }); - }) - ); - - promises.push( - new Promise((resolve, reject) => { - db.getVirtualParameters((err, res) => { - if (err) return void reject(err); - - const virtualParameters = {}; - for (const r of res) { - virtualParameters[r._id] = {}; - virtualParameters[r._id].md5 = crypto - .createHash("md5") - .update(r.script) - .digest("hex"); - virtualParameters[r._id].script = new vm.Script( - `"use strict";(function(){\n${r.script}\n})();`, - { filename: r._id, lineOffset: -1, timeout: 50 } - ); - } - - resolve(virtualParameters); - }); - }) - ); - - promises.push( - new Promise((resolve, reject) => { - db.getFiles((err, res) => { - if (err) return void reject(err); - - const files = {}; - for (const r of res) { - const id = r.filename || r._id.toString(); - files[id] = {}; - files[id].length = r.length; - files[id].md5 = r.md5; - files[id].contentType = r.contentType; - } - - resolve(files); - }); - }) - ); - - promises.push( - new Promise((resolve, reject) => { - db.getPermissions((err, perms) => { - if (err) return void reject(err); - const permissions: Permissions = {}; - for (const p of perms) { - if (!permissions[p.role]) permissions[p.role] = {}; - if (!permissions[p.role][p.access]) - permissions[p.role][p.access] = {}; - - permissions[p.role][p.access][p.resource] = { - access: p.access, - filter: parse(p.filter || "true") - }; - if (p.validate) { - permissions[p.role][p.access][p.resource].validate = parse( - p.validate - ); - } - } - - resolve(permissions); - }); - }) - ); - - promises.push( - new Promise((resolve, reject) => { - db.getUsers((err, _users) => { - if (err) return void reject(err); - const users = {}; - for (const user of _users) { - users[user._id] = { - password: user.password, - salt: user.salt, - roles: user.roles.split(",").map(s => s.trim()) - }; - } - resolve(users); - }); - }) - ); - - promises.push( - new Promise((resolve, reject) => { - db.getConfig((err, conf) => { - if (err) return void reject(err); - - conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)); - - const ui = { - filters: {}, - device: {}, - index: {}, - overview: { charts: {}, groups: {} } - }; - const _config = {}; - - for (const c of conf) { - // Evaluate expressions to simplify them - const val = expression.evaluate(c.value); - _config[c.id] = val; - if (c.id.startsWith("ui.")) { - const keys = c.id.split("."); - // remove the first key(ui) - keys.shift(); - let ref = ui; - while (keys.length > 1) { - const k = keys.shift(); - if (typeof ref[k] !== "object") ref[k] = {}; - ref = ref[k]; - } - ref[keys[0]] = val; - } - } - - if (!Object.keys(ui["index"]).length) { - ui["index"] = { - "0": { - label: "ID", - parameter: ["PARAM", "DeviceID.ID"] - } - }; - } - - resolve([_config, ui]); - }); - }) - ); - - Promise.all(promises) - .then(res => { - const snapshot = { - presets: res[0], - provisions: res[1], - virtualParameters: res[2], - files: res[3], - permissions: res[4], - users: res[5], - config: res[6][0], - ui: res[6][1] - }; - - if (currentSnapshot) { - const h = currentSnapshot; - const s = snapshots.get(h); - setTimeout(() => { - if (snapshots.get(h) === s) snapshots.delete(h); - }, EVICT_TIMEOUT).unref(); - } - - currentSnapshot = computeHash(snapshot); - snapshots.set(currentSnapshot, snapshot); - cache.set("presets_hash", currentSnapshot, 300, err => { - unlockOrExtend(0); - if (err) return void callback(err); - nextRefresh = now + (REFRESH - (now % REFRESH)); - callback(); - }); - }) - .catch(callback); - }); - }); -} - -interface GetCurrentSnapshotCallback { - (err: null, currentSnapshot: string): void; - (err: Error, currentSnapshot?: null): void; -} - -export function getCurrentSnapshot(callback: GetCurrentSnapshotCallback); -export function getCurrentSnapshot(): Promise; -export function getCurrentSnapshot( - callback?: GetCurrentSnapshotCallback -): Promise | void { - if (Date.now() < nextRefresh) { - if (callback) return callback(null, currentSnapshot); - return Promise.resolve(currentSnapshot); + if (currentSnapshot && dbHash === currentSnapshot) { + nextRefresh = now + (REFRESH - (now % REFRESH)); + return; } - if (callback) { - return refresh(err => { - callback(err, currentSnapshot); - }); + const unlockOrExtend = await cache.lock("presets_hash_lock", 3); + + const res = await Promise.all([ + fetchPresets(), + fetchProvisions(), + fetchVirtualParameters(), + fetchFiles(), + fetchPermissions(), + fetchUsers(), + fetchConfig() + ]); + + const snapshot = { + presets: res[0], + provisions: res[1], + virtualParameters: res[2], + files: res[3], + permissions: res[4], + users: res[5], + config: res[6][0], + ui: res[6][1] + }; + + if (currentSnapshot) { + const h = currentSnapshot; + const s = snapshots.get(h); + setTimeout(() => { + if (snapshots.get(h) === s) snapshots.delete(h); + }, EVICT_TIMEOUT).unref(); } - return new Promise((resolve, reject) => { - refresh(err => { - if (err) return void reject(err); - resolve(currentSnapshot); - }); - }); + currentSnapshot = computeHash(snapshot); + snapshots.set(currentSnapshot, snapshot); + await cache.set("presets_hash", currentSnapshot, 300); + await unlockOrExtend(0); + + nextRefresh = now + (REFRESH - (now % REFRESH)); +} + +export async function getCurrentSnapshot(): Promise { + if (Date.now() > nextRefresh) await refresh(); + return currentSnapshot; } export function hasSnapshot(hash): boolean { diff --git a/lib/nbi.ts b/lib/nbi.ts index 84ea65f..16b0865 100644 --- a/lib/nbi.ts +++ b/lib/nbi.ts @@ -91,24 +91,34 @@ export function listener(request, response): void { err => { if (err) return void throwError(err, response); - cache.del("presets_hash", err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del("presets_hash") + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); } ); } else if (request.method === "DELETE") { db.presetsCollection.deleteOne({ _id: presetName }, err => { if (err) return void throwError(err, response); - cache.del("presets_hash", err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del("presets_hash") + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); }); } else { response.writeHead(405, { Allow: "PUT, DELETE" }); @@ -128,24 +138,34 @@ export function listener(request, response): void { err => { if (err) return void throwError(err, response); - cache.del("presets_hash", err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del("presets_hash") + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); } ); } else if (request.method === "DELETE") { db.objectsCollection.deleteOne({ _id: objectName }, err => { if (err) return void throwError(err, response); - cache.del("presets_hash", err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del("presets_hash") + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); }); } else { response.writeHead(405, { Allow: "PUT, DELETE" }); @@ -176,24 +196,34 @@ export function listener(request, response): void { err => { if (err) return void throwError(err, response); - cache.del("presets_hash", err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del("presets_hash") + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); } ); } else if (request.method === "DELETE") { db.provisionsCollection.deleteOne({ _id: provisionName }, err => { if (err) return void throwError(err, response); - cache.del("presets_hash", err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del("presets_hash") + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); }); } else { response.writeHead(405, { Allow: "PUT, DELETE" }); @@ -224,12 +254,17 @@ export function listener(request, response): void { err => { if (err) return void throwError(err, response); - cache.del("presets_hash", err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del("presets_hash") + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); } ); } else if (request.method === "DELETE") { @@ -238,12 +273,17 @@ export function listener(request, response): void { err => { if (err) return void throwError(err, response); - cache.del("presets_hash", err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del("presets_hash") + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); } ); } else { @@ -258,7 +298,6 @@ export function listener(request, response): void { db.devicesCollection.updateOne( { _id: deviceId }, { $addToSet: { _tags: tag } }, - { safe: true }, err => { if (err) return void throwError(err, response); response.writeHead(200); @@ -269,7 +308,6 @@ export function listener(request, response): void { db.devicesCollection.updateOne( { _id: deviceId }, { $pull: { _tags: tag } }, - { safe: true }, err => { if (err) return void throwError(err, response); @@ -296,21 +334,31 @@ export function listener(request, response): void { return void db.tasksCollection.deleteOne({ _id: objId }, err => { if (err) return void throwError(err, response); - cache.del(`${deviceId}_tasks_faults_operations`, err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del(`${deviceId}_tasks_faults_operations`) + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); }); } - cache.del(`${deviceId}_tasks_faults_operations`, err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del(`${deviceId}_tasks_faults_operations`) + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); }); } else { response.writeHead(405, { Allow: "DELETE" }); @@ -324,83 +372,95 @@ export function listener(request, response): void { if (body.length) { const task = JSON.parse(body.toString()); task.device = deviceId; - apiFunctions.insertTasks(task, err => { - if (err) return void throwError(err, response); + apiFunctions + .insertTasks(task) + .then(() => { + cache + .del(`${deviceId}_tasks_faults_operations`) + .then(() => { + if (urlParts.query.connection_request != null) { + apiFunctions + .connectionRequest(deviceId) + .then(() => { + const taskTimeout = + (urlParts.query.timeout && + parseInt(urlParts.query.timeout as string)) || + config.get("DEVICE_ONLINE_THRESHOLD", deviceId); - cache.del(`${deviceId}_tasks_faults_operations`, err => { - if (err) return void throwError(err, response); + apiFunctions + .watchTask(deviceId, task._id, taskTimeout) + .then(status => { + if (status === "timeout") { + response.writeHead( + 202, + "Task queued but not processed", + { + "Content-Type": "application/json" + } + ); + response.end(JSON.stringify(task)); + } else if (status === "fault") { + db.tasksCollection.findOne( + { _id: task._id }, + (err, task2) => { + if (err) + return void throwError(err, response); - if (urlParts.query.connection_request != null) { - apiFunctions.connectionRequest(deviceId, err => { - if (err) { - response.writeHead(202, err.message, { - "Content-Type": "application/json" - }); - response.end(JSON.stringify(task)); - return; - } - - const taskTimeout = - (urlParts.query.timeout && - parseInt(urlParts.query.timeout as string)) || - config.get("DEVICE_ONLINE_THRESHOLD", deviceId); - - apiFunctions.watchTask( - deviceId, - task._id, - taskTimeout, - (err, status) => { - if (err) return void throwError(err, response); - - if (status === "timeout") { - response.writeHead( - 202, - "Task queued but not processed", - { - "Content-Type": "application/json" - } - ); - response.end(JSON.stringify(task)); - } else if (status === "fault") { - db.tasksCollection.findOne( - { _id: task._id }, - (err, task2) => { - if (err) return void throwError(err, response); - - response.writeHead(202, "Task faulted", { - "Content-Type": "application/json" + response.writeHead(202, "Task faulted", { + "Content-Type": "application/json" + }); + response.end(JSON.stringify(task2)); + } + ); + } else { + response.writeHead(200, { + "Content-Type": "application/json" + }); + response.end(JSON.stringify(task)); + } + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); }); - response.end(JSON.stringify(task2)); - } - ); - } else { - response.writeHead(200, { + }); + }) + .catch(err => { + response.writeHead(202, err.message, { "Content-Type": "application/json" }); response.end(JSON.stringify(task)); - } - } - ); + }); + } else { + response.writeHead(202, { + "Content-Type": "application/json" + }); + response.end(JSON.stringify(task)); + } + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); }); - } else { - response.writeHead(202, { - "Content-Type": "application/json" - }); - response.end(JSON.stringify(task)); - } + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); }); - }); } else if (urlParts.query.connection_request != null) { // No task, send connection request only - apiFunctions.connectionRequest(deviceId, err => { - if (err) { + apiFunctions + .connectionRequest(deviceId) + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { response.writeHead(504); response.end(`${err.name}: ${err.message}`); - return; - } - response.writeHead(200); - response.end(); - }); + }); } else { response.writeHead(400); response.end(); @@ -438,12 +498,17 @@ export function listener(request, response): void { err => { if (err) return void throwError(err, response); - cache.del(`${deviceId}_tasks_faults_operations`, err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del(`${deviceId}_tasks_faults_operations`) + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); } ); } @@ -468,12 +533,17 @@ export function listener(request, response): void { err => { if (err) return void throwError(err, response); - cache.del(`${deviceId}_tasks_faults_operations`, err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + cache + .del(`${deviceId}_tasks_faults_operations`) + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); } ); } @@ -547,12 +617,17 @@ export function listener(request, response): void { const deviceId = querystring.unescape( DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1] ); - apiFunctions.deleteDevice(deviceId, err => { - if (err) return void throwError(err, response); - - response.writeHead(200); - response.end(); - }); + apiFunctions + .deleteDevice(deviceId) + .then(() => { + response.writeHead(200); + response.end(); + }) + .catch(err => { + setTimeout(() => { + throwError(err, response); + }); + }); } else if (QUERY_REGEX.test(urlParts.pathname)) { let collectionName = QUERY_REGEX.exec(urlParts.pathname)[1]; diff --git a/lib/sandbox.ts b/lib/sandbox.ts index f1cbb36..c32f1f4 100644 --- a/lib/sandbox.ts +++ b/lib/sandbox.ts @@ -24,7 +24,7 @@ import * as extensions from "./extensions"; import * as logger from "./logger"; import * as scheduling from "./scheduling"; import Path from "./common/path"; -import { Fault, SessionContext } from "./types"; +import { Fault, SessionContext, ScriptResult } from "./types"; // Used for throwing to exit user script and commit const COMMIT = Symbol(); @@ -38,25 +38,35 @@ const context = vm.createContext(); let state; -const runningExtensions = new WeakMap(); -function runExtension(sessionContext, key, extCall, callback): void { +const runningExtensions = new WeakMap< + SessionContext, + Map> +>(); +function runExtension(sessionContext, key, extCall): Promise { let re = runningExtensions.get(sessionContext); if (!re) { - re = {}; + re = new Map>(); runningExtensions.set(sessionContext, re); } - if (re[key]) { - re[key].push(callback); - } else { - re[key] = [callback]; - extensions.run(extCall, (err, fault, res) => { - const callbacks = re[key]; - delete re[key]; - if (!err && !fault) sessionContext.extensionsCache[key] = res; - for (const c of callbacks) c(err, fault); - }); + let prom = re.get(key); + if (!prom) { + re.set( + key, + (prom = new Promise((resolve, reject) => { + extensions + .run(extCall) + .then(({ fault, value }) => { + re.delete(key); + if (!fault) sessionContext.extensionsCache[key] = value; + resolve(fault); + }) + .catch(reject); + })) + ); } + + return prom; } class SandboxDate { @@ -343,15 +353,14 @@ function errorToFault(err: Error): Fault { return fault; } -export function run( +export async function run( script: vm.Script, - globals, + globals: {}, sessionContext: SessionContext, startRevision: number, maxRevision: number, - callback, extCounter = 0 -): void { +): Promise { state = { sessionContext: sessionContext, revision: startRevision, @@ -374,61 +383,56 @@ export function run( ret = script.runInContext(context, { displayErrors: false }); status = 0; } catch (err) { - if (err === COMMIT) status = 1; - else if (err === EXT) status = 2; - else return void callback(null, errorToFault(err)); + if (err === COMMIT) { + status = 1; + } else if (err === EXT) { + status = 2; + } else { + return { + fault: errorToFault(err), + clear: null, + declare: null, + done: false, + returnValue: null + }; + } } const _state = state; - const args = Array.from(arguments); + let fault; - // Need to maintain a counter of ext() calls to avoid calling certain - // functions (e.g. log()) multiple times as calling ext() results in - // re-execution of script without revision being incremented. - args[6] = 0 - (state.extCounter - extCounter); + await Promise.all( + Object.entries(_state.extensions).map(async ([k, v]) => { + fault = (await runExtension(_state.sessionContext, k, v)) || fault; + }) + ); - let counter = 3; - for (const key of Object.keys(_state.extensions)) { - counter += 2; - runExtension( - _state.sessionContext, - key, - _state.extensions[key], - (err, fault) => { - if (err || fault) { - if (counter & 1) callback(err, fault); - return void (counter = 0); - } - if ((counter -= 2) === 1) { - if (status === 2) { - return void run.apply(null, args); - } else { - return void callback( - null, - null, - _state.clear, - _state.declarations, - status === 0, - ret - ); - } - } - } + if (fault) { + return { + fault: fault, + clear: null, + declare: null, + done: false, + returnValue: null + }; + } + + if (status === 2) { + return run( + script, + globals, + sessionContext, + startRevision, + maxRevision, + extCounter - _state.extCounter ); } - if ((counter -= 2) === 1) { - if (status === 2) { - run.apply(null, args); - } else { - callback( - null, - null, - _state.clear, - _state.declarations, - status === 0, - ret - ); - } - } + return { + fault: null, + clear: _state.clear, + declare: _state.declarations, + done: status === 0, + returnValue: ret + }; } diff --git a/lib/session.ts b/lib/session.ts index 441096d..762035b 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -43,9 +43,25 @@ import { CpeResponse, CpeFault, CpeSetResponse, - CpeGetResponse + CpeGetResponse, + AcsRequest, + AcsResponse, + InformRequest, + TransferCompleteRequest, + Operation, + ScriptResult } from "./types"; +const VALID_PARAM_TYPES = new Set([ + "xsd:int", + "xsd:unsignedInt", + "xsd:boolean", + "xsd:string", + "xsd:dateTime", + "xsd:base64", + "xsd:hexBinary" +]); + function initDeviceData(): DeviceData { return { paths: new PathSet(), @@ -60,9 +76,8 @@ function initDeviceData(): DeviceData { export function init( deviceId: string, cwmpVersion: string, - timeout: number, - callback: (err: Error, sessionContext: SessionContext) => void -): void { + timeout: number +): SessionContext { const timestamp = Date.now(); const sessionContext: SessionContext = { timestamp: timestamp, @@ -82,51 +97,46 @@ export function init( state: 0 }; - callback(null, sessionContext); + return sessionContext; } -function loadParameters(sessionContext: SessionContext, callback): void { - if (!sessionContext.toLoad || !sessionContext.toLoad.size) - return void callback(); +async function loadParameters(sessionContext: SessionContext): Promise { + if (!sessionContext.toLoad || !sessionContext.toLoad.size) return; const toLoad = Array.from(sessionContext.toLoad.entries()); - db.fetchDevice( + let [parameters, loaded] = await db.fetchDevice( sessionContext.deviceId, sessionContext.timestamp, - toLoad, - (err, parameters, loaded) => { - if (err) return void callback(err); - - if (!parameters) { - // Device not available in database, mark as new - sessionContext.new = true; - loaded = [ - [ - Path.parse(""), - (1 << +config.get("MAX_DEPTH", sessionContext.deviceId)) - 1 - ] - ]; - parameters = []; - } - - for (const p of loaded) { - const path = sessionContext.deviceData.paths.add(p[0]); - if (p[1]) { - const l = sessionContext.deviceData.loaded.get(path) | 0; - sessionContext.deviceData.loaded.set(path, l | p[1]); - } - } - - for (const p of parameters) { - const path = sessionContext.deviceData.paths.add(p[0]); - sessionContext.deviceData.timestamps.set(path, p[1], 0); - if (p[2]) sessionContext.deviceData.attributes.set(path, p[2], 0); - } - - delete sessionContext.toLoad; - callback(); - } + toLoad ); + + if (!parameters) { + // Device not available in database, mark as new + sessionContext.new = true; + loaded = [ + [ + Path.parse(""), + (1 << +config.get("MAX_DEPTH", sessionContext.deviceId)) - 1 + ] + ]; + parameters = []; + } + + for (const p of loaded) { + const path = sessionContext.deviceData.paths.add(p[0]); + if (p[1]) { + const l = sessionContext.deviceData.loaded.get(path) | 0; + sessionContext.deviceData.loaded.set(path, l | p[1]); + } + } + + for (const p of parameters) { + const path = sessionContext.deviceData.paths.add(p[0]); + sessionContext.deviceData.timestamps.set(path, p[1], 0); + if (p[2]) sessionContext.deviceData.attributes.set(path, p[2], 0); + } + + delete sessionContext.toLoad; } function generateRpcId(sessionContext: SessionContext): string { @@ -137,7 +147,10 @@ function generateRpcId(sessionContext: SessionContext): string { ); } -export function inform(sessionContext: SessionContext, rpcReq, callback): void { +export async function inform( + sessionContext: SessionContext, + rpcReq: InformRequest +): Promise { const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; const params: [Path, number, Attributes][] = [ [ @@ -188,7 +201,7 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { timestamp, { object: [timestamp, 0], - value: [timestamp, p.slice(1)] + value: [timestamp, p.slice(1) as [string | number | boolean, string]] } ]); } @@ -220,60 +233,56 @@ export function inform(sessionContext: SessionContext, rpcReq, callback): void { for (const p of params) loadPath(sessionContext, p[0]); - loadParameters(sessionContext, err => { - if (err) return void callback(err); + await loadParameters(sessionContext); - if (sessionContext.new) { - params.push([ - Path.parse("DeviceID.ID"), - timestamp, - { - object: [timestamp, 0], - writable: [timestamp, 0], - value: [timestamp, [sessionContext.deviceId, "xsd:string"]] - } - ]); - params.push([ - Path.parse("Events.Registered"), - timestamp, - { - object: [timestamp, 0], - writable: [timestamp, 0], - value: [timestamp, [sessionContext.timestamp, "xsd:dateTime"]] - } - ]); - } - - sessionContext.deviceData.timestamps.revision = 1; - sessionContext.deviceData.attributes.revision = 1; - - let toClear = null; - for (const p of params) { - // Don't need to clear wildcards for Events - if (p[0].segments[0] === "Events") { - device.set(sessionContext.deviceData, p[0], p[1], p[2]); - } else { - toClear = device.set( - sessionContext.deviceData, - p[0], - p[1], - p[2], - toClear - ); + if (sessionContext.new) { + params.push([ + Path.parse("DeviceID.ID"), + timestamp, + { + object: [timestamp, 0], + writable: [timestamp, 0], + value: [timestamp, [sessionContext.deviceId, "xsd:string"]] } - } + ]); + params.push([ + Path.parse("Events.Registered"), + timestamp, + { + object: [timestamp, 0], + writable: [timestamp, 0], + value: [timestamp, [sessionContext.timestamp, "xsd:dateTime"]] + } + ]); + } - clear(sessionContext, toClear, err => { - callback(err, { name: "InformResponse" }); - }); - }); + sessionContext.deviceData.timestamps.revision = 1; + sessionContext.deviceData.attributes.revision = 1; + + let toClear = null; + for (const p of params) { + // Don't need to clear wildcards for Events + if (p[0].segments[0] === "Events") { + device.set(sessionContext.deviceData, p[0], p[1], p[2]); + } else { + toClear = device.set( + sessionContext.deviceData, + p[0], + p[1], + p[2], + toClear + ); + } + } + + await clear(sessionContext, toClear); + return { name: "InformResponse" }; } -export function transferComplete( +export async function transferComplete( sessionContext: SessionContext, - rpcReq, - callback -): void { + rpcReq: TransferCompleteRequest +): Promise<{ acsResponse: AcsResponse; operation: Operation; fault: Fault }> { const revision = (sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1; sessionContext.deviceData.timestamps.revision = revision; @@ -281,8 +290,13 @@ export function transferComplete( const commandKey = rpcReq.commandKey; const operation = sessionContext.operations[commandKey]; - if (!operation) - return void callback(null, { name: "TransferCompleteResponse" }); + if (!operation) { + return { + acsResponse: { name: "TransferCompleteResponse" }, + operation: null, + fault: null + }; + } const instance = operation.args.instance; @@ -291,160 +305,153 @@ export function transferComplete( sessionContext.operationsTouched[commandKey] = 1; if (rpcReq.faultStruct && rpcReq.faultStruct.faultCode !== "0") { - return void revertDownloadParameters( - sessionContext, - operation.args.instance, - err => { - const fault = { - code: `cwmp.${rpcReq.faultStruct.faultCode}`, - message: rpcReq.faultStruct.faultString, - detail: rpcReq.faultStruct, - timestamp: operation.timestamp - }; + await revertDownloadParameters(sessionContext, operation.args.instance); - callback(err, { name: "TransferCompleteResponse" }, operation, fault); - } - ); + const fault: Fault = { + code: `cwmp.${rpcReq.faultStruct.faultCode}`, + message: rpcReq.faultStruct.faultString, + detail: rpcReq.faultStruct, + timestamp: operation.timestamp + }; + + return { + acsResponse: { name: "TransferCompleteResponse" }, + operation: operation, + fault: fault + }; } loadPath(sessionContext, Path.parse(`Downloads.${instance}.*`)); - loadParameters(sessionContext, err => { - if (err) return void callback(err); + await loadParameters(sessionContext); - let toClear = null; - const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; + let toClear = null; + const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; - let p; + let p; - p = sessionContext.deviceData.paths.add( - Path.parse(`Downloads.${instance}.LastDownload`) - ); - toClear = device.set( - sessionContext.deviceData, - p, - timestamp, - { value: [timestamp, [operation.timestamp, "xsd:dateTime"]] }, - toClear - ); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastDownload`) + ); + toClear = device.set( + sessionContext.deviceData, + p, + timestamp, + { value: [timestamp, [operation.timestamp, "xsd:dateTime"]] }, + toClear + ); - p = sessionContext.deviceData.paths.add( - Path.parse(`Downloads.${instance}.LastFileType`) - ); - toClear = device.set( - sessionContext.deviceData, - p, - timestamp, - { value: [timestamp, [operation.args.fileType, "xsd:string"]] }, - toClear - ); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastFileType`) + ); + toClear = device.set( + sessionContext.deviceData, + p, + timestamp, + { value: [timestamp, [operation.args.fileType, "xsd:string"]] }, + toClear + ); - p = sessionContext.deviceData.paths.add( - Path.parse(`Downloads.${instance}.LastFileName`) - ); - toClear = device.set( - sessionContext.deviceData, - p, - timestamp, - { value: [timestamp, [operation.args.fileName, "xsd:string"]] }, - toClear - ); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastFileName`) + ); + toClear = device.set( + sessionContext.deviceData, + p, + timestamp, + { value: [timestamp, [operation.args.fileName, "xsd:string"]] }, + toClear + ); - p = sessionContext.deviceData.paths.add( - Path.parse(`Downloads.${instance}.LastTargetFileName`) - ); - toClear = device.set( - sessionContext.deviceData, - p, - timestamp, - { value: [timestamp, [operation.args.targetFileName, "xsd:string"]] }, - toClear - ); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastTargetFileName`) + ); + toClear = device.set( + sessionContext.deviceData, + p, + timestamp, + { value: [timestamp, [operation.args.targetFileName, "xsd:string"]] }, + toClear + ); - p = sessionContext.deviceData.paths.add( - Path.parse(`Downloads.${instance}.StartTime`) - ); - toClear = device.set( - sessionContext.deviceData, - p, - timestamp, - { value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] }, - toClear - ); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.StartTime`) + ); + toClear = device.set( + sessionContext.deviceData, + p, + timestamp, + { value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] }, + toClear + ); - p = sessionContext.deviceData.paths.add( - Path.parse(`Downloads.${instance}.CompleteTime`) - ); - toClear = device.set( - sessionContext.deviceData, - p, - timestamp, - { value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] }, - toClear - ); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.CompleteTime`) + ); + toClear = device.set( + sessionContext.deviceData, + p, + timestamp, + { value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] }, + toClear + ); - clear(sessionContext, toClear, err => { - callback(err, { name: "TransferCompleteResponse" }, operation); - }); - }); + await clear(sessionContext, toClear); + return { + acsResponse: { name: "TransferCompleteResponse" }, + operation: operation, + fault: null + }; } -function revertDownloadParameters( +async function revertDownloadParameters( sessionContext: SessionContext, - instance, - callback -): void { + instance +): Promise { loadPath(sessionContext, Path.parse(`Downloads.${instance}.*`)); - loadParameters(sessionContext, err => { - if (err) return void callback(err); + await loadParameters(sessionContext); - const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; + const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; - let p; + let p; - p = sessionContext.deviceData.paths.add( - Path.parse(`Downloads.${instance}.LastDownload`) - ); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.LastDownload`) + ); - const lastDownload = sessionContext.deviceData.attributes.get(p); + const lastDownload = sessionContext.deviceData.attributes.get(p); - p = sessionContext.deviceData.paths.add( - Path.parse(`Downloads.${instance}.Download`) - ); + p = sessionContext.deviceData.paths.add( + Path.parse(`Downloads.${instance}.Download`) + ); - const toClear = device.set(sessionContext.deviceData, p, timestamp, { - value: [ - timestamp, - [ - lastDownload && lastDownload.value[1] ? lastDownload.value[1][0] : 0, - "xsd:dateTime" - ] + const toClear = device.set(sessionContext.deviceData, p, timestamp, { + value: [ + timestamp, + [ + lastDownload && lastDownload.value[1] ? lastDownload.value[1][0] : 0, + "xsd:dateTime" ] - }); - - clear(sessionContext, toClear, callback); + ] }); + + await clear(sessionContext, toClear); } -export function timeoutOperations( - sessionContext: SessionContext, - callback -): void { +export async function timeoutOperations( + sessionContext: SessionContext +): Promise<{ faults: Fault[]; operations: Operation[] }> { const revision = (sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1; sessionContext.deviceData.timestamps.revision = revision; sessionContext.deviceData.attributes.revision = revision; const faults = []; const operations = []; - let counter = 3; for (const [commandKey, operation] of Object.entries(operations)) { - if (operation.name !== "Download") { - return void callback( - new Error(`Unknown operation name ${operation.name}`) - ); - } + if (operation.name !== "Download") + throw new Error(`Unknown operation name ${operation.name}`); const DOWNLOAD_TIMEOUT = +localCache.getConfig( @@ -467,17 +474,11 @@ export function timeoutOperations( operations.push(operation); - counter += 2; - revertDownloadParameters(sessionContext, operation.args.instance, err => { - if (err) { - if (counter & 1) callback(err); - return void (counter = 0); - } - if ((counter -= 2) === 1) callback(null, faults, operations); - }); + revertDownloadParameters(sessionContext, operation.args.instance); } } - if ((counter -= 2) === 1) callback(null, faults, operations); + + return { faults, operations }; } export function addProvisions( @@ -569,233 +570,189 @@ export function clearProvisions(sessionContext: SessionContext): void { sessionContext.extensionsCache = {}; } -function runProvisions( +async function runProvisions( sessionContext: SessionContext, provisions: any[][], startRevision: number, - endRevision: number, - callback: ( - err: Error, - fault?: Fault, - ret?: any, - decs?: Declaration[], - toClear?: Clear[] - ) => void -): void { - let done = true; - let allDeclarations = []; - let allClear = []; - let counter = 3; + endRevision: number +): Promise { const allProvisions = localCache.getProvisions(sessionContext.cacheSnapshot); - for (const [j, provision] of provisions.entries()) { - if (!allProvisions[provision[0]]) { - allDeclarations[j] = []; - allClear[j] = []; - if (defaultProvisions[provision[0]]) { - done = - defaultProvisions[provision[0]]( + const res = await Promise.all( + provisions.map(async provision => { + if (!allProvisions[provision[0]]) { + if (defaultProvisions[provision[0]]) { + const dec = []; + const done = defaultProvisions[provision[0]]( sessionContext, provision, - allDeclarations[j], + dec, startRevision, endRevision - ) && done; + ); + return { + fault: null, + clear: null, + declare: dec, + done: done, + returnValue: null + }; + } + return null; } - continue; - } - counter += 2; + return sandbox.run( + allProvisions[provision[0]].script, + { args: provision.slice(1) }, + sessionContext, + startRevision, + endRevision + ); + }) + ); - sandbox.run( - allProvisions[provision[0]].script, - { args: provision.slice(1) }, - sessionContext, - startRevision, - endRevision, - (err, _fault, _clear, _declarations, _done) => { - if (err || _fault) { - if (counter & 1) callback(err, _fault); - return void (counter = 0); - } - done = done && _done; - allDeclarations[j] = _declarations || []; - allClear[j] = _clear || []; + let done = true; + let allDeclarations = []; + let allClear = []; + let fault; - if ((counter -= 2) === 1) { - allDeclarations = Array.prototype.concat.apply([], allDeclarations); - if (done) for (const d of allDeclarations) d.defer = false; - allClear = Array.prototype.concat.apply([], allClear); - callback(null, null, done, allDeclarations, allClear); - } - } - ); + for (const r of res) { + if (!r) continue; + done = done && r.done; + if (r.declare) allDeclarations = allDeclarations.concat(r.declare); + if (r.clear) allClear = allClear.concat(r.clear); + fault = r.fault || fault; } - if ((counter -= 2) === 1) { - allDeclarations = Array.prototype.concat.apply([], allDeclarations); - if (done) for (const d of allDeclarations) d.defer = false; - allClear = Array.prototype.concat.apply([], allClear); - callback(null, null, done, allDeclarations, allClear); - } + if (done) for (const d of allDeclarations) d.defer = false; + + return { + fault: fault, + clear: allClear, + declare: allDeclarations, + done: done, + returnValue: null + }; } -function runVirtualParameters( +async function runVirtualParameters( sessionContext: SessionContext, provisions: any[][], startRevision: number, - endRevision: number, - callback: ( - err: Error, - fault?: Fault, - ret?: any, - decs?: Declaration[], - toClear?: Clear[] - ) => void -): void { - let done = true; - const virtualParameterUpdates = []; - let allDeclarations = []; - let allClear = []; - let counter = 3; + endRevision: number +): Promise { const allVirtualParameters = localCache.getVirtualParameters( sessionContext.cacheSnapshot ); - for (const [j, provision] of provisions.entries()) { - counter += 2; - const globals = { - args: provision.slice(1) - }; + const res = await Promise.all( + provisions.map(async provision => { + const globals = { + args: provision.slice(1) + }; - sandbox.run( - allVirtualParameters[provision[0]].script, - globals, - sessionContext, - startRevision, - endRevision, - (err, _fault, _clear, _declarations, _done, _returnValue) => { - if (err || _fault) { - if (counter & 1) callback(err, _fault); - return void (counter = 0); + const r = await sandbox.run( + allVirtualParameters[provision[0]].script, + globals, + sessionContext, + startRevision, + endRevision + ); + + if (r.done && !r.fault) { + if (!r.returnValue) { + r.fault = { + code: "script", + message: "Invalid virtual parameter return value" + }; + return r; } - done = done && _done; - allDeclarations[j] = _declarations || []; - allClear[j] = _clear || []; + const ret: { + writable?: boolean; + value?: [string | number | boolean, string?]; + } = {}; - if (_done) { - if (!_returnValue) { - if (counter & 1) { - callback(null, { - code: "script", - message: "Invalid virtual parameter return value" - }); - } + if (r.returnValue.writable != null) { + ret.writable = !!r.returnValue.writable; + } else if ( + provision[1].writable != null || + provision[2].writable != null + ) { + r.fault = { + code: "script", + message: `Virtual parameter '${ + provision[0] + }' must provide 'writable' attribute` + }; + return r; + } - return void (counter = 0); + if (r.returnValue.value != null) { + let v: string | number | boolean, t: string; + + if (Array.isArray(r.returnValue.value)) [v, t] = r.returnValue.value; + else v = r.returnValue.value; + + if (!t) { + if (typeof v === "number") t = "xsd:int"; + else if (typeof v === "boolean") t = "xsd:boolean"; + else if (((v as unknown) as object) instanceof Date) + t = "xsd:datetime"; + else t = "xsd:string"; } - const ret: { - writable?: number; - value?: [string | number | boolean, string?]; - } = {}; - - if (_returnValue.writable != null) { - ret.writable = +!!_returnValue.writable; - } else if ( - provision[1].writable != null || - provision[2].writable != null - ) { - if (counter & 1) { - callback(null, { - code: "script", - message: "Virtual parameter must provide declared attributes" - }); - } - - return void (counter = 0); - } - - if (_returnValue.value != null) { - if (!Array.isArray(_returnValue.value)) - _returnValue.value = [_returnValue.value]; - - if (!_returnValue.value[1]) { - if (typeof _returnValue.value[0] === "number") - _returnValue.value[1] = "xsd:int"; - else if (typeof _returnValue.value[0] === "boolean") - _returnValue.value[1] = "xsd:boolean"; - else if (_returnValue.value[0] instanceof Date) - _returnValue.value[1] = "xsd:dateTime"; - else _returnValue.value[1] = "xsd:string"; - } - - const allowed = { - "xsd:int": 1, - "xsd:unsignedInt": 1, - "xsd:boolean": 1, - "xsd:string": 1, - "xsd:dateTime": 1, - "xsd:base64": 1, - "xsd:hexBinary": 1 + if (v == null || !VALID_PARAM_TYPES.has(t)) { + r.fault = { + code: "script", + message: "Invalid virtual parameter value attribute" }; - - if ( - _returnValue.value[0] == null || - !allowed[_returnValue.value[1]] - ) { - if (counter & 1) { - callback(null, { - code: "script", - message: "Invalid virtual parameter value attribute" - }); - } - - return void (counter = 0); - } - ret.value = device.sanitizeParameterValue(_returnValue.value); - } else if (provision[1].value != null || provision[2].value != null) { - if (counter & 1) { - callback(null, { - code: "script", - message: "Virtual parameter must provide declared attributes" - }); - } - - return void (counter = 0); + return r; } - virtualParameterUpdates[j] = ret; + + ret.value = device.sanitizeParameterValue([v, t]); + } else if (provision[1].value != null || provision[2].value != null) { + r.fault = { + code: "script", + message: `Virtual parameter '${ + provision[0] + }' must provide 'value' attribute` + }; + return r; } - if ((counter -= 2) === 1) { - allDeclarations = Array.prototype.concat.apply([], allDeclarations); - if (done) for (const d of allDeclarations) d.defer = false; - allClear = Array.prototype.concat.apply([], allClear); - callback( - null, - null, - done ? virtualParameterUpdates : null, - allDeclarations, - allClear - ); - } + r.returnValue = ret; } - ); + return r; + }) + ); + + let done = true; + const virtualParameterUpdates = []; + let allDeclarations = []; + let allClear = []; + let fault; + + for (const r of res) { + if (!r) { + virtualParameterUpdates.push(null); + continue; + } + + done = done && r.done; + if (r.declare) allDeclarations = allDeclarations.concat(r.declare); + if (r.clear) allClear = allClear.concat(r.clear); + virtualParameterUpdates.push(r.returnValue); + fault = r.fault || fault; } - if ((counter -= 2) === 1) { - allDeclarations = Array.prototype.concat.apply([], allDeclarations); - if (done) for (const d of allDeclarations) d.defer = false; - allClear = Array.prototype.concat.apply([], allClear); - callback( - null, - null, - done ? virtualParameterUpdates : null, - allDeclarations, - allClear - ); - } + return { + fault: fault, + clear: allClear, + declare: allDeclarations, + done: done, + returnValue: done ? virtualParameterUpdates : null + }; } function runDeclarations( @@ -1010,18 +967,16 @@ function runDeclarations( ); } -export function rpcRequest( +export async function rpcRequest( sessionContext: SessionContext, - _declarations: Declaration[], - callback -): void { + _declarations: Declaration[] +): Promise<{ fault: Fault; rpcId: string; rpc: AcsRequest }> { if (sessionContext.rpcRequest != null) { - return void callback( - null, - null, - generateRpcId(sessionContext), - sessionContext.rpcRequest - ); + return { + fault: null, + rpcId: generateRpcId(sessionContext), + rpc: sessionContext.rpcRequest + }; } if ( @@ -1030,7 +985,7 @@ export function rpcRequest( !(_declarations && _declarations.length) && !sessionContext.provisions.length ) - return void callback(); + return { fault: null, rpcId: null, rpc: null }; if ( sessionContext.declarations.length <= @@ -1050,60 +1005,56 @@ export function rpcRequest( provisions = sessionContext.virtualParameters[inception - 1]; } - return void run( + const { + fault, + clear: toClear, + declare: decs, + done: done, + returnValue: ret + } = await run( sessionContext, provisions, sessionContext.revisions[inception - 1] || 0, - sessionContext.revisions[inception], - (err, fault, ret, decs, toClear) => { - if (err) return void callback(err); - - if (fault) { - fault.timestamp = sessionContext.timestamp; - return void callback(null, fault); - } - - // Enforce max clear timestamp - for (const c of toClear) { - if (c[1] > sessionContext.timestamp) c[1] = sessionContext.timestamp; - - if (c[2]) { - for (const [k, v] of Object.entries(c[2])) { - if (v > sessionContext.timestamp) - c[2][k] = sessionContext.timestamp; - } - } - } - - sessionContext.declarations.push(decs); - sessionContext.provisionsRet[inception] = ret; - - for (const d of decs) { - // Enforce max timestamp - if (d.pathGet > sessionContext.timestamp) - d.pathGet = sessionContext.timestamp; - - if (d.attrGet) { - for (const [k, v] of Object.entries(d.attrGet)) { - if (v > sessionContext.timestamp) - d.attrGet[k] = sessionContext.timestamp; - } - } - - for (const ad of device.getAliasDeclarations(d.path, 1)) - loadPath(sessionContext, ad.path); - } - - clear(sessionContext, toClear, err => { - if (err) return void callback(err); - - loadParameters(sessionContext, err => { - if (err) return void callback(err); - rpcRequest(sessionContext, _declarations, callback); - }); - }); - } + sessionContext.revisions[inception] ); + + if (fault) { + fault.timestamp = sessionContext.timestamp; + return { fault: fault, rpcId: null, rpc: null }; + } + + // Enforce max clear timestamp + for (const c of toClear) { + if (c[1] > sessionContext.timestamp) c[1] = sessionContext.timestamp; + + if (c[2]) { + for (const [k, v] of Object.entries(c[2])) + if (v > sessionContext.timestamp) c[2][k] = sessionContext.timestamp; + } + } + + sessionContext.declarations.push(decs); + sessionContext.provisionsRet[inception] = inception ? ret : done; + + for (const d of decs) { + // Enforce max timestamp + if (d.pathGet > sessionContext.timestamp) + d.pathGet = sessionContext.timestamp; + + if (d.attrGet) { + for (const [k, v] of Object.entries(d.attrGet)) { + if (v > sessionContext.timestamp) + d.attrGet[k] = sessionContext.timestamp; + } + } + + for (const ad of device.getAliasDeclarations(d.path, 1)) + loadPath(sessionContext, ad.path); + } + + await clear(sessionContext, toClear); + await loadParameters(sessionContext); + return rpcRequest(sessionContext, _declarations); } if (_declarations && _declarations.length) { @@ -1118,36 +1069,45 @@ export function rpcRequest( loadPath(sessionContext, ad.path); } - return void loadParameters(sessionContext, err => { - if (err) return void callback(err); - - rpcRequest(sessionContext, null, callback); - }); + await loadParameters(sessionContext); + return rpcRequest(sessionContext, null); } if (sessionContext.rpcCount >= 255) { - return void callback(null, { - code: "too_many_rpcs", - message: "Too many RPC requests", - timestamp: sessionContext.timestamp - }); + return { + fault: { + code: "too_many_rpcs", + message: "Too many RPC requests", + timestamp: sessionContext.timestamp + }, + rpcId: null, + rpc: null + }; } if (sessionContext.revisions.length >= 8) { - return void callback(null, { - code: "deeply_nested_vparams", - message: - "Virtual parameters are referencing other virtual parameters in a deeply nested manner", - timestamp: sessionContext.timestamp - }); + return { + fault: { + code: "deeply_nested_vparams", + message: + "Virtual parameters are referencing other virtual parameters in a deeply nested manner", + timestamp: sessionContext.timestamp + }, + rpcId: null, + rpc: null + }; } if (sessionContext.cycle >= 255) { - return void callback(null, { - code: "too_many_cycles", - message: "Too many provision cycles", - timestamp: sessionContext.timestamp - }); + return { + fault: { + code: "too_many_cycles", + message: "Too many provision cycles", + timestamp: sessionContext.timestamp + }, + rpcId: null, + rpc: null + }; } // Multiply by two because every iteration is two @@ -1160,11 +1120,15 @@ export function rpcRequest( ) * 2; if (sessionContext.iteration >= MAX_ITERATIONS * (sessionContext.cycle + 1)) { - return void callback(null, { - code: "too_many_commits", - message: "Too many commit iterations", - timestamp: sessionContext.timestamp - }); + return { + fault: { + code: "too_many_commits", + message: "Too many commit iterations", + timestamp: sessionContext.timestamp + }, + rpcId: null, + rpc: null + }; } if ( @@ -1268,14 +1232,12 @@ export function rpcRequest( return false; }); - return void clear(sessionContext, toClear, err => { - if (err) return void callback(err); - sessionContext.syncState.virtualParameterDeclarations[inception] = vpd; - rpcRequest(sessionContext, null, callback); - }); + await clear(sessionContext, toClear); + sessionContext.syncState.virtualParameterDeclarations[inception] = vpd; + return rpcRequest(sessionContext, null); } - if (!sessionContext.syncState) return void callback(); + if (!sessionContext.syncState) return { fault: null, rpcId: null, rpc: null }; const inception = sessionContext.declarations.length - 1; @@ -1292,7 +1254,7 @@ export function rpcRequest( if (sessionContext.deviceData.changes.has("prerequisite")) { delete sessionContext.syncState; device.clearTrackers(sessionContext.deviceData, "prerequisite"); - return void rpcRequest(sessionContext, null, callback); + return rpcRequest(sessionContext, null); } let toClear; @@ -1445,10 +1407,8 @@ export function rpcRequest( } if (toClear || sessionContext.deviceData.changes.has("prerequisite")) { - return void clear(sessionContext, toClear, err => { - if (err) return void callback(err); - rpcRequest(sessionContext, null, callback); - }); + await clear(sessionContext, toClear); + return rpcRequest(sessionContext, null); } provisions = generateSetVirtualParameterProvisions( @@ -1463,16 +1423,15 @@ export function rpcRequest( if (provisions) { sessionContext.virtualParameters.push(provisions); sessionContext.revisions.push(sessionContext.revisions[inception]); - return void rpcRequest(sessionContext, null, callback); + return rpcRequest(sessionContext, null); } if (sessionContext.rpcRequest) { - return void callback( - null, - null, - generateRpcId(sessionContext), - sessionContext.rpcRequest - ); + return { + fault: null, + rpcId: generateRpcId(sessionContext), + rpc: sessionContext.rpcRequest + }; } ++sessionContext.revisions[inception]; @@ -1480,7 +1439,7 @@ export function rpcRequest( sessionContext.syncState.virtualParameterDeclarations.pop(); const ret = sessionContext.provisionsRet.splice(inception)[0]; - if (!ret) return void rpcRequest(sessionContext, null, callback); + if (!ret) return rpcRequest(sessionContext, null); sessionContext.revisions.pop(); const rev = @@ -1496,7 +1455,7 @@ export function rpcRequest( } const vparams = sessionContext.virtualParameters.pop(); - if (!vparams) return void callback(); + if (!vparams) return { fault: null, rpcId: null, rpc: null }; const timestamp = sessionContext.timestamp + sessionContext.iteration; let toClear; @@ -1513,10 +1472,8 @@ export function rpcRequest( ); } - clear(sessionContext, toClear, err => { - if (err) return void callback(err); - rpcRequest(sessionContext, null, callback); - }); + await clear(sessionContext, toClear); + return rpcRequest(sessionContext, null); } function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest { @@ -2315,12 +2272,11 @@ function processInstances( } } -function clear( +async function clear( sessionContext: SessionContext, - toClear: Clear[], - callback -): void { - if (!toClear || !toClear.length) return void callback(); + toClear: Clear[] +): Promise { + if (!toClear || !toClear.length) return; const MAX_DEPTH = +config.get("MAX_DEPTH", sessionContext.deviceId); @@ -2343,23 +2299,18 @@ function clear( } } - loadParameters(sessionContext, err => { - if (err) return void callback(err); - for (const c of toClear) - device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]); - - callback(); - }); + await loadParameters(sessionContext); + for (const c of toClear) + device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]); } -export function rpcResponse( +export async function rpcResponse( sessionContext: SessionContext, id: string, - _rpcRes: CpeResponse, - callback -): void { + _rpcRes: CpeResponse +): Promise { if (id !== generateRpcId(sessionContext)) - return void callback(new Error("Request ID not recognized")); + throw new Error("Request ID not recognized"); ++sessionContext.rpcCount; @@ -2426,11 +2377,8 @@ export function rpcResponse( switch (rpcRes.name) { case "GetParameterValuesResponse": - if (rpcReq.name !== "GetParameterValues") { - return void callback( - new Error("Response name does not match request name") - ); - } + if (rpcReq.name !== "GetParameterValues") + throw new Error("Response name does not match request name"); for (const p of rpcRes.parameterList) { toClear = device.set( @@ -2444,11 +2392,8 @@ export function rpcResponse( break; case "GetParameterNamesResponse": - if (rpcReq.name !== "GetParameterNames") { - return void callback( - new Error("Response name does not match request name") - ); - } + if (rpcReq.name !== "GetParameterNames") + throw new Error("Response name does not match request name"); if (rpcReq.parameterPath.endsWith(".")) root = Path.parse(rpcReq.parameterPath.slice(0, -1)); @@ -2518,45 +2463,39 @@ export function rpcResponse( loadPath(sessionContext, root, (1 << MAX_DEPTH) - 1); } - loadParameters(sessionContext, err => { - if (err) return void callback(err); - - if (!root.length) { - for (const n of [ - "DeviceID", - "Events", - "Tags", - "Reboot", - "FactoryReset", - "VirtualParameters", - "Downloads" - ]) { - const p = sessionContext.deviceData.paths.get(Path.parse(n)); - if (p && sessionContext.deviceData.attributes.has(p)) - sessionContext.deviceData.timestamps.set(p, timestamp); - } + await loadParameters(sessionContext); + if (!root.length) { + for (const n of [ + "DeviceID", + "Events", + "Tags", + "Reboot", + "FactoryReset", + "VirtualParameters", + "Downloads" + ]) { + const p = sessionContext.deviceData.paths.get(Path.parse(n)); + if (p && sessionContext.deviceData.attributes.has(p)) + sessionContext.deviceData.timestamps.set(p, timestamp); } + } - for (const p of params) { - toClear = device.set( - sessionContext.deviceData, - p[0], - p[1], - p[2], - toClear - ); - } - - clear(sessionContext, toClear, callback); - }); - return; - case "SetParameterValuesResponse": - if (rpcReq.name !== "SetParameterValues") { - return void callback( - new Error("Response name does not match request name") + for (const p of params) { + toClear = device.set( + sessionContext.deviceData, + p[0], + p[1], + p[2], + toClear ); } + await clear(sessionContext, toClear); + return; + case "SetParameterValuesResponse": + if (rpcReq.name !== "SetParameterValues") + throw new Error("Response name does not match request name"); + for (const p of rpcReq.parameterList) { toClear = device.set( sessionContext.deviceData, @@ -2694,17 +2633,16 @@ export function rpcResponse( } break; default: - return void callback(new Error("Response name not recognized")); + throw new Error("Response name not recognized"); } - clear(sessionContext, toClear, callback); + await clear(sessionContext, toClear); } -export function rpcFault( +export async function rpcFault( sessionContext: SessionContext, id: string, - faultResponse: CpeFault, - callback: (err: Error, fault?: Fault) => void -): void { + faultResponse: CpeFault +): Promise { const rpcReq = sessionContext.rpcRequest as GetAcsRequest & SetAcsRequest; delete sessionContext.syncState; delete sessionContext.rpcRequest; @@ -2739,7 +2677,8 @@ export function rpcFault( toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]]; } - if (toClear) return void clear(sessionContext, toClear, callback); + if (toClear) await clear(sessionContext, toClear); + return null; } const fault: Fault = { @@ -2748,10 +2687,12 @@ export function rpcFault( detail: faultResponse.detail }; - callback(null, fault); + return fault; } -export function deserialize(sessionContextString, callback): void { +export async function deserialize( + sessionContextString +): Promise { const sessionContext = JSON.parse(sessionContextString); for (const decs of sessionContext.declarations) @@ -2773,12 +2714,14 @@ export function deserialize(sessionContextString, callback): void { sessionContext.deviceData = deviceData; // Ensure cache is populated - localCache.getCurrentSnapshot(err => { - callback(err, sessionContext); - }); + await localCache.getCurrentSnapshot(); + + return sessionContext; } -export function serialize(sessionContext: SessionContext, callback): void { +export async function serialize( + sessionContext: SessionContext +): Promise { const deviceData = []; for (const path of sessionContext.deviceData.paths.find( @@ -2807,5 +2750,5 @@ export function serialize(sessionContext: SessionContext, callback): void { const sessionContextString = JSON.stringify(jsonSessionContext); - callback(null, sessionContextString); + return sessionContextString; } diff --git a/lib/soap.ts b/lib/soap.ts index c4a963b..166c9a9 100644 --- a/lib/soap.ts +++ b/lib/soap.ts @@ -21,7 +21,6 @@ import { parseXml, Document, Element, ParserOptions } from "libxmljs"; import * as config from "./config"; import { version as VERSION } from "../package.json"; import { - CpeResponse, CpeGetResponse, CpeSetResponse, InformRequest, @@ -29,7 +28,9 @@ import { CpeRequest, FaultStruct, SpvFault, - CpeFault + CpeFault, + SoapMessage, + TransferCompleteRequest } from "./types"; const SERVER_NAME = `GenieACS/${VERSION}`; @@ -444,7 +445,7 @@ function GetRPCMethodsResponse(xml, methodResponse): void { for (const m of methodResponse.methodList) el.node("string").text(m); } -function TransferComplete(xml): AcsResponse { +function TransferComplete(xml): TransferCompleteRequest { let commandKey, _faultStruct, startTime, completeTime; for (const c of xml.childNodes()) { switch (c.name()) { @@ -545,18 +546,7 @@ function fault(xml): CpeFault { return { faultCode, faultString, detail }; } -export function request( - data, - cwmpVersion, - warn -): { - id: string; - cwmpVersion: string; - sessionTimeout: number; - cpeRequest?: CpeRequest; - cpeFault?: CpeFault; - cpeResponse?: CpeResponse; -} { +export function request(data, cwmpVersion, warn): SoapMessage { warnings = warn; const rpc = { diff --git a/lib/types.ts b/lib/types.ts index 5f3229f..7775ff0 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -16,6 +16,7 @@ export interface Fault { message: string; stack?: string; }; + timestamp?: number; } export interface SessionFault extends Fault { @@ -255,14 +256,20 @@ export interface InformRequest extends CpeRequest { parameterList: [string, string | number | boolean, string][]; } -export interface AcsResponse { - name: string; +export interface TransferCompleteRequest extends CpeRequest { + name: "TransferComplete"; commandKey?: string; faultStruct?: FaultStruct; startTime?: number; completeTime?: number; } +export interface AcsResponse { + name: string; + commandKey?: string; + faultStruct?: FaultStruct; +} + export interface QueryOptions { projection?: any; skip?: number; @@ -308,3 +315,20 @@ export type PermissionSet = { filter: Expression; }; }[]; + +export interface SoapMessage { + id: string; + cwmpVersion: string; + sessionTimeout: number; + cpeRequest?: CpeRequest; + cpeFault?: CpeFault; + cpeResponse?: CpeResponse; +} + +export interface ScriptResult { + fault: Fault; + clear: Clear[]; + declare: Declaration[]; + done: boolean; + returnValue: any; +} diff --git a/lib/ui/api-functions.ts b/lib/ui/api-functions.ts index a3d13d9..9060896 100644 --- a/lib/ui/api-functions.ts +++ b/lib/ui/api-functions.ts @@ -11,144 +11,110 @@ import { deleteDevice } from "../api-functions"; -function deleteFault(id): Promise { +async function deleteFault(id): Promise { const deviceId = id.split(":", 1)[0]; const channel = id.slice(deviceId.length + 1); - return new Promise((resolve, reject) => { - Promise.all([ - db.deleteFault(id), - channel.startsWith("task_") - ? db.deleteTask(new ObjectID(channel.slice(5))) - : null - ]) - .then(() => { - del(`${deviceId}_tasks_faults_operations`, err => { - err ? reject(err) : resolve(); - }); - }) - .catch(reject); - }); + + await Promise.all([ + db.deleteFault(id), + channel.startsWith("task_") + ? db.deleteTask(new ObjectID(channel.slice(5))) + : null + ]); + + await del(`${deviceId}_tasks_faults_operations`); } -export function deleteResource(resource, id): Promise { - return new Promise((resolve, reject) => { - let promise; +export async function deleteResource(resource, id): Promise { + switch (resource) { + case "devices": + await deleteDevice(id); + break; - switch (resource) { - case "devices": - promise = new Promise((res, rej) => { - deleteDevice(id, err => { - if (err) return void rej(err); - res(); - }); - }); - break; + case "files": + await db.deleteFile(id); + break; - case "files": - promise = db.deleteFile(id); - break; + case "faults": + await deleteFault(id); + break; - case "faults": - promise = deleteFault(id); - break; + case "provisions": + await db.deleteProvision(id); + break; - case "provisions": - promise = db.deleteProvision(id); - break; + case "presets": + await db.deletePreset(id); + break; - case "presets": - promise = db.deletePreset(id); - break; + case "virtualParameters": + await db.deleteVirtualParameter(id); + break; - case "virtualParameters": - promise = db.deleteVirtualParameter(id); - break; + case "config": + await db.deleteConfig(id); + break; - case "config": - promise = db.deleteConfig(id); - break; + case "permissions": + await db.deletePermission(id); + break; - case "permissions": - promise = db.deletePermission(id); - break; + case "users": + await db.deleteUser(id); + break; + } - case "users": - promise = db.deleteUser(id); - break; - } - - promise - .then(() => { - del("presets_hash", err => { - if (err) return void reject(err); - resolve(); - }); - }) - .catch(reject); - }); + await del("presets_hash"); } -export function postTasks( +export async function postTasks( deviceId, tasks, timeout ): Promise<{ connectionRequest: string; tasks: any[] }> { - return new Promise((resolve, reject) => { - for (const task of tasks) task.device = deviceId; + for (const task of tasks) task.device = deviceId; - insertTasks(tasks, err => { - if (err) return void reject(err); - const statuses = tasks.map(t => { - return { _id: t._id, status: "pending" }; - }); - del(`${deviceId}_tasks_faults_operations`, err => { - if (err) return void reject(err); - connectionRequest(deviceId, err => { - if (err) { - return void resolve({ - connectionRequest: err.message, - tasks: statuses - }); - } - - const sample = tasks[tasks.length - 1]; - - // Waiting for session to finish or timeout - watchTask(deviceId, sample._id, timeout, err => { - if (err) return void reject(err); - - const promises2 = []; - for (const s of statuses) { - promises2.push(db.query("tasks", ["=", ["PARAM", "_id"], s._id])); - promises2.push( - db.query("faults", [ - "=", - ["PARAM", "_id"], - `${deviceId}:task_${s._id}` - ]) - ); - } - - Promise.all(promises2) - .then(res => { - for (const [i, r] of statuses.entries()) { - if (res[i * 2].length === 0) { - r.status = "done"; - } else if (res[i * 2 + 1].length === 1) { - r.status = "fault"; - r.fault = res[i * 2 + 1][0]; - } - db.deleteTask(r._id); - } - - resolve({ connectionRequest: "OK", tasks: statuses }); - }) - .catch(reject); - }); - }); - }); - }); + tasks = await insertTasks(tasks); + const statuses = tasks.map(t => { + return { _id: t._id, status: "pending" }; }); + + await del(`${deviceId}_tasks_faults_operations`); + + try { + await connectionRequest(deviceId); + } catch (err) { + return { + connectionRequest: err.message, + tasks: statuses + }; + } + + const sample = tasks[tasks.length - 1]; + + // Waiting for session to finish or timeout + await watchTask(deviceId, sample._id, timeout); + + const promises = []; + for (const s of statuses) { + promises.push(db.query("tasks", ["=", ["PARAM", "_id"], s._id])); + promises.push( + db.query("faults", ["=", ["PARAM", "_id"], `${deviceId}:task_${s._id}`]) + ); + } + + const res = await Promise.all(promises); + for (const [i, r] of statuses.entries()) { + if (res[i * 2].length === 0) { + r.status = "done"; + } else if (res[i * 2 + 1].length === 1) { + r.status = "fault"; + r.fault = res[i * 2 + 1][0]; + } + db.deleteTask(r._id); + } + + return { connectionRequest: "OK", tasks: statuses }; } interface PingResponse { @@ -184,35 +150,24 @@ export function ping(host): Promise { }); } -export function putResource(resource, id, data): Promise { - return new Promise((resolve, reject) => { - let promise; +export async function putResource(resource, id, data): Promise { + if (resource === "presets") { + await db.putPreset(id, data); + } else if (resource === "provisions") { + await db.putProvision(id, data); + } else if (resource === "virtualParameters") { + await db.putVirtualParameter(id, data); + } else if (resource === "config") { + await db.putConfig(id, data); + } else if (resource === "permissions") { + await db.putPermission(id, data); + } else if (resource === "users") { + delete data.password; + delete data.salt; + await db.putUser(id, data); + } - if (resource === "presets") { - promise = db.putPreset(id, data); - } else if (resource === "provisions") { - promise = db.putProvision(id, data); - } else if (resource === "virtualParameters") { - promise = db.putVirtualParameter(id, data); - } else if (resource === "config") { - promise = db.putConfig(id, data); - } else if (resource === "permissions") { - promise = db.putPermission(id, data); - } else if (resource === "users") { - delete data.password; - delete data.salt; - promise = db.putUser(id, data); - } - - promise - .then(() => { - del("presets_hash", err => { - if (err) return void reject(err); - resolve(); - }); - }) - .catch(reject); - }); + await del("presets_hash"); } export function authSimple(snapshot, username, password): Promise { diff --git a/lib/ui/api.ts b/lib/ui/api.ts index 75f421b..1b2baec 100644 --- a/lib/ui/api.ts +++ b/lib/ui/api.ts @@ -546,16 +546,7 @@ router.put("/users/:id/password", async (ctx, next) => { const password = await hashPassword(newPassword, salt); await db.putUser(username, { password, salt }); - function delCache(): Promise { - return new Promise((resolve, reject) => { - del("presets_hash", err => { - if (err) return void reject(err); - resolve(); - }); - }); - } - - await delCache(); + await del("presets_hash"); logger.accessInfo(log); ctx.body = "";