Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6733cef5dd | ||
|
|
96add7efe2 | ||
|
|
d311dad861 | ||
|
|
4a55f56e48 | ||
|
|
601690e3b5 | ||
|
|
765f0be7bc | ||
|
|
ab27ebfaef | ||
|
|
f135be403c | ||
|
|
c07f57af1f | ||
|
|
df5e4cf454 | ||
|
|
85b5e12c94 | ||
|
|
8533ee7f2e | ||
|
|
77563fc85e | ||
|
|
0e02b7c7ea | ||
|
|
f966194e22 | ||
|
|
306015f022 | ||
|
|
20857549d0 | ||
|
|
e31d74bd45 | ||
|
|
5c6d85025c | ||
|
|
209c0f7620 | ||
|
|
559aa0fd36 | ||
|
|
ed0aa8ae7a | ||
|
|
4bcee572f1 | ||
|
|
3d38de74d7 | ||
|
|
5b5346f858 | ||
|
|
f57bcae70c | ||
|
|
1c9aa493c6 | ||
|
|
6f0e1d66ce | ||
|
|
b5efe6c578 | ||
|
|
fd34dd8839 | ||
|
|
17889cf41c | ||
|
|
c2e8585801 | ||
|
|
46e009afab | ||
|
|
78eec1b5ca | ||
|
|
59cd930b6c | ||
|
|
db3736e60b | ||
|
|
d10fa2e073 |
No files matched your search
+27
-22
@@ -35,10 +35,13 @@ const SERVICE_ADDRESS = config.get("CWMP_INTERFACE");
|
|||||||
const SERVICE_PORT = config.get("CWMP_PORT");
|
const SERVICE_PORT = config.get("CWMP_PORT");
|
||||||
|
|
||||||
function exitWorkerGracefully(): void {
|
function exitWorkerGracefully(): void {
|
||||||
db.disconnect();
|
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||||
cache.disconnect();
|
Promise.all([
|
||||||
extensions.killAll();
|
db.disconnect(),
|
||||||
cluster.worker.disconnect();
|
cache.disconnect(),
|
||||||
|
extensions.killAll(),
|
||||||
|
cluster.worker.disconnect()
|
||||||
|
]).catch(exitWorkerUngracefully);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitWorkerUngracefully(): void {
|
function exitWorkerUngracefully(): void {
|
||||||
@@ -81,8 +84,6 @@ if (!cluster.worker) {
|
|||||||
cert: config.get("CWMP_SSL_CERT")
|
cert: config.get("CWMP_SSL_CERT")
|
||||||
};
|
};
|
||||||
|
|
||||||
const keepAliveTimeout = config.get("CWMP_KEEP_ALIVE_TIMEOUT") as number;
|
|
||||||
|
|
||||||
process.on("uncaughtException", err => {
|
process.on("uncaughtException", err => {
|
||||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
logger.error({
|
logger.error({
|
||||||
@@ -96,21 +97,7 @@ if (!cluster.worker) {
|
|||||||
.catch(exitWorkerUngracefully);
|
.catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
const initPromise = Promise.all([db.connect(), cache.connect()])
|
||||||
server
|
|
||||||
.stop()
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
server
|
|
||||||
.stop()
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all([db.connect(), cache.connect()])
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
server.start(
|
server.start(
|
||||||
SERVICE_PORT,
|
SERVICE_PORT,
|
||||||
@@ -118,7 +105,7 @@ if (!cluster.worker) {
|
|||||||
ssl,
|
ssl,
|
||||||
cwmp.listener,
|
cwmp.listener,
|
||||||
cwmp.onConnection,
|
cwmp.onConnection,
|
||||||
keepAliveTimeout
|
0
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -126,4 +113,22 @@ if (!cluster.worker) {
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
+25
-18
@@ -34,9 +34,12 @@ const SERVICE_ADDRESS = config.get("FS_INTERFACE");
|
|||||||
const SERVICE_PORT = config.get("FS_PORT");
|
const SERVICE_PORT = config.get("FS_PORT");
|
||||||
|
|
||||||
function exitWorkerGracefully(): void {
|
function exitWorkerGracefully(): void {
|
||||||
db.disconnect();
|
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||||
cache.disconnect();
|
Promise.all([
|
||||||
cluster.worker.disconnect();
|
db.disconnect(),
|
||||||
|
cache.disconnect(),
|
||||||
|
cluster.worker.disconnect()
|
||||||
|
]).catch(exitWorkerUngracefully);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitWorkerUngracefully(): void {
|
function exitWorkerUngracefully(): void {
|
||||||
@@ -90,21 +93,7 @@ if (!cluster.worker) {
|
|||||||
.catch(exitWorkerUngracefully);
|
.catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
const initPromise = Promise.all([db.connect(), cache.connect()])
|
||||||
server
|
|
||||||
.stop()
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
server
|
|
||||||
.stop()
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all([db.connect(), cache.connect()])
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
||||||
})
|
})
|
||||||
@@ -113,4 +102,22 @@ if (!cluster.worker) {
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
+26
-19
@@ -35,10 +35,13 @@ const SERVICE_ADDRESS = config.get("NBI_INTERFACE");
|
|||||||
const SERVICE_PORT = config.get("NBI_PORT");
|
const SERVICE_PORT = config.get("NBI_PORT");
|
||||||
|
|
||||||
function exitWorkerGracefully(): void {
|
function exitWorkerGracefully(): void {
|
||||||
db.disconnect();
|
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||||
cache.disconnect();
|
Promise.all([
|
||||||
extensions.killAll();
|
db.disconnect(),
|
||||||
cluster.worker.disconnect();
|
cache.disconnect(),
|
||||||
|
extensions.killAll(),
|
||||||
|
cluster.worker.disconnect()
|
||||||
|
]).catch(exitWorkerUngracefully);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitWorkerUngracefully(): void {
|
function exitWorkerUngracefully(): void {
|
||||||
@@ -94,21 +97,7 @@ if (!cluster.worker) {
|
|||||||
.catch(exitWorkerUngracefully);
|
.catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
const initPromise = Promise.all([db.connect(), cache.connect()])
|
||||||
server
|
|
||||||
.stop()
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
server
|
|
||||||
.stop()
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.all([db.connect(), cache.connect()])
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
||||||
})
|
})
|
||||||
@@ -117,4 +106,22 @@ if (!cluster.worker) {
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
+29
-22
@@ -36,11 +36,14 @@ const SERVICE_ADDRESS = config.get("UI_INTERFACE");
|
|||||||
const SERVICE_PORT = config.get("UI_PORT");
|
const SERVICE_PORT = config.get("UI_PORT");
|
||||||
|
|
||||||
function exitWorkerGracefully(): void {
|
function exitWorkerGracefully(): void {
|
||||||
db.disconnect();
|
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||||
db2.disconnect();
|
Promise.all([
|
||||||
cache.disconnect();
|
db.disconnect(),
|
||||||
extensions.killAll();
|
db2.disconnect(),
|
||||||
cluster.worker.disconnect();
|
cache.disconnect(),
|
||||||
|
extensions.killAll(),
|
||||||
|
cluster.worker.disconnect()
|
||||||
|
]).catch(exitWorkerUngracefully);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitWorkerUngracefully(): void {
|
function exitWorkerUngracefully(): void {
|
||||||
@@ -99,28 +102,12 @@ if (!cluster.worker) {
|
|||||||
.catch(exitWorkerUngracefully);
|
.catch(exitWorkerUngracefully);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
stopping = true;
|
|
||||||
server
|
|
||||||
.stop()
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
stopping = true;
|
|
||||||
server
|
|
||||||
.stop()
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
const _listener = (req, res): void => {
|
const _listener = (req, res): void => {
|
||||||
if (stopping) res.setHeader("Connection", "close");
|
if (stopping) res.setHeader("Connection", "close");
|
||||||
listener(req, res);
|
listener(req, res);
|
||||||
};
|
};
|
||||||
|
|
||||||
Promise.all([db2.connect(), cache.connect()])
|
const initPromise = Promise.all([db2.connect(), cache.connect()])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
|
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
|
||||||
})
|
})
|
||||||
@@ -129,4 +116,24 @@ if (!cluster.worker) {
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
stopping = true;
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
stopping = true;
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,7 @@ const externals = [
|
|||||||
"child_process",
|
"child_process",
|
||||||
"dgram",
|
"dgram",
|
||||||
"url",
|
"url",
|
||||||
|
"iconv-lite",
|
||||||
"koa",
|
"koa",
|
||||||
"koa-router",
|
"koa-router",
|
||||||
"koa-compress",
|
"koa-compress",
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.. _administration-faq:
|
||||||
|
|
||||||
|
Administration FAQ
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. _administration-faq-duplicate-log-entries:
|
||||||
|
|
||||||
|
Duplicate log entries when using :func:`log()` function
|
||||||
|
-------------------------------------------------------
|
||||||
|
|
||||||
|
Because GenieACS uses a full fledged scripting language for device
|
||||||
|
configuration, the only way to guarantee that it has satisfied the 'desired
|
||||||
|
state' is by repeatedly executing the script until there's no more
|
||||||
|
discrepancies with the current device state. Though it may seem like this will
|
||||||
|
cause duplicate requests going to the device, this isn't actually the case
|
||||||
|
because device configuration are stated declaratively and that the scripts
|
||||||
|
themselves are pure functions in the context of a session (e.g. Date.now()
|
||||||
|
always returns the same value within the session).
|
||||||
|
|
||||||
|
To illustrate with an example, consider the following script:
|
||||||
|
|
||||||
|
.. code:: javascript
|
||||||
|
|
||||||
|
log("Executing script");
|
||||||
|
declare("Device.param", null, {value: 1});
|
||||||
|
commit();
|
||||||
|
declare("Device.param", null, {value: 2});
|
||||||
|
|
||||||
|
This will set the value of the 'Device.param' to 1, then to 2. Then as the
|
||||||
|
script is run again the value is set back to 1 and so on. A stable state will
|
||||||
|
never be reached so GenieACS will execute the script a few times until it gives
|
||||||
|
up and throws a fault. This is an edge case that should be avoided. A more
|
||||||
|
typical case is where the script is run once or twice. Essentially if an
|
||||||
|
execution doesn't result in any request to the CPE or a change in the data
|
||||||
|
model then a stable state is deemed to have been reached.
|
||||||
|
|
||||||
|
Configurations not pushed to device after factory reset
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
After a device is reset to its factory default state, the cached data model in
|
||||||
|
GenieACS's database needs to be invalidated to force rediscovery. Ensure the
|
||||||
|
following lines are called on ``0 BOOTSTRAP`` event:
|
||||||
|
|
||||||
|
.. code:: javascript
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Clear cached data model to force a refresh
|
||||||
|
clear("Device", now);
|
||||||
|
clear("InternetGatewayDevice", now);
|
||||||
|
|
||||||
|
|
||||||
|
Most device parameters are missing
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
For performance reasons (server, client, and network), GenieACS by default only
|
||||||
|
fetches parts of the data model that are necessary to satisfy the declarations
|
||||||
|
in your provision scripts. Create declarations for any parameters you need
|
||||||
|
fetched by default.
|
||||||
|
|
||||||
|
If you're unsure and want to explore the available parameters exposed by the
|
||||||
|
device, refresh the root parameter (e.g. ``InternetGatewayDevice``) from
|
||||||
|
GenieACS's UI. You typically only need to do that one time for a given CPE
|
||||||
|
model.
|
||||||
@@ -415,6 +415,15 @@ parameterValues key. For example:
|
|||||||
-X POST \
|
-X POST \
|
||||||
--data '{"name":"addObject","objectName":"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection"}'
|
--data '{"name":"addObject","objectName":"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection"}'
|
||||||
|
|
||||||
|
``deleteObject``
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
curl -i 'http://localhost:7557/devices/00236a-SR552n-SR552NA084%252D0003269/tasks?timeout=3000&connection_request' \
|
||||||
|
-X POST \
|
||||||
|
--data '{"name":"deleteObject","objectName":"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1"}'
|
||||||
|
|
||||||
``reboot``
|
``reboot``
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,214 @@
|
|||||||
Environment Variables
|
Environment Variables
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
TODO
|
Configuring GenieACS services can be done through the following environment
|
||||||
|
variables:
|
||||||
|
|
||||||
|
.. attention::
|
||||||
|
|
||||||
|
All GenieACS environment variables must be prefixed with ``GENIEACS_``.
|
||||||
|
|
||||||
|
MONGODB_CONNECTION_URL
|
||||||
|
MongoDB connection string.
|
||||||
|
|
||||||
|
Default: ``mongodb://127.0.0.1/genieacs``
|
||||||
|
|
||||||
|
EXT_DIR
|
||||||
|
The directory from which to look up extension scripts.
|
||||||
|
|
||||||
|
Default: ``<installation dir>/config/ext``
|
||||||
|
|
||||||
|
EXT_TIMEOUT
|
||||||
|
Timeout (in milliseconds) to allow for calls to extensions to return a
|
||||||
|
response.
|
||||||
|
|
||||||
|
Default: ``3000``
|
||||||
|
|
||||||
|
DEBUG_FILE
|
||||||
|
File to dump CPE debug log.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
DEBUG_FORMAT
|
||||||
|
Debug log format. Valid values are 'yaml' and 'json'.
|
||||||
|
|
||||||
|
Default: ``yaml``
|
||||||
|
|
||||||
|
LOG_FORMAT
|
||||||
|
The format used for the log entries in ``CWMP_LOG_FILE``, ``NBI_LOG_FILE``,
|
||||||
|
``FS_LOG_FILE``, and ``UI_LOG_FILE``. Possible values are ``simple`` and
|
||||||
|
``json``.
|
||||||
|
|
||||||
|
Default: ``simple``
|
||||||
|
|
||||||
|
ACCESS_LOG_FORMAT
|
||||||
|
The format used for the log entries in ``CWMP_ACCESS_LOG_FILE``,
|
||||||
|
``NBI_ACCESS_LOG_FILE``, ``FS_ACCESS_LOG_FILE``, and ``UI_ACCESS_LOG_FILE``.
|
||||||
|
Possible values are ``simple`` and ``json``.
|
||||||
|
|
||||||
|
Default: ``simple``
|
||||||
|
|
||||||
|
CWMP_WORKER_PROCESSES
|
||||||
|
The number of worker processes to spawn for genieacs-cwmp. A value of 0 means
|
||||||
|
as many as there are CPU cores available.
|
||||||
|
|
||||||
|
Default: ``0``
|
||||||
|
|
||||||
|
CWMP_PORT
|
||||||
|
The TCP port that genieacs-cwmp listens on.
|
||||||
|
|
||||||
|
Default: ``7547``
|
||||||
|
|
||||||
|
CWMP_INTERFACE
|
||||||
|
The network interface that genieacs-cwmp binds to.
|
||||||
|
|
||||||
|
Default: ``0.0.0.0``
|
||||||
|
|
||||||
|
CWMP_SSL_CERT
|
||||||
|
Path to certificate file. If omitted, non-secure HTTP will be used.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
CWMP_SSL_KEY
|
||||||
|
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
CWMP_LOG_FILE
|
||||||
|
File to log process related events for genieacs-cwmp. If omitted, logs will
|
||||||
|
go to stderr.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
CWMP_ACCESS_LOG_FILE
|
||||||
|
File to log incoming requests for genieacs-cwmp. If omitted, logs will go to
|
||||||
|
stdout.
|
||||||
|
|
||||||
|
Default: unest
|
||||||
|
|
||||||
|
NBI_WORKER_PROCESSES
|
||||||
|
The number of worker processes to spawn for genieacs-nbi. A value of 0 means
|
||||||
|
as many as there are CPU cores available.
|
||||||
|
|
||||||
|
Default: ``0``
|
||||||
|
|
||||||
|
NBI_PORT
|
||||||
|
The TCP port that genieacs-nbi listens on.
|
||||||
|
|
||||||
|
Default: ``7557``
|
||||||
|
|
||||||
|
NBI_INTERFACE
|
||||||
|
The network interface that genieacs-nbi binds to.
|
||||||
|
|
||||||
|
Default: ``0.0.0.0``
|
||||||
|
|
||||||
|
NBI_SSL_CERT
|
||||||
|
Path to certificate file. If omitted, non-secure HTTP will be used.
|
||||||
|
|
||||||
|
Default: unest
|
||||||
|
|
||||||
|
NBI_SSL_KEY
|
||||||
|
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
NBI_LOG_FILE
|
||||||
|
File to log process related events for genieacs-nbi. If omitted, logs will go
|
||||||
|
to stderr.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
NBI_ACCESS_LOG_FILE
|
||||||
|
File to log incoming requests for genieacs-nbi. If omitted, logs will go to
|
||||||
|
stdout.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
FS_WORKER_PROCESSES
|
||||||
|
The number of worker processes to spawn for genieacs-fs. A value of 0 means
|
||||||
|
as many as there are CPU cores available.
|
||||||
|
|
||||||
|
Default: ``0``
|
||||||
|
|
||||||
|
FS_PORT
|
||||||
|
The TCP port that genieacs-fs listens on.
|
||||||
|
|
||||||
|
Default: ``7567``
|
||||||
|
|
||||||
|
FS_INTERFACE
|
||||||
|
The network interface that genieacs-fs binds to.
|
||||||
|
|
||||||
|
Default: ``0.0.0.0``
|
||||||
|
|
||||||
|
FS_SSL_CERT
|
||||||
|
Path to certificate file. If omitted, non-secure HTTP will be used.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
FS_SSL_KEY
|
||||||
|
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
FS_LOG_FILE
|
||||||
|
File to log process related events for genieacs-fs. If omitted, logs will go
|
||||||
|
to stderr.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
FS_ACCESS_LOG_FILE
|
||||||
|
File to log incoming requests for genieacs-fs. If omitted, logs will go to
|
||||||
|
stdout.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
FS_HOSTNAME
|
||||||
|
The hostname to use when generating the file URL for TR-069 Download
|
||||||
|
requests. Set this if the hostname where genieacs-fs is listening differs
|
||||||
|
from the hostname in the ACS URL.
|
||||||
|
|
||||||
|
Default: the hostname from the ACS URL
|
||||||
|
|
||||||
|
UI_WORKER_PROCESSES
|
||||||
|
The number of worker processes to spawn for genieacs-ui. A value of 0 means
|
||||||
|
as many as there are CPU cores available.
|
||||||
|
|
||||||
|
Default: ``0``
|
||||||
|
|
||||||
|
UI_PORT
|
||||||
|
The TCP port that genieacs-ui listens on.
|
||||||
|
|
||||||
|
Default: ``3000``
|
||||||
|
|
||||||
|
UI_INTERFACE
|
||||||
|
The network interface that genieacs-ui binds to.
|
||||||
|
|
||||||
|
Default: ``0.0.0.0``
|
||||||
|
|
||||||
|
UI_SSL_CERT
|
||||||
|
Path to certificate file. If omitted, non-secure HTTP will be used.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
UI_SSL_KEY
|
||||||
|
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
UI_LOG_FILE
|
||||||
|
File to log process related events for genieacs-ui. If omitted, logs will go
|
||||||
|
to stderr.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
UI_ACCESS_LOG_FILE
|
||||||
|
File to log incoming requests for genieacs-ui. If omitted, logs will go to
|
||||||
|
stdout.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
|
|
||||||
|
UI_JWT_SECRET
|
||||||
|
The key used for signing JWT tokens that are stored in browser cookies. The
|
||||||
|
string can be up to 64 characters in length.
|
||||||
|
|
||||||
|
Default: unset
|
||||||
@@ -30,6 +30,7 @@ Welcome to GenieACS's documentation!
|
|||||||
|
|
||||||
provisions
|
provisions
|
||||||
virtual-parameters
|
virtual-parameters
|
||||||
|
administration-faq
|
||||||
|
|
||||||
.. raw:: latex
|
.. raw:: latex
|
||||||
|
|
||||||
|
|||||||
+19
-17
@@ -34,7 +34,7 @@ Install GenieACS
|
|||||||
|
|
||||||
.. parsed-literal::
|
.. parsed-literal::
|
||||||
|
|
||||||
sudo npm install -g genieacs@\ |release|
|
sudo npm install -g --unsafe-perm genieacs@\ |release|
|
||||||
|
|
||||||
.. topic:: Installing from source
|
.. topic:: Installing from source
|
||||||
|
|
||||||
@@ -68,11 +68,12 @@ Configure systemd
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
GENIEACS_CWMP_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-cmwp-access.log
|
GENIEACS_CWMP_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-cwmp-access.log
|
||||||
GENIEACS_NBI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-nbi-access.log
|
GENIEACS_NBI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-nbi-access.log
|
||||||
GENIEACS_FS_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-fs-access.log
|
GENIEACS_FS_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-fs-access.log
|
||||||
GENIEACS_UI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-ui-access.log
|
GENIEACS_UI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-ui-access.log
|
||||||
GENIEACS_DEBUG_FILE=/var/log/genieacs/genieacs-debug.yaml
|
GENIEACS_DEBUG_FILE=/var/log/genieacs/genieacs-debug.yaml
|
||||||
|
GENIEACS_EXT_DIR=/opt/genieacs/ext
|
||||||
GENIEACS_UI_JWT_SECRET=secret
|
GENIEACS_UI_JWT_SECRET=secret
|
||||||
|
|
||||||
Set file ownership and permissions:
|
Set file ownership and permissions:
|
||||||
@@ -82,7 +83,7 @@ Configure systemd
|
|||||||
sudo chown genieacs:genieacs /opt/genieacs/genieacs.env
|
sudo chown genieacs:genieacs /opt/genieacs/genieacs.env
|
||||||
sudo chmod 600 /opt/genieacs/genieacs.env
|
sudo chmod 600 /opt/genieacs/genieacs.env
|
||||||
|
|
||||||
.. topic:: Create logs directory
|
.. topic:: Create logs directory
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
@@ -100,11 +101,16 @@ Configure systemd
|
|||||||
:file:`/var/log/genieacs/` while process logs go to *journald*. Use
|
:file:`/var/log/genieacs/` while process logs go to *journald*. Use
|
||||||
``journalctl`` command to view process logs.
|
``journalctl`` command to view process logs.
|
||||||
|
|
||||||
|
.. attention::
|
||||||
|
|
||||||
|
If the command :command:`systemctl edit --force --full` fails, you can
|
||||||
|
create the unit file manually.
|
||||||
|
|
||||||
1. Run the following command to create ``genieacs-cwmp`` service:
|
1. Run the following command to create ``genieacs-cwmp`` service:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
sudo systemctl edit --force genieacs-cwmp
|
sudo systemctl edit --force --full genieacs-cwmp
|
||||||
|
|
||||||
Then paste the following in the editor and save:
|
Then paste the following in the editor and save:
|
||||||
|
|
||||||
@@ -117,8 +123,7 @@ Configure systemd
|
|||||||
[Service]
|
[Service]
|
||||||
User=genieacs
|
User=genieacs
|
||||||
EnvironmentFile=/opt/genieacs/genieacs.env
|
EnvironmentFile=/opt/genieacs/genieacs.env
|
||||||
ExecStart=genieacs-cwmp
|
ExecStart=/usr/bin/genieacs-cwmp
|
||||||
KillMode=process
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
@@ -127,7 +132,7 @@ Configure systemd
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
sudo systemctl edit --force genieacs-nbi
|
sudo systemctl edit --force --full genieacs-nbi
|
||||||
|
|
||||||
Then paste the following in the editor and save:
|
Then paste the following in the editor and save:
|
||||||
|
|
||||||
@@ -140,8 +145,7 @@ Configure systemd
|
|||||||
[Service]
|
[Service]
|
||||||
User=genieacs
|
User=genieacs
|
||||||
EnvironmentFile=/opt/genieacs/genieacs.env
|
EnvironmentFile=/opt/genieacs/genieacs.env
|
||||||
ExecStart=genieacs-nbi
|
ExecStart=/usr/bin/genieacs-nbi
|
||||||
KillMode=process
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
@@ -150,7 +154,7 @@ Configure systemd
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
sudo systemctl edit --force genieacs-fs
|
sudo systemctl edit --force --full genieacs-fs
|
||||||
|
|
||||||
Then paste the following in the editor and save:
|
Then paste the following in the editor and save:
|
||||||
|
|
||||||
@@ -163,8 +167,7 @@ Configure systemd
|
|||||||
[Service]
|
[Service]
|
||||||
User=genieacs
|
User=genieacs
|
||||||
EnvironmentFile=/opt/genieacs/genieacs.env
|
EnvironmentFile=/opt/genieacs/genieacs.env
|
||||||
ExecStart=genieacs-fs
|
ExecStart=/usr/bin/genieacs-fs
|
||||||
KillMode=process
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
@@ -173,7 +176,7 @@ Configure systemd
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
sudo systemctl edit --force genieacs-ui
|
sudo systemctl edit --force --full genieacs-ui
|
||||||
|
|
||||||
Then paste the following in the editor and save:
|
Then paste the following in the editor and save:
|
||||||
|
|
||||||
@@ -186,19 +189,18 @@ Configure systemd
|
|||||||
[Service]
|
[Service]
|
||||||
User=genieacs
|
User=genieacs
|
||||||
EnvironmentFile=/opt/genieacs/genieacs.env
|
EnvironmentFile=/opt/genieacs/genieacs.env
|
||||||
ExecStart=genieacs-ui
|
ExecStart=/usr/bin/genieacs-ui
|
||||||
KillMode=process
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
|
||||||
.. topic:: Configure log file rotation using logrotate
|
.. topic:: Configure log file rotation using logrotate
|
||||||
|
|
||||||
Save the following under :file:`/etc/logrotate.d/genieacs`
|
Save the following as :file:`/etc/logrotate.d/genieacs`
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
/var/log/genieacs/*.{log,yaml} {
|
/var/log/genieacs/*.log /var/log/genieacs/*.yaml {
|
||||||
daily
|
daily
|
||||||
rotate 30
|
rotate 30
|
||||||
compress
|
compress
|
||||||
|
|||||||
@@ -128,6 +128,14 @@ script filename while second argument is the function name within that script.
|
|||||||
Any remaining arguments will be passed to that function. See :ref:`extensions`
|
Any remaining arguments will be passed to that function. See :ref:`extensions`
|
||||||
for more details.
|
for more details.
|
||||||
|
|
||||||
|
``log(message)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Prints out a string in genieacs-cwmp's access log. It's meant to be used for
|
||||||
|
debugging. Note that you may see multiple log entries as the script can be
|
||||||
|
executed multiple times in a session. See :ref:`this FAQ
|
||||||
|
<administration-faq-duplicate-log-entries>`.
|
||||||
|
|
||||||
.. _path-format:
|
.. _path-format:
|
||||||
|
|
||||||
Path format
|
Path format
|
||||||
|
|||||||
+20
-5
@@ -88,7 +88,7 @@ export async function connectionRequest(deviceId): Promise<void> {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const UDP_CONNECTION_REQUEST_PORT = +getConfig(
|
const UDP_CONNECTION_REQUEST_PORT = +getConfig(
|
||||||
snapshot,
|
snapshot,
|
||||||
"cwmp.udpConnectionRequestAuth",
|
"cwmp.udpConnectionRequestPort",
|
||||||
context,
|
context,
|
||||||
now
|
now
|
||||||
);
|
);
|
||||||
@@ -226,11 +226,14 @@ function sanitizeTask(task): void {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "download":
|
case "download":
|
||||||
if (typeof task.fileType !== "string" || !task.fileType.length)
|
// genieacs-gui sends file ID instead of fileName and fileType
|
||||||
throw new Error("Missing 'fileType' property");
|
if (!task.file) {
|
||||||
|
if (typeof task.fileType !== "string" || !task.fileType.length)
|
||||||
|
throw new Error("Missing 'fileType' property");
|
||||||
|
|
||||||
if (typeof task.fileName !== "string" || !task.fileName.length)
|
if (typeof task.fileName !== "string" || !task.fileName.length)
|
||||||
throw new Error("Missing 'fileName' property");
|
throw new Error("Missing 'fileName' property");
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
task.targetFileName != null &&
|
task.targetFileName != null &&
|
||||||
@@ -238,6 +241,18 @@ function sanitizeTask(task): void {
|
|||||||
)
|
)
|
||||||
throw new Error("Invalid 'targetFileName' property");
|
throw new Error("Invalid 'targetFileName' property");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "provisions":
|
||||||
|
if (
|
||||||
|
!Array.isArray(task.provisions) ||
|
||||||
|
!task.provisions.every(arr =>
|
||||||
|
arr.every(
|
||||||
|
s => s == null || ["boolean", "number", "string"].includes(typeof s)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw new Error("Invalid 'provisions' property");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
|
|||||||
+5
-5
@@ -22,16 +22,16 @@ import * as config from "./config";
|
|||||||
|
|
||||||
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
|
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
|
||||||
|
|
||||||
let mongoClient: MongoClient;
|
let clientPromise: Promise<MongoClient>;
|
||||||
let mongoCollection: Collection;
|
let mongoCollection: Collection;
|
||||||
let mongoTimeOffset = 0;
|
let mongoTimeOffset = 0;
|
||||||
|
|
||||||
export async function connect(): Promise<void> {
|
export async function connect(): Promise<void> {
|
||||||
const MONGODB_CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
|
const MONGODB_CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
|
||||||
mongoClient = await MongoClient.connect(MONGODB_CONNECTION_URL, {
|
clientPromise = MongoClient.connect(MONGODB_CONNECTION_URL, {
|
||||||
useNewUrlParser: true
|
useNewUrlParser: true
|
||||||
});
|
});
|
||||||
const db = mongoClient.db();
|
const db = (await clientPromise).db();
|
||||||
mongoCollection = db.collection("cache");
|
mongoCollection = db.collection("cache");
|
||||||
await mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
|
await mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -40,7 +40,7 @@ export async function connect(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function disconnect(): Promise<void> {
|
export async function disconnect(): Promise<void> {
|
||||||
if (mongoClient) await mongoClient.close();
|
if (clientPromise) await (await clientPromise).close();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(key): Promise<any> {
|
export async function get(key): Promise<any> {
|
||||||
@@ -62,7 +62,7 @@ export async function get(key): Promise<any> {
|
|||||||
} else {
|
} else {
|
||||||
const res = await mongoCollection.findOne({ _id: { $in: [key] } });
|
const res = await mongoCollection.findOne({ _id: { $in: [key] } });
|
||||||
if (res && res["expire"] > expire) return res["value"];
|
if (res && res["expire"] > expire) return res["value"];
|
||||||
return res;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-10
@@ -22,7 +22,19 @@ import { cpus } from "os";
|
|||||||
import * as logger from "./logger";
|
import * as logger from "./logger";
|
||||||
|
|
||||||
let respawnTimestamp = 0;
|
let respawnTimestamp = 0;
|
||||||
let tooManyCrashesTimestamp = 0;
|
let crashes: number[] = [];
|
||||||
|
|
||||||
|
function fork(): cluster.Worker {
|
||||||
|
const w = cluster.fork();
|
||||||
|
w.on("error", (err: NodeJS.ErrnoException) => {
|
||||||
|
// Avoid exception when attempting to kill the process just as it's exiting
|
||||||
|
if (err.code !== "EPIPE") throw err;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!w.isDead()) throw err;
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
function restartWorker(worker, code, signal): void {
|
function restartWorker(worker, code, signal): void {
|
||||||
const msg = {
|
const msg = {
|
||||||
@@ -39,14 +51,21 @@ function restartWorker(worker, code, signal): void {
|
|||||||
logger.error(msg);
|
logger.error(msg);
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
respawnTimestamp = Math.max(now, respawnTimestamp + 2000);
|
crashes.push(now);
|
||||||
if (respawnTimestamp === now) {
|
|
||||||
tooManyCrashesTimestamp = now;
|
|
||||||
cluster.fork();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (now - tooManyCrashesTimestamp > 60000) {
|
let min1 = 0,
|
||||||
|
min2 = 0,
|
||||||
|
min3 = 0;
|
||||||
|
|
||||||
|
crashes = crashes.filter(n => {
|
||||||
|
if (n > now - 60000) ++min1;
|
||||||
|
else if (n > now - 120000) ++min2;
|
||||||
|
else if (n > now - 180000) ++min3;
|
||||||
|
else return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (min1 > 5 && min2 > 5 && min3 > 5) {
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
cluster.removeListener("exit", restartWorker);
|
cluster.removeListener("exit", restartWorker);
|
||||||
for (const pid in cluster.workers) cluster.workers[pid].kill();
|
for (const pid in cluster.workers) cluster.workers[pid].kill();
|
||||||
@@ -58,9 +77,15 @@ function restartWorker(worker, code, signal): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
respawnTimestamp = Math.max(now, respawnTimestamp + 2000);
|
||||||
|
if (respawnTimestamp === now) {
|
||||||
|
fork();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (process.exitCode) return;
|
if (process.exitCode) return;
|
||||||
cluster.fork();
|
fork();
|
||||||
}, respawnTimestamp - now);
|
}, respawnTimestamp - now);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +109,7 @@ export function start(workerCount, servicePort, serviceAddress): void {
|
|||||||
|
|
||||||
if (!workerCount) workerCount = Math.max(2, cpus().length);
|
if (!workerCount) workerCount = Math.max(2, cpus().length);
|
||||||
|
|
||||||
for (let i = 0; i < workerCount; ++i) cluster.fork();
|
for (let i = 0; i < workerCount; ++i) fork();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stop(): void {
|
export function stop(): void {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { PermissionSet, Expression } from "../types";
|
import { PermissionSet, Expression } from "../types";
|
||||||
import { evaluate } from "./expression";
|
import { evaluate, or } from "./expression";
|
||||||
|
|
||||||
export default class Authorizer {
|
export default class Authorizer {
|
||||||
private permissionSets: PermissionSet[];
|
private permissionSets: PermissionSet[];
|
||||||
@@ -27,14 +27,16 @@ export default class Authorizer {
|
|||||||
(mutationType, mutation, any) => boolean
|
(mutationType, mutation, any) => boolean
|
||||||
>;
|
>;
|
||||||
private hasAccessCache: Map<string, boolean>;
|
private hasAccessCache: Map<string, boolean>;
|
||||||
|
private getFilterCache: Map<string, Expression>;
|
||||||
|
|
||||||
public constructor(permissionSets) {
|
public constructor(permissionSets) {
|
||||||
this.permissionSets = permissionSets;
|
this.permissionSets = permissionSets;
|
||||||
this.validatorCache = new WeakMap();
|
this.validatorCache = new WeakMap();
|
||||||
this.hasAccessCache = new Map();
|
this.hasAccessCache = new Map();
|
||||||
|
this.getFilterCache = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasAccess(resourceType, access): boolean {
|
public hasAccess(resourceType: string, access: number): boolean {
|
||||||
const cacheKey = `${resourceType}-${access}`;
|
const cacheKey = `${resourceType}-${access}`;
|
||||||
if (this.hasAccessCache.has(cacheKey))
|
if (this.hasAccessCache.has(cacheKey))
|
||||||
return this.hasAccessCache.get(cacheKey);
|
return this.hasAccessCache.get(cacheKey);
|
||||||
@@ -55,10 +57,29 @@ export default class Authorizer {
|
|||||||
return has;
|
return has;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFilter(resourceType: string, access: number): Expression {
|
||||||
|
const cacheKey = `${resourceType}-${access}`;
|
||||||
|
if (this.getFilterCache.has(cacheKey))
|
||||||
|
return this.getFilterCache.get(cacheKey);
|
||||||
|
|
||||||
|
let filter: Expression = null;
|
||||||
|
for (const permissionSet of this.permissionSets) {
|
||||||
|
for (const perm of permissionSet) {
|
||||||
|
if (perm[resourceType]) {
|
||||||
|
if (perm[resourceType].access >= access)
|
||||||
|
filter = or(filter, perm[resourceType].filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getFilterCache.set(cacheKey, filter);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
public getValidator(
|
public getValidator(
|
||||||
resourceType,
|
resourceType,
|
||||||
resource
|
resource
|
||||||
): (mutationType: string, mutation: any, args: any) => boolean {
|
): (mutationType: string, mutation?: any, args?: any) => boolean {
|
||||||
if (this.validatorCache.has(resource))
|
if (this.validatorCache.has(resource))
|
||||||
return this.validatorCache.get(resource);
|
return this.validatorCache.get(resource);
|
||||||
|
|
||||||
@@ -120,4 +141,8 @@ export default class Authorizer {
|
|||||||
this.validatorCache.set(resource, validator);
|
this.validatorCache.set(resource, validator);
|
||||||
return validator;
|
return validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPermissionSets(): PermissionSet[] {
|
||||||
|
return this.permissionSets;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,7 @@ export async function evaluateAsync(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function and(exp1, exp2): Expression {
|
export function and(exp1: Expression, exp2: Expression): Expression {
|
||||||
if (!isArray(exp1)) return exp1 ? exp2 : exp1;
|
if (!isArray(exp1)) return exp1 ? exp2 : exp1;
|
||||||
if (!isArray(exp2)) return exp2 ? exp1 : exp2;
|
if (!isArray(exp2)) return exp2 ? exp1 : exp2;
|
||||||
|
|
||||||
@@ -292,7 +292,7 @@ export function and(exp1, exp2): Expression {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function or(exp1, exp2): Expression {
|
export function or(exp1: Expression, exp2: Expression): Expression {
|
||||||
if (!isArray(exp1)) return exp1 ? exp1 : exp2;
|
if (!isArray(exp1)) return exp1 ? exp1 : exp2;
|
||||||
if (!isArray(exp2)) return exp2 ? exp2 : exp1;
|
if (!isArray(exp2)) return exp2 ? exp2 : exp1;
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const options = {
|
|||||||
CWMP_SSL_KEY: { type: "string", default: "" },
|
CWMP_SSL_KEY: { type: "string", default: "" },
|
||||||
CWMP_LOG_FILE: { type: "path", default: "" },
|
CWMP_LOG_FILE: { type: "path", default: "" },
|
||||||
CWMP_ACCESS_LOG_FILE: { type: "path", default: "" },
|
CWMP_ACCESS_LOG_FILE: { type: "path", default: "" },
|
||||||
CWMP_KEEP_ALIVE_TIMEOUT: { type: "int", default: 0 },
|
|
||||||
|
|
||||||
NBI_WORKER_PROCESSES: { type: "int", default: 0 },
|
NBI_WORKER_PROCESSES: { type: "int", default: 0 },
|
||||||
NBI_PORT: { type: "int", default: 7557 },
|
NBI_PORT: { type: "int", default: 7557 },
|
||||||
|
|||||||
+64
-24
@@ -50,6 +50,7 @@ import { IncomingMessage, ServerResponse } from "http";
|
|||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { TLSSocket } from "tls";
|
import { TLSSocket } from "tls";
|
||||||
|
import { decode, encodingExists } from "iconv-lite";
|
||||||
import { dependencies as DEPENDENCIES } from "../package.json";
|
import { dependencies as DEPENDENCIES } from "../package.json";
|
||||||
import { parseXmlDeclaration } from "./xml-parser";
|
import { parseXmlDeclaration } from "./xml-parser";
|
||||||
import * as debug from "./debug";
|
import * as debug from "./debug";
|
||||||
@@ -189,14 +190,15 @@ async function writeResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const httpResponse = sessionContext.httpResponse;
|
const httpResponse = sessionContext.httpResponse;
|
||||||
|
const connection = httpResponse.connection;
|
||||||
|
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(data));
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(data));
|
||||||
httpResponse.writeHead(res.code, res.headers);
|
httpResponse.writeHead(res.code, res.headers);
|
||||||
httpResponse.end(data);
|
|
||||||
|
|
||||||
if (sessionContext.debug)
|
if (sessionContext.debug)
|
||||||
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, res.data);
|
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, res.data);
|
||||||
|
httpResponse.end(data);
|
||||||
|
|
||||||
if (sessionContext.httpRequest.connection.destroyed) {
|
if (connection.destroyed) {
|
||||||
logger.accessError({
|
logger.accessError({
|
||||||
sessionContext: sessionContext,
|
sessionContext: sessionContext,
|
||||||
message: "Connection dropped"
|
message: "Connection dropped"
|
||||||
@@ -211,7 +213,7 @@ async function writeResponse(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sessionContext.lastActivity = Date.now();
|
sessionContext.lastActivity = Date.now();
|
||||||
currentSessions.set(sessionContext.httpRequest.connection, sessionContext);
|
currentSessions.set(connection, sessionContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,7 +670,7 @@ async function nextRpc(sessionContext: SessionContext): Promise<void> {
|
|||||||
break;
|
break;
|
||||||
case "download":
|
case "download":
|
||||||
session.addProvisions(sessionContext, `task_${task._id}`, [
|
session.addProvisions(sessionContext, `task_${task._id}`, [
|
||||||
["download", task.fileType, task.fileName, task.targetFileName]
|
["download", task.fileType, task.fileName, task.targetFileName || ""]
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case "addObject":
|
case "addObject":
|
||||||
@@ -684,6 +686,13 @@ async function nextRpc(sessionContext: SessionContext): Promise<void> {
|
|||||||
["instances", task.objectName, 0]
|
["instances", task.objectName, 0]
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
|
case "provisions":
|
||||||
|
session.addProvisions(
|
||||||
|
sessionContext,
|
||||||
|
`task_${task._id}`,
|
||||||
|
task.provisions
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Task name not recognized");
|
throw new Error("Task name not recognized");
|
||||||
}
|
}
|
||||||
@@ -772,7 +781,7 @@ async function sendAcsRequest(
|
|||||||
downloadRequest.fileSize = 0;
|
downloadRequest.fileSize = 0;
|
||||||
if (!downloadRequest.url) {
|
if (!downloadRequest.url) {
|
||||||
const FS_PORT = config.get("FS_PORT");
|
const FS_PORT = config.get("FS_PORT");
|
||||||
const FS_SSL = config.get("FS_SSL");
|
const FS_SSL = config.get("FS_SSL_CERT");
|
||||||
let hostname = config.get("FS_HOSTNAME");
|
let hostname = config.get("FS_HOSTNAME");
|
||||||
if (!hostname) {
|
if (!hostname) {
|
||||||
if (sessionContext.httpRequest.headers["host"])
|
if (sessionContext.httpRequest.headers["host"])
|
||||||
@@ -822,11 +831,15 @@ async function getSession(connection, sessionId): Promise<SessionContext> {
|
|||||||
return session.deserialize(sessionContextString);
|
return session.deserialize(sessionContextString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only needed to prevent tree shaking from removing the remoteAddress
|
||||||
|
// workaround in onConnection function.
|
||||||
|
const remoteAddressWorkaround = new WeakMap<Socket, string>();
|
||||||
|
|
||||||
// When socket closes, store active sessions in cache
|
// When socket closes, store active sessions in cache
|
||||||
export function onConnection(socket): void {
|
export function onConnection(socket: Socket): void {
|
||||||
// The property remoteAddress may be undefined after the connection is
|
// The property remoteAddress may be undefined after the connection is
|
||||||
// closed, unless we read it at least once (caching?)
|
// closed, unless we read it at least once (caching?)
|
||||||
socket.remoteAddress;
|
remoteAddressWorkaround.set(socket, socket.remoteAddress);
|
||||||
|
|
||||||
socket.on("close", async () => {
|
socket.on("close", async () => {
|
||||||
const sessionContext = currentSessions.get(socket);
|
const sessionContext = currentSessions.get(socket);
|
||||||
@@ -952,9 +965,9 @@ async function reportBadState(sessionContext: SessionContext): Promise<void> {
|
|||||||
const body = "Bad session state";
|
const body = "Bad session state";
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(body));
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(body));
|
||||||
httpResponse.writeHead(400, { Connection: "close" });
|
httpResponse.writeHead(400, { Connection: "close" });
|
||||||
httpResponse.end(body);
|
|
||||||
if (sessionContext.debug)
|
if (sessionContext.debug)
|
||||||
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body);
|
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body);
|
||||||
|
httpResponse.end(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function responseUnauthorized(
|
async function responseUnauthorized(
|
||||||
@@ -989,9 +1002,9 @@ async function responseUnauthorized(
|
|||||||
const body = "Unauthorized";
|
const body = "Unauthorized";
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(body));
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(body));
|
||||||
httpResponse.writeHead(401, resHeaders);
|
httpResponse.writeHead(401, resHeaders);
|
||||||
httpResponse.end(body);
|
|
||||||
if (sessionContext.debug)
|
if (sessionContext.debug)
|
||||||
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body);
|
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body);
|
||||||
|
httpResponse.end(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processRequest(
|
async function processRequest(
|
||||||
@@ -1105,6 +1118,15 @@ export function listener(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decodeString(buffer: Buffer, charset: string): string {
|
||||||
|
try {
|
||||||
|
return buffer.toString(charset);
|
||||||
|
} catch (err) {
|
||||||
|
if (encodingExists(charset)) return decode(buffer, charset);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async function listenerAsync(
|
async function listenerAsync(
|
||||||
httpRequest: IncomingMessage,
|
httpRequest: IncomingMessage,
|
||||||
httpResponse: ServerResponse
|
httpResponse: ServerResponse
|
||||||
@@ -1202,7 +1224,6 @@ async function listenerAsync(
|
|||||||
const _body = "Invalid session";
|
const _body = "Invalid session";
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(_body));
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(_body));
|
||||||
httpResponse.writeHead(400, { Connection: "close" });
|
httpResponse.writeHead(400, { Connection: "close" });
|
||||||
httpResponse.end(_body);
|
|
||||||
if (sessionContext.debug) {
|
if (sessionContext.debug) {
|
||||||
debug.outgoingHttpResponse(
|
debug.outgoingHttpResponse(
|
||||||
httpResponse,
|
httpResponse,
|
||||||
@@ -1210,7 +1231,7 @@ async function listenerAsync(
|
|||||||
_body
|
_body
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
httpResponse.end(_body);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (stats.concurrentRequests > MAX_CONCURRENT_REQUESTS) {
|
} else if (stats.concurrentRequests > MAX_CONCURRENT_REQUESTS) {
|
||||||
@@ -1233,10 +1254,36 @@ async function listenerAsync(
|
|||||||
if (!charset) {
|
if (!charset) {
|
||||||
const parse = parseXmlDeclaration(body);
|
const parse = parseXmlDeclaration(body);
|
||||||
const e = parse ? parse.find(s => s.localName === "encoding") : null;
|
const e = parse ? parse.find(s => s.localName === "encoding") : null;
|
||||||
charset = e ? e.value : "utf8";
|
charset = e ? e.value.toLowerCase() : "utf8";
|
||||||
}
|
}
|
||||||
|
|
||||||
const bodyStr = body.toString(charset);
|
const bodyStr = decodeString(body, charset);
|
||||||
|
|
||||||
|
if (bodyStr == null) {
|
||||||
|
const msg = `Unknown encoding '${charset}'`;
|
||||||
|
logger.accessError({
|
||||||
|
message: "XML parse error",
|
||||||
|
parseError: msg,
|
||||||
|
sessionContext: sessionContext || {
|
||||||
|
httpRequest: httpRequest,
|
||||||
|
httpResponse: httpResponse
|
||||||
|
}
|
||||||
|
});
|
||||||
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(msg));
|
||||||
|
httpResponse.writeHead(400, { Connection: "close" });
|
||||||
|
if (sessionContext) {
|
||||||
|
if (sessionContext.debug)
|
||||||
|
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, msg);
|
||||||
|
} else {
|
||||||
|
const cacheSnapshot = await localCache.getCurrentSnapshot();
|
||||||
|
const d = !!localCache.getConfig(cacheSnapshot, "cwmp.debug", {
|
||||||
|
remoteAddress: httpResponse.connection.remoteAddress
|
||||||
|
});
|
||||||
|
if (d) debug.outgoingHttpResponse(httpResponse, null, msg);
|
||||||
|
}
|
||||||
|
httpResponse.end(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
async function parsedRpc(_sessionContext, rpc, parseWarnings): Promise<void> {
|
async function parsedRpc(_sessionContext, rpc, parseWarnings): Promise<void> {
|
||||||
for (const w of parseWarnings) {
|
for (const w of parseWarnings) {
|
||||||
@@ -1270,7 +1317,6 @@ async function listenerAsync(
|
|||||||
});
|
});
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(error.message));
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(error.message));
|
||||||
httpResponse.writeHead(400, { Connection: "close" });
|
httpResponse.writeHead(400, { Connection: "close" });
|
||||||
httpResponse.end(error.message);
|
|
||||||
if (sessionContext) {
|
if (sessionContext) {
|
||||||
if (sessionContext.debug) {
|
if (sessionContext.debug) {
|
||||||
debug.outgoingHttpResponse(
|
debug.outgoingHttpResponse(
|
||||||
@@ -1284,14 +1330,9 @@ async function listenerAsync(
|
|||||||
const d = !!localCache.getConfig(cacheSnapshot, "cwmp.debug", {
|
const d = !!localCache.getConfig(cacheSnapshot, "cwmp.debug", {
|
||||||
remoteAddress: httpResponse.connection.remoteAddress
|
remoteAddress: httpResponse.connection.remoteAddress
|
||||||
});
|
});
|
||||||
if (d) {
|
if (d) debug.outgoingHttpResponse(httpResponse, null, error.message);
|
||||||
debug.outgoingHttpResponse(
|
|
||||||
httpResponse,
|
|
||||||
sessionContext.deviceId,
|
|
||||||
error.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
httpResponse.end(error.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1318,14 +1359,13 @@ async function listenerAsync(
|
|||||||
const _body = "Invalid session";
|
const _body = "Invalid session";
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(_body));
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(_body));
|
||||||
httpResponse.writeHead(400, { Connection: "close" });
|
httpResponse.writeHead(400, { Connection: "close" });
|
||||||
httpResponse.end(_body);
|
|
||||||
const cacheSnapshot = await localCache.getCurrentSnapshot();
|
const cacheSnapshot = await localCache.getCurrentSnapshot();
|
||||||
const d = !!localCache.getConfig(cacheSnapshot, "cwmp.debug", {
|
const d = !!localCache.getConfig(cacheSnapshot, "cwmp.debug", {
|
||||||
remoteAddress: httpResponse.connection.remoteAddress
|
remoteAddress: httpResponse.connection.remoteAddress
|
||||||
});
|
});
|
||||||
if (d)
|
if (d)
|
||||||
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, _body);
|
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, _body);
|
||||||
|
httpResponse.end(_body);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,13 +44,21 @@ export let tasksCollection: Collection,
|
|||||||
usersCollection: Collection,
|
usersCollection: Collection,
|
||||||
configCollection: Collection;
|
configCollection: Collection;
|
||||||
|
|
||||||
|
let clientPromise: Promise<MongoClient>;
|
||||||
export let client: MongoClient;
|
export let client: MongoClient;
|
||||||
|
|
||||||
|
function compareAccessLists(list1: string[], list2: string[]): boolean {
|
||||||
|
if (list1.length !== list2.length) return false;
|
||||||
|
for (const [i, v] of list1.entries()) if (v !== list2[i]) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export async function connect(): Promise<void> {
|
export async function connect(): Promise<void> {
|
||||||
client = await MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), {
|
clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), {
|
||||||
useNewUrlParser: true
|
useNewUrlParser: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client = await clientPromise;
|
||||||
const db = client.db();
|
const db = client.db();
|
||||||
|
|
||||||
tasksCollection = db.collection("tasks");
|
tasksCollection = db.collection("tasks");
|
||||||
@@ -70,7 +78,7 @@ export async function connect(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function disconnect(): Promise<void> {
|
export async function disconnect(): Promise<void> {
|
||||||
if (client) await client.close();
|
if (clientPromise) await (await clientPromise).close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimize projection by removing overlaps
|
// Optimize projection by removing overlaps
|
||||||
@@ -116,6 +124,8 @@ export async function fetchDevice(
|
|||||||
|
|
||||||
function storeParams(obj, path: string, pathLength: number, ts): void {
|
function storeParams(obj, path: string, pathLength: number, ts): void {
|
||||||
if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"];
|
if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"];
|
||||||
|
if (obj["_attributesTimestamp"])
|
||||||
|
obj["_attributesTimestamp"] = +obj["_attributesTimestamp"];
|
||||||
|
|
||||||
const attrs: Attributes = {};
|
const attrs: Attributes = {};
|
||||||
let t = obj["_timestamp"] || 1;
|
let t = obj["_timestamp"] || 1;
|
||||||
@@ -133,6 +143,16 @@ export async function fetchDevice(
|
|||||||
|
|
||||||
if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0];
|
if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0];
|
||||||
|
|
||||||
|
if (obj["_notification"] != null) {
|
||||||
|
attrs.notification = [
|
||||||
|
obj["_attributesTimestamp"] || 1,
|
||||||
|
obj["_notification"]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj["_accessList"] != null)
|
||||||
|
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
||||||
|
|
||||||
res.push([Path.parse(path.slice(0, -1)), t, attrs]);
|
res.push([Path.parse(path.slice(0, -1)), t, attrs]);
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(obj)) {
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
@@ -329,6 +349,12 @@ export async function saveDevice(
|
|||||||
const object2 = ((diff[2] || {}).object || [])[1];
|
const object2 = ((diff[2] || {}).object || [])[1];
|
||||||
const writable2 = ((diff[2] || {}).writable || [])[1];
|
const writable2 = ((diff[2] || {}).writable || [])[1];
|
||||||
const writable1 = ((diff[1] || {}).writable || [])[1];
|
const writable1 = ((diff[1] || {}).writable || [])[1];
|
||||||
|
const attributesTimestamp1 = ((diff[1] || {}).notification || [])[0];
|
||||||
|
const attributesTimestamp2 = ((diff[2] || {}).notification || [])[0];
|
||||||
|
const notification1 = ((diff[1] || {}).notification || [])[1];
|
||||||
|
const notification2 = ((diff[2] || {}).notification || [])[1];
|
||||||
|
const accessList1 = ((diff[1] || {}).accessList || [])[1];
|
||||||
|
const accessList2 = ((diff[2] || {}).accessList || [])[1];
|
||||||
|
|
||||||
switch (path.segments[0]) {
|
switch (path.segments[0]) {
|
||||||
case "Events":
|
case "Events":
|
||||||
@@ -455,6 +481,46 @@ export async function saveDevice(
|
|||||||
path.length ? path.toString() + "._writable" : "_writable"
|
path.length ? path.toString() + "._writable" : "_writable"
|
||||||
] = !!writable2;
|
] = !!writable2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "notification":
|
||||||
|
if (
|
||||||
|
!diff[1] ||
|
||||||
|
!diff[1].notification ||
|
||||||
|
notification2 !== notification1
|
||||||
|
) {
|
||||||
|
update["$set"][
|
||||||
|
path.length
|
||||||
|
? path.toString() + "._notification"
|
||||||
|
: "_notification"
|
||||||
|
] = notification2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributesTimestamp2 !== attributesTimestamp1) {
|
||||||
|
update["$set"][
|
||||||
|
path.toString() + "._attributesTimestamp"
|
||||||
|
] = new Date(attributesTimestamp2);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "accessList":
|
||||||
|
if (
|
||||||
|
!diff[1] ||
|
||||||
|
!diff[1].accessList ||
|
||||||
|
!compareAccessLists(accessList2, accessList1)
|
||||||
|
) {
|
||||||
|
update["$set"][
|
||||||
|
path.length
|
||||||
|
? path.toString() + "._accessList"
|
||||||
|
: "_accessList"
|
||||||
|
] = accessList2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributesTimestamp2 !== attributesTimestamp1) {
|
||||||
|
update["$set"][
|
||||||
|
path.toString() + "._attributesTimestamp"
|
||||||
|
] = new Date(attributesTimestamp2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,6 +536,12 @@ export async function saveDevice(
|
|||||||
if (attrName === "value") {
|
if (attrName === "value") {
|
||||||
update["$unset"][p + "_type"] = 1;
|
update["$unset"][p + "_type"] = 1;
|
||||||
update["$unset"][p + "_timestamp"] = 1;
|
update["$unset"][p + "_timestamp"] = 1;
|
||||||
|
} else if (attrName === "notification") {
|
||||||
|
if (accessList2 == null)
|
||||||
|
update["$unset"][`${p}_attributesTimestamp`] = 1;
|
||||||
|
} else if (attrName === "accessList") {
|
||||||
|
if (notification2 == null)
|
||||||
|
update["$unset"][`${p}_attributesTimestamp`] = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-10
@@ -25,21 +25,49 @@ import * as scheduling from "./scheduling";
|
|||||||
const MAX_DEPTH = +config.get("MAX_DEPTH");
|
const MAX_DEPTH = +config.get("MAX_DEPTH");
|
||||||
|
|
||||||
export function refresh(sessionContext, provision, declarations): boolean {
|
export function refresh(sessionContext, provision, declarations): boolean {
|
||||||
const segments = Path.parse(provision[1]).segments.slice();
|
if (
|
||||||
const l = segments.length;
|
(provision.length !== 2 || typeof provision[1] !== "string") &&
|
||||||
segments.length = MAX_DEPTH;
|
(provision.length !== 3 ||
|
||||||
segments.fill("*", l);
|
typeof provision[1] !== "string" ||
|
||||||
const path = Path.parse(segments.join("."));
|
typeof provision[2] !== "number") &&
|
||||||
|
(provision.length < 4 ||
|
||||||
|
typeof provision[1] !== "string" ||
|
||||||
|
typeof provision[2] !== "number" ||
|
||||||
|
typeof provision[3] !== "boolean")
|
||||||
|
)
|
||||||
|
throw new Error("Invalid arguments");
|
||||||
|
|
||||||
const every = 1000 * (provision[2] || 1);
|
const every = 1000 * (provision[2] || 1);
|
||||||
const offset = scheduling.variance(sessionContext.deviceId, every);
|
const offset = scheduling.variance(sessionContext.deviceId, every);
|
||||||
const t = scheduling.interval(sessionContext.timestamp, every, offset);
|
const t = scheduling.interval(sessionContext.timestamp, every, offset);
|
||||||
|
|
||||||
for (let i = l; i < path.length; ++i) {
|
let attrGet;
|
||||||
|
let refreshChildren;
|
||||||
|
if (provision[3] == null) {
|
||||||
|
refreshChildren = true;
|
||||||
|
attrGet = { object: 1, writable: 1, value: t };
|
||||||
|
} else {
|
||||||
|
attrGet = {};
|
||||||
|
refreshChildren = !!provision[3];
|
||||||
|
for (const a of provision.slice(4)) attrGet[a] = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = Path.parse(provision[1]);
|
||||||
|
let l = path.length;
|
||||||
|
if (refreshChildren) {
|
||||||
|
const segments = path.segments.slice();
|
||||||
|
l = segments.length;
|
||||||
|
segments.length = MAX_DEPTH;
|
||||||
|
segments.fill("*", l);
|
||||||
|
path = Path.parse(segments.join("."));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = l; i <= path.length; ++i) {
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: path.slice(0, i),
|
path: path.slice(0, i),
|
||||||
pathGet: t,
|
pathGet: t,
|
||||||
pathSet: null,
|
pathSet: null,
|
||||||
attrGet: { object: 1, writable: 1, value: t },
|
attrGet: attrGet,
|
||||||
attrSet: null,
|
attrSet: null,
|
||||||
defer: true
|
defer: true
|
||||||
});
|
});
|
||||||
@@ -49,12 +77,38 @@ export function refresh(sessionContext, provision, declarations): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function value(sessionContext, provision, declarations): boolean {
|
export function value(sessionContext, provision, declarations): boolean {
|
||||||
|
if (
|
||||||
|
provision.length < 3 ||
|
||||||
|
provision.length > 4 ||
|
||||||
|
typeof provision[1] !== "string"
|
||||||
|
)
|
||||||
|
throw new Error("Invalid arguments");
|
||||||
|
|
||||||
|
let attr: string, val: any;
|
||||||
|
|
||||||
|
if (provision.length === 3) {
|
||||||
|
attr = "value";
|
||||||
|
val = provision[2];
|
||||||
|
} else {
|
||||||
|
attr = provision[2] || "";
|
||||||
|
val = provision[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attr === "accessList") {
|
||||||
|
val = (val || "")
|
||||||
|
.split(",")
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => !!s);
|
||||||
|
} else if (attr === "value") {
|
||||||
|
val = [val];
|
||||||
|
}
|
||||||
|
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: Path.parse(provision[1]),
|
path: Path.parse(provision[1]),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
pathSet: null,
|
pathSet: null,
|
||||||
attrGet: { value: 1 },
|
attrGet: { [attr]: 1 },
|
||||||
attrSet: { value: [provision[2]] },
|
attrSet: { [attr]: val },
|
||||||
defer: true
|
defer: true
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,6 +116,13 @@ export function value(sessionContext, provision, declarations): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function tag(sessionContext, provision, declarations): boolean {
|
export function tag(sessionContext, provision, declarations): boolean {
|
||||||
|
if (
|
||||||
|
provision.length !== 3 ||
|
||||||
|
typeof provision[1] !== "string" ||
|
||||||
|
typeof provision[2] !== "boolean"
|
||||||
|
)
|
||||||
|
throw new Error("Invalid arguments");
|
||||||
|
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: Path.parse(`Tags.${provision[1]}`),
|
path: Path.parse(`Tags.${provision[1]}`),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
@@ -75,6 +136,8 @@ export function tag(sessionContext, provision, declarations): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function reboot(sessionContext, provision, declarations): boolean {
|
export function reboot(sessionContext, provision, declarations): boolean {
|
||||||
|
if (provision.length !== 1) throw new Error("Invalid arguments");
|
||||||
|
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: Path.parse("Reboot"),
|
path: Path.parse("Reboot"),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
@@ -88,6 +151,8 @@ export function reboot(sessionContext, provision, declarations): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function reset(sessionContext, provision, declarations): boolean {
|
export function reset(sessionContext, provision, declarations): boolean {
|
||||||
|
if (provision.length !== 1) throw new Error("Invalid arguments");
|
||||||
|
|
||||||
declarations.push({
|
declarations.push({
|
||||||
path: Path.parse("FactoryReset"),
|
path: Path.parse("FactoryReset"),
|
||||||
pathGet: 1,
|
pathGet: 1,
|
||||||
@@ -101,6 +166,17 @@ export function reset(sessionContext, provision, declarations): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function download(sessionContext, provision, declarations): boolean {
|
export function download(sessionContext, provision, declarations): boolean {
|
||||||
|
if (
|
||||||
|
(provision.length !== 3 ||
|
||||||
|
typeof provision[1] !== "string" ||
|
||||||
|
typeof provision[2] !== "string") &&
|
||||||
|
(provision.length !== 4 ||
|
||||||
|
typeof provision[1] !== "string" ||
|
||||||
|
typeof provision[2] !== "string" ||
|
||||||
|
typeof provision[3] !== "string")
|
||||||
|
)
|
||||||
|
throw new Error("Invalid arguments");
|
||||||
|
|
||||||
const alias = [
|
const alias = [
|
||||||
`FileType:${JSON.stringify(provision[1] || "")}`,
|
`FileType:${JSON.stringify(provision[1] || "")}`,
|
||||||
`FileName:${JSON.stringify(provision[2] || "")}`,
|
`FileName:${JSON.stringify(provision[2] || "")}`,
|
||||||
@@ -135,9 +211,12 @@ export function instances(
|
|||||||
startRevision,
|
startRevision,
|
||||||
endRevision
|
endRevision
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (provision.length !== 3 || typeof provision[1] !== "string")
|
||||||
|
throw new Error("Invalid arguments");
|
||||||
|
|
||||||
let count = Number(provision[2]);
|
let count = Number(provision[2]);
|
||||||
|
|
||||||
if (Number.isNaN(count)) return true;
|
if (Number.isNaN(count)) throw new Error("Invalid arguments");
|
||||||
|
|
||||||
const path = Path.parse(provision[1]);
|
const path = Path.parse(provision[1]);
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -23,7 +23,9 @@ import { DeviceData, Declaration, Clear } from "./types";
|
|||||||
const CHANGE_FLAGS = {
|
const CHANGE_FLAGS = {
|
||||||
object: 2,
|
object: 2,
|
||||||
writable: 4,
|
writable: 4,
|
||||||
value: 8
|
value: 8,
|
||||||
|
notification: 16,
|
||||||
|
accessList: 32
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseBool(v): boolean {
|
function parseBool(v): boolean {
|
||||||
|
|||||||
+2
-1
@@ -21,6 +21,7 @@ import { spawn, ChildProcess } from "child_process";
|
|||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import * as config from "./config";
|
import * as config from "./config";
|
||||||
import { Fault } from "./types";
|
import { Fault } from "./types";
|
||||||
|
import { ROOT_DIR } from "./config";
|
||||||
|
|
||||||
const TIMEOUT = +config.get("EXT_TIMEOUT");
|
const TIMEOUT = +config.get("EXT_TIMEOUT");
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export function run(args): Promise<{ fault: Fault; value: any }> {
|
|||||||
jobs.set(id, resolve);
|
jobs.set(id, resolve);
|
||||||
|
|
||||||
if (!processes[scriptName]) {
|
if (!processes[scriptName]) {
|
||||||
const p = spawn("bin/genieacs-ext", [scriptName], {
|
const p = spawn(ROOT_DIR + "/bin/genieacs-ext", [scriptName], {
|
||||||
stdio: ["inherit", "inherit", "inherit", "ipc"]
|
stdio: ["inherit", "inherit", "inherit", "ipc"]
|
||||||
});
|
});
|
||||||
processes[scriptName] = p;
|
processes[scriptName] = p;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import Path from "./common/path";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright 2013-2019 GenieACS Inc.
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
*
|
*
|
||||||
@@ -27,9 +29,12 @@ const UNDISCOVERED_DEPTH = 7;
|
|||||||
// flags is an int where its bits mark the segments in the pattern that
|
// flags is an int where its bits mark the segments in the pattern that
|
||||||
// need refreshing. Leading 0s indicate that the pattern up to that
|
// need refreshing. Leading 0s indicate that the pattern up to that
|
||||||
// point has been discovered.
|
// point has been discovered.
|
||||||
export function estimateGpnCount(gpnPatterns, depth = 0): number {
|
export function estimateGpnCount(
|
||||||
const children = {};
|
gpnPatterns: [Path, number][],
|
||||||
const wildcardChildren = [];
|
depth: number = 0
|
||||||
|
): number {
|
||||||
|
const children: { [segment: string]: [Path, number][] } = {};
|
||||||
|
const wildcardChildren: [Path, number][] = [];
|
||||||
let wildcardDiscovered = false;
|
let wildcardDiscovered = false;
|
||||||
let gpnCount = 0;
|
let gpnCount = 0;
|
||||||
|
|
||||||
@@ -37,7 +42,7 @@ export function estimateGpnCount(gpnPatterns, depth = 0): number {
|
|||||||
const path = pattern[0];
|
const path = pattern[0];
|
||||||
const flags = pattern[1] >> depth;
|
const flags = pattern[1] >> depth;
|
||||||
|
|
||||||
const k = path[depth];
|
const k = path.segments[depth] as string;
|
||||||
|
|
||||||
if (!k) {
|
if (!k) {
|
||||||
if (flags & 1) gpnCount = 1;
|
if (flags & 1) gpnCount = 1;
|
||||||
|
|||||||
+3
-3
@@ -60,7 +60,7 @@ const INFORM_SCRIPT = `
|
|||||||
const username = declare("DeviceID.ID", {value: 1}).value[0]
|
const username = declare("DeviceID.ID", {value: 1}).value[0]
|
||||||
|
|
||||||
// Password will be fixed a given device because Math.random() is seeded with devcie ID by default.
|
// Password will be fixed a given device because Math.random() is seeded with devcie ID by default.
|
||||||
const password = "" + Math.random();
|
const password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
|
||||||
|
|
||||||
const informInterval = 300;
|
const informInterval = 300;
|
||||||
|
|
||||||
@@ -135,8 +135,7 @@ export async function seed(options): Promise<void> {
|
|||||||
{ _id: "ui.filters.1.parameter", value: "DeviceID.ProductClass" },
|
{ _id: "ui.filters.1.parameter", value: "DeviceID.ProductClass" },
|
||||||
{ _id: "ui.filters.1.type", value: "'string'" },
|
{ _id: "ui.filters.1.type", value: "'string'" },
|
||||||
{ _id: "ui.filters.2.label", value: "'Tag'" },
|
{ _id: "ui.filters.2.label", value: "'Tag'" },
|
||||||
{ _id: "ui.filters.2.parameter", value: "tag" },
|
{ _id: "ui.filters.2.type", value: "'tag'" }
|
||||||
{ _id: "ui.filters.2.type", value: "'string'" }
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +377,7 @@ export async function seed(options): Promise<void> {
|
|||||||
_id: "bootstrap",
|
_id: "bootstrap",
|
||||||
weight: 0,
|
weight: 0,
|
||||||
channel: "bootstrap",
|
channel: "bootstrap",
|
||||||
|
events: "0 BOOTSTRAP",
|
||||||
provision: "bootstrap"
|
provision: "bootstrap"
|
||||||
},
|
},
|
||||||
{ _id: "default", weight: 0, channel: "default", provision: "default" },
|
{ _id: "default", weight: 0, channel: "default", provision: "default" },
|
||||||
|
|||||||
+52
-28
@@ -34,8 +34,6 @@ export function processDeviceFilter(filter): Expression {
|
|||||||
else if (p === "DeviceID") return ["PARAM", "_deviceId"];
|
else if (p === "DeviceID") return ["PARAM", "_deviceId"];
|
||||||
else if (p.startsWith("DeviceID."))
|
else if (p.startsWith("DeviceID."))
|
||||||
return ["PARAM", "_deviceId._" + p.slice(9)];
|
return ["PARAM", "_deviceId._" + p.slice(9)];
|
||||||
else if (p === "Tags") return ["PARAM", "_tags"];
|
|
||||||
else if (p === "tag") return ["PARAM", "_tags"];
|
|
||||||
else if (p === "Events.Inform") return ["PARAM", "_lastInform"];
|
else if (p === "Events.Inform") return ["PARAM", "_lastInform"];
|
||||||
else if (p === "Events.Registered") return ["PARAM", "_registered"];
|
else if (p === "Events.Registered") return ["PARAM", "_registered"];
|
||||||
else if (p === "Events.0_BOOTSTRAP") return ["PARAM", "_lastBootstrap"];
|
else if (p === "Events.0_BOOTSTRAP") return ["PARAM", "_lastBootstrap"];
|
||||||
@@ -50,6 +48,8 @@ export function processDeviceFilter(filter): Expression {
|
|||||||
else if (exp[0] === "IS NOT NULL") return ["=", ["PARAM", "_tags"], t];
|
else if (exp[0] === "IS NOT NULL") return ["=", ["PARAM", "_tags"], t];
|
||||||
else if (exp[0] === "=" && exp[2] === true)
|
else if (exp[0] === "=" && exp[2] === true)
|
||||||
return ["=", ["PARAM", "_tags"], t];
|
return ["=", ["PARAM", "_tags"], t];
|
||||||
|
else if (exp[0] === "<>" && exp[2] !== true)
|
||||||
|
return ["=", ["PARAM", "_tags"], t];
|
||||||
} else if (
|
} else if (
|
||||||
["=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"].includes(exp[0])
|
["=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"].includes(exp[0])
|
||||||
) {
|
) {
|
||||||
@@ -285,19 +285,30 @@ function parseDate(d): number | string {
|
|||||||
return isNaN(n) ? "" + d : n;
|
return isNaN(n) ? "" + d : n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FlatAttributes {
|
||||||
|
object?: boolean;
|
||||||
|
objectTimestamp?: number;
|
||||||
|
writable?: boolean;
|
||||||
|
writableTimestamp?: number;
|
||||||
|
value?: [string | number | boolean, string];
|
||||||
|
valueTimestamp?: number;
|
||||||
|
notification?: number;
|
||||||
|
notificationTimestamp?: number;
|
||||||
|
accessList?: string[];
|
||||||
|
accessListTimestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface FlatDevice {
|
interface FlatDevice {
|
||||||
[param: string]: {
|
[param: string]: FlatAttributes;
|
||||||
object: boolean;
|
|
||||||
objectTimestamp: number;
|
|
||||||
writable: boolean;
|
|
||||||
writableTimestamp: number;
|
|
||||||
value: [string | number | boolean, string];
|
|
||||||
valueTimestamp: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flattenDevice(device): FlatDevice {
|
export function flattenDevice(device): FlatDevice {
|
||||||
function recursive(input, root, output, timestamp): void {
|
function recursive(
|
||||||
|
input,
|
||||||
|
root: string,
|
||||||
|
output: FlatDevice,
|
||||||
|
timestamp: number
|
||||||
|
): void {
|
||||||
for (const [name, tree] of Object.entries(input)) {
|
for (const [name, tree] of Object.entries(input)) {
|
||||||
if (!root) {
|
if (!root) {
|
||||||
if (name === "_lastInform") {
|
if (name === "_lastInform") {
|
||||||
@@ -338,7 +349,7 @@ export function flattenDevice(device): FlatDevice {
|
|||||||
};
|
};
|
||||||
} else if (name === "_id") {
|
} else if (name === "_id") {
|
||||||
output["DeviceID.ID"] = {
|
output["DeviceID.ID"] = {
|
||||||
value: [tree, "xsd:string"],
|
value: [tree as string, "xsd:string"],
|
||||||
valueTimestamp: timestamp,
|
valueTimestamp: timestamp,
|
||||||
writable: false,
|
writable: false,
|
||||||
writableTimestamp: timestamp,
|
writableTimestamp: timestamp,
|
||||||
@@ -380,7 +391,7 @@ export function flattenDevice(device): FlatDevice {
|
|||||||
};
|
};
|
||||||
} else if (name === "_tags") {
|
} else if (name === "_tags") {
|
||||||
output["Tags"] = {
|
output["Tags"] = {
|
||||||
writable: true,
|
writable: false,
|
||||||
writableTimestamp: timestamp,
|
writableTimestamp: timestamp,
|
||||||
object: true,
|
object: true,
|
||||||
objectTimestamp: timestamp
|
objectTimestamp: timestamp
|
||||||
@@ -390,7 +401,7 @@ export function flattenDevice(device): FlatDevice {
|
|||||||
output[`Tags.${t}`] = {
|
output[`Tags.${t}`] = {
|
||||||
value: [true, "xsd:boolean"],
|
value: [true, "xsd:boolean"],
|
||||||
valueTimestamp: timestamp,
|
valueTimestamp: timestamp,
|
||||||
writable: false,
|
writable: true,
|
||||||
writableTimestamp: timestamp,
|
writableTimestamp: timestamp,
|
||||||
object: false,
|
object: false,
|
||||||
objectTimestamp: timestamp
|
objectTimestamp: timestamp
|
||||||
@@ -407,34 +418,44 @@ export function flattenDevice(device): FlatDevice {
|
|||||||
else if (+input["_timestamp"] > timestamp)
|
else if (+input["_timestamp"] > timestamp)
|
||||||
childrenTimestamp = +input["_timestamp"];
|
childrenTimestamp = +input["_timestamp"];
|
||||||
|
|
||||||
const attrs = {};
|
const attrs: FlatAttributes = {};
|
||||||
if (tree["_value"] != null) {
|
if (tree["_value"] != null) {
|
||||||
attrs["value"] = [
|
attrs.value = [
|
||||||
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
|
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
|
||||||
tree["_type"]
|
tree["_type"]
|
||||||
];
|
];
|
||||||
attrs["valueTimestamp"] = +(tree["_timestamp"] || childrenTimestamp);
|
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp);
|
||||||
attrs["object"] = false;
|
attrs.object = false;
|
||||||
attrs["objectTimestamp"] = childrenTimestamp;
|
attrs.objectTimestamp = childrenTimestamp;
|
||||||
} else if (tree["_object"] != null) {
|
} else if (tree["_object"] != null) {
|
||||||
attrs["object"] = tree["_object"];
|
attrs.object = tree["_object"];
|
||||||
attrs["objectTimestamp"] = childrenTimestamp;
|
attrs.objectTimestamp = childrenTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tree["_writable"] != null) {
|
if (tree["_writable"] != null) {
|
||||||
attrs["writable"] = tree["_writable"];
|
attrs.writable = tree["_writable"];
|
||||||
attrs["writableTimestamp"] = childrenTimestamp;
|
attrs.writableTimestamp = childrenTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree["_notification"] != null) {
|
||||||
|
attrs.notification = tree["_notification"];
|
||||||
|
attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree["_accessList"] != null) {
|
||||||
|
attrs.accessList = tree["_accessList"];
|
||||||
|
attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = root ? `${root}.${name}` : name;
|
const r = root ? `${root}.${name}` : name;
|
||||||
output[r] = attrs;
|
output[r] = attrs;
|
||||||
|
|
||||||
if (attrs["object"] || tree["object"] == null)
|
if (attrs.object || tree["object"] == null)
|
||||||
recursive(tree, r, output, childrenTimestamp);
|
recursive(tree, r, output, childrenTimestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDevice = {};
|
const newDevice: FlatDevice = {};
|
||||||
const timestamp = new Date(device["_lastInform"] || 1).getTime();
|
const timestamp = new Date(device["_lastInform"] || 1).getTime();
|
||||||
recursive(device, "", newDevice, timestamp);
|
recursive(device, "", newDevice, timestamp);
|
||||||
return newDevice;
|
return newDevice;
|
||||||
@@ -522,9 +543,12 @@ export function mongoQueryToFilter(query): Expression {
|
|||||||
}
|
}
|
||||||
exps.push([op, ["PARAM", k], vv]);
|
exps.push([op, ["PARAM", k], vv]);
|
||||||
}
|
}
|
||||||
if (exps.length === 1) return exps[0];
|
if (exps.length === 1) {
|
||||||
const and: Expression = ["AND"];
|
expressions.push(exps[0]);
|
||||||
expressions.push(and.concat(exps));
|
} else {
|
||||||
|
const and: Expression = ["AND"];
|
||||||
|
expressions.push(and.concat(exps));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
expressions.push(["=", ["PARAM", k], v]);
|
expressions.push(["=", ["PARAM", k], v]);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -21,13 +21,13 @@ import * as url from "url";
|
|||||||
import * as mongodb from "mongodb";
|
import * as mongodb from "mongodb";
|
||||||
import * as querystring from "querystring";
|
import * as querystring from "querystring";
|
||||||
import * as vm from "vm";
|
import * as vm from "vm";
|
||||||
import { exec } from "child_process";
|
|
||||||
import * as config from "./config";
|
import * as config from "./config";
|
||||||
import * as db from "./db";
|
import * as db from "./db";
|
||||||
import * as query from "./query";
|
import * as query from "./query";
|
||||||
import * as apiFunctions from "./api-functions";
|
import * as apiFunctions from "./api-functions";
|
||||||
import * as cache from "./cache";
|
import * as cache from "./cache";
|
||||||
import { version as VERSION } from "../package.json";
|
import { version as VERSION } from "../package.json";
|
||||||
|
import { ping } from "./ping";
|
||||||
|
|
||||||
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
||||||
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
||||||
@@ -591,8 +591,9 @@ export function listener(request, response): void {
|
|||||||
}
|
}
|
||||||
} else if (PING_REGEX.test(urlParts.pathname)) {
|
} else if (PING_REGEX.test(urlParts.pathname)) {
|
||||||
const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]);
|
const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]);
|
||||||
exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => {
|
ping(host, (err, res, stdout) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
if (!res) throwError(err, response);
|
||||||
response.writeHead(404, { "Cache-Control": "no-cache" });
|
response.writeHead(404, { "Cache-Control": "no-cache" });
|
||||||
response.end(`${err.name}: ${err.message}`);
|
response.end(`${err.name}: ${err.message}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { platform } from "os";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
|
||||||
|
interface Ping {
|
||||||
|
packetsTransmitted: number;
|
||||||
|
packetsReceived: number;
|
||||||
|
packetLoss: number;
|
||||||
|
min: number;
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
mdev: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ping(
|
||||||
|
host: string,
|
||||||
|
callback: (err, res?, stdout?) => void
|
||||||
|
): void {
|
||||||
|
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
|
||||||
|
switch (platform()) {
|
||||||
|
case "linux":
|
||||||
|
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
|
||||||
|
parseRegExp1 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
|
||||||
|
parseRegExp2 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "freebsd":
|
||||||
|
// Send a single packet because on FreeBSD only superuser can send
|
||||||
|
// packets that are only 200 ms apart.
|
||||||
|
cmd = `ping -t 1 -c 3 ${host}`;
|
||||||
|
parseRegExp1 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
|
||||||
|
parseRegExp2 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return callback(new Error("Platform not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(cmd, (err, stdout) => {
|
||||||
|
let parsed: Ping;
|
||||||
|
if (stdout) {
|
||||||
|
const m1 = stdout.match(parseRegExp1);
|
||||||
|
if (m1) {
|
||||||
|
parsed = {
|
||||||
|
packetsTransmitted: +m1[1],
|
||||||
|
packetsReceived: +m1[2],
|
||||||
|
packetLoss: +m1[3],
|
||||||
|
min: +m1[4],
|
||||||
|
avg: +m1[5],
|
||||||
|
max: +m1[6],
|
||||||
|
mdev: +m1[7]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const m2 = stdout.match(parseRegExp2);
|
||||||
|
if (m2) {
|
||||||
|
parsed = {
|
||||||
|
packetsTransmitted: +m2[1],
|
||||||
|
packetsReceived: +m2[2],
|
||||||
|
packetLoss: +m2[3],
|
||||||
|
min: null,
|
||||||
|
avg: null,
|
||||||
|
max: null,
|
||||||
|
mdev: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err, parsed, stdout);
|
||||||
|
});
|
||||||
|
}
|
||||||
+4
-3
@@ -46,7 +46,8 @@ function closeServer(timeout, callback): void {
|
|||||||
|
|
||||||
const cb = callback;
|
const cb = callback;
|
||||||
callback = null;
|
callback = null;
|
||||||
cb();
|
// Allow some time for connection close events to fire
|
||||||
|
setTimeout(cb, 50);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ export function start(
|
|||||||
ssl,
|
ssl,
|
||||||
_listener,
|
_listener,
|
||||||
onConnection?,
|
onConnection?,
|
||||||
keepAliveTimeout: number = 0
|
keepAliveTimeout: number = -1
|
||||||
): void {
|
): void {
|
||||||
listener = _listener;
|
listener = _listener;
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ export function start(
|
|||||||
if (onConnection != null) server.on("connection", onConnection);
|
if (onConnection != null) server.on("connection", onConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keepAliveTimeout) server.keepAliveTimeout = keepAliveTimeout;
|
if (keepAliveTimeout >= 0) server.keepAliveTimeout = keepAliveTimeout;
|
||||||
server.listen(port, networkInterface);
|
server.listen(port, networkInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+213
-28
@@ -537,15 +537,29 @@ async function runProvisions(
|
|||||||
if (!allProvisions[provision[0]]) {
|
if (!allProvisions[provision[0]]) {
|
||||||
if (defaultProvisions[provision[0]]) {
|
if (defaultProvisions[provision[0]]) {
|
||||||
const dec = [];
|
const dec = [];
|
||||||
const done = defaultProvisions[provision[0]](
|
let done = true;
|
||||||
sessionContext,
|
let fault = null;
|
||||||
provision,
|
try {
|
||||||
dec,
|
done = defaultProvisions[provision[0]](
|
||||||
startRevision,
|
sessionContext,
|
||||||
endRevision
|
provision,
|
||||||
);
|
dec,
|
||||||
|
startRevision,
|
||||||
|
endRevision
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
fault = {
|
||||||
|
code: `script.${err.name}`,
|
||||||
|
message: err.message,
|
||||||
|
detail: {
|
||||||
|
name: err.name,
|
||||||
|
message: err.message,
|
||||||
|
stack: `${err.name}: ${err.message}\n at ${provision[0]}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
fault: null,
|
fault: fault,
|
||||||
clear: null,
|
clear: null,
|
||||||
declare: dec,
|
declare: dec,
|
||||||
done: done,
|
done: done,
|
||||||
@@ -715,9 +729,12 @@ function runDeclarations(
|
|||||||
exist: new Set(),
|
exist: new Set(),
|
||||||
object: new Set(),
|
object: new Set(),
|
||||||
writable: new Set(),
|
writable: new Set(),
|
||||||
value: new Set()
|
value: new Set(),
|
||||||
|
notification: new Set(),
|
||||||
|
accessList: new Set()
|
||||||
},
|
},
|
||||||
spv: new Map(),
|
spv: new Map(),
|
||||||
|
spa: new Map(),
|
||||||
gpn: new Set<Path>(),
|
gpn: new Set<Path>(),
|
||||||
gpnPatterns: new Map(),
|
gpnPatterns: new Map(),
|
||||||
tags: new Map(),
|
tags: new Map(),
|
||||||
@@ -771,6 +788,9 @@ function runDeclarations(
|
|||||||
let path = declaration.path;
|
let path = declaration.path;
|
||||||
let unpacked: Path[];
|
let unpacked: Path[];
|
||||||
|
|
||||||
|
// Can't run declarations on root
|
||||||
|
if (!path.length) continue;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(path.alias | path.wildcard) & 1 ||
|
(path.alias | path.wildcard) & 1 ||
|
||||||
path.segments[0] === "VirtualParameters"
|
path.segments[0] === "VirtualParameters"
|
||||||
@@ -1446,6 +1466,8 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest {
|
|||||||
syncState.refreshAttributes.value.has(p) ||
|
syncState.refreshAttributes.value.has(p) ||
|
||||||
syncState.refreshAttributes.object.has(p) ||
|
syncState.refreshAttributes.object.has(p) ||
|
||||||
syncState.refreshAttributes.writable.has(p) ||
|
syncState.refreshAttributes.writable.has(p) ||
|
||||||
|
syncState.refreshAttributes.notification.has(p) ||
|
||||||
|
syncState.refreshAttributes.accessList.has(p) ||
|
||||||
syncState.gpn.has(p)
|
syncState.gpn.has(p)
|
||||||
) {
|
) {
|
||||||
found = true;
|
found = true;
|
||||||
@@ -1474,7 +1496,9 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest {
|
|||||||
syncState.refreshAttributes.value.has(p) ||
|
syncState.refreshAttributes.value.has(p) ||
|
||||||
(p.length > path.length &&
|
(p.length > path.length &&
|
||||||
(syncState.refreshAttributes.object.has(p) ||
|
(syncState.refreshAttributes.object.has(p) ||
|
||||||
syncState.refreshAttributes.writable.has(p)))
|
syncState.refreshAttributes.writable.has(p) ||
|
||||||
|
syncState.refreshAttributes.notification.has(p) ||
|
||||||
|
syncState.refreshAttributes.accessList.has(p)))
|
||||||
) {
|
) {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
@@ -1522,7 +1546,7 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest {
|
|||||||
let nextLevel;
|
let nextLevel;
|
||||||
let est = 0;
|
let est = 0;
|
||||||
if (path.length >= GPN_NEXT_LEVEL) {
|
if (path.length >= GPN_NEXT_LEVEL) {
|
||||||
const patterns = [[path, 0]];
|
const patterns: [Path, number][] = [[path, 0]];
|
||||||
for (const p of sessionContext.deviceData.paths.find(
|
for (const p of sessionContext.deviceData.paths.find(
|
||||||
path,
|
path,
|
||||||
true,
|
true,
|
||||||
@@ -1582,9 +1606,57 @@ function generateGetRpcRequest(sessionContext: SessionContext): GetAcsRequest {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
syncState.refreshAttributes.notification.size ||
|
||||||
|
syncState.refreshAttributes.accessList.size
|
||||||
|
) {
|
||||||
|
const GPV_BATCH_SIZE = localCache.getConfig(
|
||||||
|
sessionContext.cacheSnapshot,
|
||||||
|
"cwmp.gpvBatchSize",
|
||||||
|
sessionContext.configContext
|
||||||
|
);
|
||||||
|
|
||||||
|
const parameterNames: string[] = [];
|
||||||
|
for (const path of syncState.refreshAttributes.notification) {
|
||||||
|
syncState.refreshAttributes.notification.delete(path);
|
||||||
|
syncState.refreshAttributes.accessList.delete(path);
|
||||||
|
// Need to check in case param is deleted
|
||||||
|
const attrs = sessionContext.deviceData.attributes.get(path);
|
||||||
|
if (attrs) {
|
||||||
|
parameterNames.push(path.toString());
|
||||||
|
if (parameterNames.length >= GPV_BATCH_SIZE) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterNames.length < GPV_BATCH_SIZE) {
|
||||||
|
for (const path of syncState.refreshAttributes.accessList) {
|
||||||
|
syncState.refreshAttributes.accessList.delete(path);
|
||||||
|
const attrs = sessionContext.deviceData.attributes.get(path);
|
||||||
|
if (attrs) {
|
||||||
|
parameterNames.push(path.toString());
|
||||||
|
if (parameterNames.length >= GPV_BATCH_SIZE) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterNames.length) {
|
||||||
|
return {
|
||||||
|
name: "GetParameterAttributes",
|
||||||
|
parameterNames: parameterNames
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compareAccessLists(list1: string[], list2: string[]): boolean {
|
||||||
|
if (list1.length !== list2.length) return false;
|
||||||
|
for (const [i, v] of list1.entries()) if (v !== list2[i]) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest {
|
function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest {
|
||||||
const syncState = sessionContext.syncState;
|
const syncState = sessionContext.syncState;
|
||||||
if (!syncState) return null;
|
if (!syncState) return null;
|
||||||
@@ -1637,7 +1709,7 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest {
|
|||||||
sessionContext.configContext
|
sessionContext.configContext
|
||||||
);
|
);
|
||||||
|
|
||||||
const parameterValues: [Path, string | number | boolean, string][] = [];
|
const parameterValues: [string, string | number | boolean, string][] = [];
|
||||||
for (const [k, v] of syncState.spv) {
|
for (const [k, v] of syncState.spv) {
|
||||||
syncState.spv.delete(k);
|
syncState.spv.delete(k);
|
||||||
const attrs = sessionContext.deviceData.attributes.get(k);
|
const attrs = sessionContext.deviceData.attributes.get(k);
|
||||||
@@ -1656,7 +1728,7 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest {
|
|||||||
val[0] -= val[0] % 1000;
|
val[0] -= val[0] % 1000;
|
||||||
|
|
||||||
if (val[0] !== curVal[0] || val[1] !== curVal[1])
|
if (val[0] !== curVal[0] || val[1] !== curVal[1])
|
||||||
parameterValues.push([k, val[0], val[1]]);
|
parameterValues.push([k.toString(), val[0], val[1]]);
|
||||||
|
|
||||||
if (parameterValues.length >= GPV_BATCH_SIZE) break;
|
if (parameterValues.length >= GPV_BATCH_SIZE) break;
|
||||||
}
|
}
|
||||||
@@ -1665,19 +1737,44 @@ function generateSetRpcRequest(sessionContext: SessionContext): SetAcsRequest {
|
|||||||
if (parameterValues.length) {
|
if (parameterValues.length) {
|
||||||
return {
|
return {
|
||||||
name: "SetParameterValues",
|
name: "SetParameterValues",
|
||||||
parameterList: parameterValues.map(
|
parameterList: parameterValues,
|
||||||
p =>
|
|
||||||
[p[0].toString(), p[1], p[2]] as [
|
|
||||||
string,
|
|
||||||
string | number | boolean,
|
|
||||||
string
|
|
||||||
]
|
|
||||||
),
|
|
||||||
DATETIME_MILLISECONDS: DATETIME_MILLISECONDS,
|
DATETIME_MILLISECONDS: DATETIME_MILLISECONDS,
|
||||||
BOOLEAN_LITERAL: BOOLEAN_LITERAL
|
BOOLEAN_LITERAL: BOOLEAN_LITERAL
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set attributes
|
||||||
|
const parameterAttributes: [string, number, string[]][] = [];
|
||||||
|
for (const [k, v] of syncState.spa) {
|
||||||
|
syncState.spa.delete(k);
|
||||||
|
const attrs = sessionContext.deviceData.attributes.get(k);
|
||||||
|
|
||||||
|
if (
|
||||||
|
v.notification != null &&
|
||||||
|
(!attrs.notification || v.notification === attrs.notification[1])
|
||||||
|
)
|
||||||
|
v.notification = null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
v.accessList != null &&
|
||||||
|
(!attrs.accessList ||
|
||||||
|
compareAccessLists(v.accessList, attrs.accessList[1]))
|
||||||
|
)
|
||||||
|
v.accessList = null;
|
||||||
|
|
||||||
|
if (v.notification != null || v.accessList != null)
|
||||||
|
parameterAttributes.push([k.toString(), v.notification, v.accessList]);
|
||||||
|
|
||||||
|
if (parameterAttributes.length >= GPV_BATCH_SIZE) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterAttributes.length) {
|
||||||
|
return {
|
||||||
|
name: "SetParameterAttributes",
|
||||||
|
parameterList: parameterAttributes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Downloads
|
// Downloads
|
||||||
for (const [p, t] of syncState.downloadsDownload) {
|
for (const [p, t] of syncState.downloadsDownload) {
|
||||||
const attrs = deviceData.attributes.get(p);
|
const attrs = deviceData.attributes.get(p);
|
||||||
@@ -2092,8 +2189,34 @@ function processDeclarations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (declareAttributeValues && declareAttributeValues.value != null)
|
if (declareAttributeValues) {
|
||||||
syncState.spv.set(currentPath, declareAttributeValues.value);
|
if (declareAttributeValues.value != null)
|
||||||
|
syncState.spv.set(currentPath, declareAttributeValues.value);
|
||||||
|
|
||||||
|
if (declareAttributeValues.notification != null) {
|
||||||
|
const spa = syncState.spa.get(currentPath);
|
||||||
|
if (spa) {
|
||||||
|
spa.notification = declareAttributeValues.notification;
|
||||||
|
} else {
|
||||||
|
syncState.spa.set(currentPath, {
|
||||||
|
notification: declareAttributeValues.notification,
|
||||||
|
accessList: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (declareAttributeValues.accessList != null) {
|
||||||
|
const spa = syncState.spa.get(currentPath);
|
||||||
|
if (spa) {
|
||||||
|
spa.accessList = declareAttributeValues.accessList;
|
||||||
|
} else {
|
||||||
|
syncState.spa.set(currentPath, {
|
||||||
|
notification: null,
|
||||||
|
accessList: declareAttributeValues.accessList
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2226,7 +2349,7 @@ export async function rpcResponse(
|
|||||||
if (p[1] !== rpcReq.instanceValues[p[0]]) {
|
if (p[1] !== rpcReq.instanceValues[p[0]]) {
|
||||||
const v = device.sanitizeParameterValue([
|
const v = device.sanitizeParameterValue([
|
||||||
rpcReq.instanceValues[p[0]],
|
rpcReq.instanceValues[p[0]],
|
||||||
p[2]
|
p[2] as string
|
||||||
]);
|
]);
|
||||||
parameterList.push([p[0], v[0], v[1]]);
|
parameterList.push([p[0], v[0], v[1]]);
|
||||||
}
|
}
|
||||||
@@ -2271,6 +2394,21 @@ export async function rpcResponse(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "GetParameterAttributesResponse":
|
||||||
|
if (rpcReq.name !== "GetParameterAttributes")
|
||||||
|
throw new Error("Response name does not match request name");
|
||||||
|
|
||||||
|
for (const p of rpcRes.parameterList) {
|
||||||
|
toClear = device.set(
|
||||||
|
sessionContext.deviceData,
|
||||||
|
Path.parse(p[0]),
|
||||||
|
timestamp,
|
||||||
|
{ notification: [timestamp, p[1]], accessList: [timestamp, p[2]] },
|
||||||
|
toClear
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "GetParameterNamesResponse":
|
case "GetParameterNamesResponse":
|
||||||
if (rpcReq.name !== "GetParameterNames")
|
if (rpcReq.name !== "GetParameterNames")
|
||||||
@@ -2387,6 +2525,38 @@ export async function rpcResponse(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "SetParameterAttributesResponse":
|
||||||
|
if (rpcReq.name !== "SetParameterAttributes")
|
||||||
|
throw new Error("Response name does not match request name");
|
||||||
|
|
||||||
|
for (const p of rpcReq.parameterList) {
|
||||||
|
let attrs;
|
||||||
|
|
||||||
|
if (p[1] != null && p[2] != null) {
|
||||||
|
attrs = {
|
||||||
|
notification: [timestamp + 1, p[1]],
|
||||||
|
accessList: [timestamp + 1, p[2]]
|
||||||
|
};
|
||||||
|
} else if (p[1] != null) {
|
||||||
|
attrs = {
|
||||||
|
notification: [timestamp + 1, p[1]]
|
||||||
|
};
|
||||||
|
} else if (p[2] != null) {
|
||||||
|
attrs = {
|
||||||
|
accessList: [timestamp + 1, p[2]]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toClear = device.set(
|
||||||
|
sessionContext.deviceData,
|
||||||
|
Path.parse(p[0]),
|
||||||
|
timestamp + 1,
|
||||||
|
attrs,
|
||||||
|
toClear
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "AddObjectResponse":
|
case "AddObjectResponse":
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
@@ -2531,7 +2701,7 @@ export async function rpcFault(
|
|||||||
|
|
||||||
// Recover from invalid parameter name faults
|
// Recover from invalid parameter name faults
|
||||||
if (faultResponse.detail.faultCode === "9005") {
|
if (faultResponse.detail.faultCode === "9005") {
|
||||||
const timestamp = sessionContext.timestamp + sessionContext.iteration;
|
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
|
||||||
const revision =
|
const revision =
|
||||||
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
|
(sessionContext.revisions[sessionContext.revisions.length - 1] || 0) + 1;
|
||||||
sessionContext.deviceData.timestamps.revision = revision;
|
sessionContext.deviceData.timestamps.revision = revision;
|
||||||
@@ -2549,13 +2719,23 @@ export async function rpcFault(
|
|||||||
p => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear
|
p => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear
|
||||||
);
|
);
|
||||||
} else if (rpcReq.name === "SetParameterValues") {
|
} else if (rpcReq.name === "SetParameterValues") {
|
||||||
toClear = rpcReq.parameterList.map(
|
toClear = (rpcReq.parameterList as [
|
||||||
p => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear
|
string,
|
||||||
);
|
string | number | boolean,
|
||||||
|
string
|
||||||
|
][]).map(p => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear);
|
||||||
} else if (rpcReq.name === "AddObject") {
|
} else if (rpcReq.name === "AddObject") {
|
||||||
toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]];
|
toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]];
|
||||||
} else if (rpcReq.name === "DeleteObject") {
|
} else if (rpcReq.name === "DeleteObject") {
|
||||||
toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]];
|
toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]];
|
||||||
|
} else if (rpcReq.name === "GetParameterAttributes") {
|
||||||
|
toClear = rpcReq.parameterNames.map(
|
||||||
|
p => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear
|
||||||
|
);
|
||||||
|
} else if (rpcReq.name === "SetParameterAttributes") {
|
||||||
|
toClear = (rpcReq.parameterList as [string, number, string[]][]).map(
|
||||||
|
p => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toClear) {
|
if (toClear) {
|
||||||
@@ -2621,8 +2801,13 @@ export async function serialize(
|
|||||||
deviceData.push(e);
|
deviceData.push(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const declarations = sessionContext.declarations.map(decs => {
|
||||||
|
return decs.map(d => Object.assign({}, d, { path: d.path.toString() }));
|
||||||
|
});
|
||||||
|
|
||||||
const jsonSessionContext = Object.assign({}, sessionContext, {
|
const jsonSessionContext = Object.assign({}, sessionContext, {
|
||||||
deviceData: deviceData,
|
deviceData: deviceData,
|
||||||
|
declarations: declarations,
|
||||||
syncState: null,
|
syncState: null,
|
||||||
toLoad: null,
|
toLoad: null,
|
||||||
httpRequest: null,
|
httpRequest: null,
|
||||||
|
|||||||
+98
@@ -188,6 +188,45 @@ function parameterValueList(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parameterAttributeList(xml: Element): [string, number, string[]][] {
|
||||||
|
return xml.children
|
||||||
|
.filter(e => e.localName === "ParameterAttributeStruct")
|
||||||
|
.map<[string, number, string[]]>(e => {
|
||||||
|
let notificationElement: Element,
|
||||||
|
accessListElement: Element,
|
||||||
|
param: string;
|
||||||
|
for (const c of e.children) {
|
||||||
|
switch (c.localName) {
|
||||||
|
case "Name":
|
||||||
|
param = c.text;
|
||||||
|
break;
|
||||||
|
case "Notification":
|
||||||
|
notificationElement = c;
|
||||||
|
break;
|
||||||
|
case "AccessList":
|
||||||
|
accessListElement = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = decodeEntities(notificationElement.text);
|
||||||
|
const notif = +notification;
|
||||||
|
|
||||||
|
if (notif !== parseInt(notification)) {
|
||||||
|
warnings.push({
|
||||||
|
message: "Invalid notification attribute",
|
||||||
|
parameter: param
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessList = accessListElement.children
|
||||||
|
.filter(c => c.localName === "string")
|
||||||
|
.map(c => decodeEntities(c.text));
|
||||||
|
|
||||||
|
return [param, notif, accessList];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function GetParameterNames(methodRequest): string {
|
function GetParameterNames(methodRequest): string {
|
||||||
return `<cwmp:GetParameterNames><ParameterPath>${
|
return `<cwmp:GetParameterNames><ParameterPath>${
|
||||||
methodRequest.parameterPath
|
methodRequest.parameterPath
|
||||||
@@ -220,6 +259,23 @@ function GetParameterValuesResponse(xml: Element): CpeGetResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GetParameterAttributes(methodRequest): string {
|
||||||
|
return `<cwmp:GetParameterAttributes><ParameterNames soap-enc:arrayType="xsd:string[${
|
||||||
|
methodRequest.parameterNames.length
|
||||||
|
}]">${methodRequest.parameterNames
|
||||||
|
.map(p => `<string>${p}</string>`)
|
||||||
|
.join("")}</ParameterNames></cwmp:GetParameterAttributes>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetParameterAttributesResponse(xml: Element): CpeGetResponse {
|
||||||
|
return {
|
||||||
|
name: "GetParameterAttributesResponse",
|
||||||
|
parameterList: parameterAttributeList(
|
||||||
|
xml.children.find(n => n.localName === "ParameterList")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function SetParameterValues(methodRequest): string {
|
function SetParameterValues(methodRequest): string {
|
||||||
const params = methodRequest.parameterList.map(p => {
|
const params = methodRequest.parameterList.map(p => {
|
||||||
let val = p[1];
|
let val = p[1];
|
||||||
@@ -250,6 +306,36 @@ function SetParameterValuesResponse(xml: Element): CpeSetResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SetParameterAttributes(methodRequest): string {
|
||||||
|
const params = methodRequest.parameterList.map(p => {
|
||||||
|
return `<SetParameterAttributesStruct><Name>${
|
||||||
|
p[0]
|
||||||
|
}</Name><NotificationChange>${
|
||||||
|
p[1] == null ? "false" : "true"
|
||||||
|
}</NotificationChange><Notification>${
|
||||||
|
p[1] == null ? "" : p[1]
|
||||||
|
}</Notification><AccessListChange>${
|
||||||
|
p[2] == null ? "false" : "true"
|
||||||
|
}</AccessListChange><AccessList soap-enc:arrayType="xsd:string[${
|
||||||
|
(p[2] || []).length
|
||||||
|
}]">${
|
||||||
|
p[2] == null
|
||||||
|
? ""
|
||||||
|
: p[2].map(s => `<string>${encodeEntities(s)}</string>`).join("")
|
||||||
|
}</AccessList></SetParameterAttributesStruct>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `<cwmp:SetParameterAttributes><ParameterList soap-enc:arrayType="cwmp:SetParameterAttributesStruct[${
|
||||||
|
methodRequest.parameterList.length
|
||||||
|
}]">${params.join("")}</ParameterList></cwmp:SetParameterAttributes>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetParameterAttributesResponse(): CpeSetResponse {
|
||||||
|
return {
|
||||||
|
name: "SetParameterAttributesResponse"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function AddObject(methodRequest): string {
|
function AddObject(methodRequest): string {
|
||||||
return `<cwmp:AddObject><ObjectName>${
|
return `<cwmp:AddObject><ObjectName>${
|
||||||
methodRequest.objectName
|
methodRequest.objectName
|
||||||
@@ -610,9 +696,15 @@ export function request(body: string, cwmpVersion, warn): SoapMessage {
|
|||||||
case "GetParameterValuesResponse":
|
case "GetParameterValuesResponse":
|
||||||
rpc.cpeResponse = GetParameterValuesResponse(methodElement);
|
rpc.cpeResponse = GetParameterValuesResponse(methodElement);
|
||||||
break;
|
break;
|
||||||
|
case "GetParameterAttributesResponse":
|
||||||
|
rpc.cpeResponse = GetParameterAttributesResponse(methodElement);
|
||||||
|
break;
|
||||||
case "SetParameterValuesResponse":
|
case "SetParameterValuesResponse":
|
||||||
rpc.cpeResponse = SetParameterValuesResponse(methodElement);
|
rpc.cpeResponse = SetParameterValuesResponse(methodElement);
|
||||||
break;
|
break;
|
||||||
|
case "SetParameterAttributesResponse":
|
||||||
|
rpc.cpeResponse = SetParameterAttributesResponse();
|
||||||
|
break;
|
||||||
case "AddObjectResponse":
|
case "AddObjectResponse":
|
||||||
rpc.cpeResponse = AddObjectResponse(methodElement);
|
rpc.cpeResponse = AddObjectResponse(methodElement);
|
||||||
break;
|
break;
|
||||||
@@ -690,9 +782,15 @@ export function response(rpc): { code: number; headers: {}; data: string } {
|
|||||||
case "GetParameterValues":
|
case "GetParameterValues":
|
||||||
body = GetParameterValues(rpc.acsRequest);
|
body = GetParameterValues(rpc.acsRequest);
|
||||||
break;
|
break;
|
||||||
|
case "GetParameterAttributes":
|
||||||
|
body = GetParameterAttributes(rpc.acsRequest);
|
||||||
|
break;
|
||||||
case "SetParameterValues":
|
case "SetParameterValues":
|
||||||
body = SetParameterValues(rpc.acsRequest);
|
body = SetParameterValues(rpc.acsRequest);
|
||||||
break;
|
break;
|
||||||
|
case "SetParameterAttributes":
|
||||||
|
body = SetParameterAttributes(rpc.acsRequest);
|
||||||
|
break;
|
||||||
case "AddObject":
|
case "AddObject":
|
||||||
body = AddObject(rpc.acsRequest);
|
body = AddObject(rpc.acsRequest);
|
||||||
break;
|
break;
|
||||||
|
|||||||
+48
-7
@@ -51,18 +51,24 @@ export interface Attributes {
|
|||||||
object?: [number, 1 | 0];
|
object?: [number, 1 | 0];
|
||||||
writable?: [number, 1 | 0];
|
writable?: [number, 1 | 0];
|
||||||
value?: [number, [string | number | boolean, string]];
|
value?: [number, [string | number | boolean, string]];
|
||||||
|
notification?: [number, number];
|
||||||
|
accessList?: [number, string[]];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttributeTimestamps {
|
export interface AttributeTimestamps {
|
||||||
object?: number;
|
object?: number;
|
||||||
writable?: number;
|
writable?: number;
|
||||||
value?: number;
|
value?: number;
|
||||||
|
notification?: number;
|
||||||
|
accessList?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AttributeValues {
|
export interface AttributeValues {
|
||||||
object?: boolean;
|
object?: boolean;
|
||||||
writable?: boolean;
|
writable?: boolean;
|
||||||
value?: [string | number | boolean, string?];
|
value?: [string | number | boolean, string?];
|
||||||
|
notification?: number;
|
||||||
|
accessList?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceData {
|
export interface DeviceData {
|
||||||
@@ -75,12 +81,21 @@ export interface DeviceData {
|
|||||||
|
|
||||||
export type VirtualParameterDeclaration = [
|
export type VirtualParameterDeclaration = [
|
||||||
Path,
|
Path,
|
||||||
{ path?: number; object?: number; writable?: number; value?: number }?,
|
{
|
||||||
|
path?: number;
|
||||||
|
object?: number;
|
||||||
|
writable?: number;
|
||||||
|
value?: number;
|
||||||
|
notification?: number;
|
||||||
|
accessList?: number;
|
||||||
|
}?,
|
||||||
{
|
{
|
||||||
path?: [number, number];
|
path?: [number, number];
|
||||||
object?: boolean;
|
object?: boolean;
|
||||||
writable?: boolean;
|
writable?: boolean;
|
||||||
value?: [string | number | boolean, string?];
|
value?: [string | number | boolean, string?];
|
||||||
|
notification?: number;
|
||||||
|
accessList?: string[];
|
||||||
}?
|
}?
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -90,8 +105,11 @@ export interface SyncState {
|
|||||||
object: Set<Path>;
|
object: Set<Path>;
|
||||||
writable: Set<Path>;
|
writable: Set<Path>;
|
||||||
value: Set<Path>;
|
value: Set<Path>;
|
||||||
|
notification: Set<Path>;
|
||||||
|
accessList: Set<Path>;
|
||||||
};
|
};
|
||||||
spv: Map<Path, [string | number | boolean, string]>;
|
spv: Map<Path, [string | number | boolean, string]>;
|
||||||
|
spa: Map<Path, { notification: number; accessList: string[] }>;
|
||||||
gpn: Set<Path>;
|
gpn: Set<Path>;
|
||||||
gpnPatterns: Map<Path, number>;
|
gpnPatterns: Map<Path, number>;
|
||||||
tags: Map<Path, boolean>;
|
tags: Map<Path, boolean>;
|
||||||
@@ -160,6 +178,7 @@ export interface Task {
|
|||||||
fileName?: string;
|
fileName?: string;
|
||||||
targetFileName?: string;
|
targetFileName?: string;
|
||||||
expiry?: number;
|
expiry?: number;
|
||||||
|
provisions?: (string | number | boolean)[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Operation {
|
export interface Operation {
|
||||||
@@ -182,7 +201,7 @@ export interface AcsRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAcsRequest extends AcsRequest {
|
export interface GetAcsRequest extends AcsRequest {
|
||||||
name: "GetParameterNames" | "GetParameterValues";
|
name: "GetParameterNames" | "GetParameterValues" | "GetParameterAttributes";
|
||||||
objectName?: string;
|
objectName?: string;
|
||||||
parameterNames?: string[];
|
parameterNames?: string[];
|
||||||
parameterPath?: string;
|
parameterPath?: string;
|
||||||
@@ -193,12 +212,15 @@ export interface GetAcsRequest extends AcsRequest {
|
|||||||
export interface SetAcsRequest extends AcsRequest {
|
export interface SetAcsRequest extends AcsRequest {
|
||||||
name:
|
name:
|
||||||
| "SetParameterValues"
|
| "SetParameterValues"
|
||||||
|
| "SetParameterAttributes"
|
||||||
| "AddObject"
|
| "AddObject"
|
||||||
| "DeleteObject"
|
| "DeleteObject"
|
||||||
| "FactoryReset"
|
| "FactoryReset"
|
||||||
| "Reboot"
|
| "Reboot"
|
||||||
| "Download";
|
| "Download";
|
||||||
parameterList?: [string, string | number | boolean, string][];
|
parameterList?:
|
||||||
|
| [string, string | number | boolean, string][]
|
||||||
|
| [string, number, string[]][];
|
||||||
instanceValues?: { [param: string]: string | number | boolean };
|
instanceValues?: { [param: string]: string | number | boolean };
|
||||||
objectName?: string;
|
objectName?: string;
|
||||||
DATETIME_MILLISECONDS?: boolean;
|
DATETIME_MILLISECONDS?: boolean;
|
||||||
@@ -235,15 +257,20 @@ export interface CpeFault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CpeGetResponse extends CpeResponse {
|
export interface CpeGetResponse extends CpeResponse {
|
||||||
name: "GetParameterNamesResponse" | "GetParameterValuesResponse";
|
name:
|
||||||
|
| "GetParameterNamesResponse"
|
||||||
|
| "GetParameterValuesResponse"
|
||||||
|
| "GetParameterAttributesResponse";
|
||||||
parameterList?:
|
parameterList?:
|
||||||
| [string, boolean][]
|
| [string, boolean][]
|
||||||
| [string, string | number | boolean, string][];
|
| [string, string | number | boolean, string][]
|
||||||
|
| [string, number, string[]][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CpeSetResponse extends CpeResponse {
|
export interface CpeSetResponse extends CpeResponse {
|
||||||
name:
|
name:
|
||||||
| "SetParameterValuesResponse"
|
| "SetParameterValuesResponse"
|
||||||
|
| "SetParameterAttributesResponse"
|
||||||
| "AddObjectResponse"
|
| "AddObjectResponse"
|
||||||
| "DeleteObjectResponse"
|
| "DeleteObjectResponse"
|
||||||
| "RebootResponse"
|
| "RebootResponse"
|
||||||
@@ -300,11 +327,19 @@ export interface Declaration {
|
|||||||
path: Path;
|
path: Path;
|
||||||
pathGet: number;
|
pathGet: number;
|
||||||
pathSet?: [number, number];
|
pathSet?: [number, number];
|
||||||
attrGet?: { object?: number; writable?: number; value?: number };
|
attrGet?: {
|
||||||
|
object?: number;
|
||||||
|
writable?: number;
|
||||||
|
value?: number;
|
||||||
|
notification?: number;
|
||||||
|
accessList?: number;
|
||||||
|
};
|
||||||
attrSet?: {
|
attrSet?: {
|
||||||
object?: boolean;
|
object?: boolean;
|
||||||
writable?: boolean;
|
writable?: boolean;
|
||||||
value?: [string | number | boolean, string?];
|
value?: [string | number | boolean, string?];
|
||||||
|
notification?: number;
|
||||||
|
accessList?: string[];
|
||||||
};
|
};
|
||||||
defer: boolean;
|
defer: boolean;
|
||||||
}
|
}
|
||||||
@@ -312,7 +347,13 @@ export interface Declaration {
|
|||||||
export type Clear = [
|
export type Clear = [
|
||||||
Path,
|
Path,
|
||||||
number,
|
number,
|
||||||
{ object?: number; writable?: number; value?: number }?,
|
{
|
||||||
|
object?: number;
|
||||||
|
writable?: number;
|
||||||
|
value?: number;
|
||||||
|
notification?: number;
|
||||||
|
accessList?: number;
|
||||||
|
}?,
|
||||||
number?
|
number?
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import Authorizer from "./common/authorizer";
|
|||||||
import * as logger from "./logger";
|
import * as logger from "./logger";
|
||||||
import * as localCache from "./local-cache";
|
import * as localCache from "./local-cache";
|
||||||
import { PermissionSet } from "./types";
|
import { PermissionSet } from "./types";
|
||||||
import { authSimple } from "./ui/api-functions";
|
import { authLocal } from "./ui/api-functions";
|
||||||
import * as init from "./init";
|
import * as init from "./init";
|
||||||
import { version as VERSION } from "../package.json";
|
import { version as VERSION } from "../package.json";
|
||||||
|
|
||||||
@@ -47,16 +47,6 @@ const router = new Router();
|
|||||||
const JWT_SECRET = "" + config.get("UI_JWT_SECRET");
|
const JWT_SECRET = "" + config.get("UI_JWT_SECRET");
|
||||||
const JWT_COOKIE = "genieacs-ui-jwt";
|
const JWT_COOKIE = "genieacs-ui-jwt";
|
||||||
|
|
||||||
function getPermissionSets(ctx): PermissionSet[] {
|
|
||||||
const allPermissions = localCache.getPermissions(ctx.state.configSnapshot);
|
|
||||||
const users = localCache.getUsers(ctx.state.configSnapshot);
|
|
||||||
const user = users[ctx.state.user.username];
|
|
||||||
const permissionSets = user.roles.map(role =>
|
|
||||||
Object.values(allPermissions[role] || {})
|
|
||||||
);
|
|
||||||
return permissionSets;
|
|
||||||
}
|
|
||||||
|
|
||||||
koa.on("error", async err => {
|
koa.on("error", async err => {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@@ -73,14 +63,39 @@ koa.use(
|
|||||||
koaJwt({
|
koaJwt({
|
||||||
secret: JWT_SECRET,
|
secret: JWT_SECRET,
|
||||||
passthrough: true,
|
passthrough: true,
|
||||||
cookie: JWT_COOKIE
|
cookie: JWT_COOKIE,
|
||||||
|
isRevoked: async (ctx, token) => {
|
||||||
|
if (token["authMethod"] === "local") {
|
||||||
|
return !localCache.getUsers(ctx.state.configSnapshot)[
|
||||||
|
token["username"]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
koa.use(async (ctx, next) => {
|
koa.use(async (ctx, next) => {
|
||||||
if (ctx.state.user && ctx.state.user.username)
|
const permissionSets: PermissionSet[] = [];
|
||||||
ctx.state.authorizer = new Authorizer(getPermissionSets(ctx));
|
|
||||||
else ctx.state.authorizer = new Authorizer([]);
|
if (ctx.state.user && ctx.state.user.username) {
|
||||||
|
const allPermissions = localCache.getPermissions(ctx.state.configSnapshot);
|
||||||
|
|
||||||
|
let user;
|
||||||
|
if (ctx.state.user.authMethod === "local") {
|
||||||
|
user = localCache.getUsers(ctx.state.configSnapshot)[
|
||||||
|
ctx.state.user.username
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid auth method");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const role of (user.roles || []).sort())
|
||||||
|
permissionSets.push(Object.values(allPermissions[role] || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.state.authorizer = new Authorizer(permissionSets);
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -103,9 +118,9 @@ router.post("/login", async ctx => {
|
|||||||
method: null
|
method: null
|
||||||
};
|
};
|
||||||
|
|
||||||
function success(method): void {
|
function success(authMethod): void {
|
||||||
log.method = method;
|
log.method = authMethod;
|
||||||
const token = jwt.sign({ username }, JWT_SECRET);
|
const token = jwt.sign({ username, authMethod }, JWT_SECRET);
|
||||||
ctx.cookies.set(JWT_COOKIE, token, { sameSite: "lax" });
|
ctx.cookies.set(JWT_COOKIE, token, { sameSite: "lax" });
|
||||||
ctx.body = JSON.stringify(token);
|
ctx.body = JSON.stringify(token);
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
@@ -118,9 +133,8 @@ router.post("/login", async ctx => {
|
|||||||
logger.accessWarn(log);
|
logger.accessWarn(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roles = await authSimple(ctx.state.configSnapshot, username, password);
|
if (await authLocal(ctx.state.configSnapshot, username, password))
|
||||||
|
return void success("local");
|
||||||
if (roles) return void success("simple");
|
|
||||||
|
|
||||||
failure();
|
failure();
|
||||||
});
|
});
|
||||||
@@ -192,9 +206,7 @@ router.post("/init", async ctx => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/", async ctx => {
|
router.get("/", async ctx => {
|
||||||
let permissionSets = [];
|
const permissionSets = ctx.state.authorizer.getPermissionSets();
|
||||||
if (ctx.state.user && ctx.state.user.username)
|
|
||||||
permissionSets = getPermissionSets(ctx);
|
|
||||||
|
|
||||||
let wizard = "";
|
let wizard = "";
|
||||||
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
|
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
|
||||||
|
|||||||
+3
-27
@@ -17,7 +17,6 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { exec } from "child_process";
|
|
||||||
import { ObjectID } from "mongodb";
|
import { ObjectID } from "mongodb";
|
||||||
import * as db from "./db";
|
import * as db from "./db";
|
||||||
import { del } from "../cache";
|
import { del } from "../cache";
|
||||||
@@ -149,29 +148,6 @@ interface PingResponse {
|
|||||||
mdev: number;
|
mdev: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ping(host): Promise<PingResponse> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => {
|
|
||||||
if (err) return void reject(err);
|
|
||||||
|
|
||||||
const m = stdout.match(
|
|
||||||
/(\d) packets transmitted, (\d) received, ([\d.%]+) packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/
|
|
||||||
);
|
|
||||||
if (!m) return void reject(new Error("Could not parse ping response"));
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
packetsTransmitted: +m[1],
|
|
||||||
packetsReceived: +m[2],
|
|
||||||
packetLoss: m[3],
|
|
||||||
min: +m[4],
|
|
||||||
avg: +m[5],
|
|
||||||
max: +m[6],
|
|
||||||
mdev: +m[7]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function putResource(resource, id, data): Promise<void> {
|
export async function putResource(resource, id, data): Promise<void> {
|
||||||
if (resource === "presets") {
|
if (resource === "presets") {
|
||||||
await db.putPreset(id, data);
|
await db.putPreset(id, data);
|
||||||
@@ -192,15 +168,15 @@ export async function putResource(resource, id, data): Promise<void> {
|
|||||||
await del("presets_hash");
|
await del("presets_hash");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authSimple(snapshot, username, password): Promise<string[]> {
|
export function authLocal(snapshot, username, password): Promise<boolean> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const users = getUsers(snapshot);
|
const users = getUsers(snapshot);
|
||||||
const user = users[username];
|
const user = users[username];
|
||||||
if (!user || !user.password) return void resolve(null);
|
if (!user || !user.password) return void resolve(null);
|
||||||
hashPassword(password, user.salt)
|
hashPassword(password, user.salt)
|
||||||
.then(hash => {
|
.then(hash => {
|
||||||
if (hash === user.password) resolve(user.roles);
|
if (hash === user.password) resolve(true);
|
||||||
else resolve(null);
|
else resolve(false);
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|||||||
+82
-37
@@ -28,6 +28,8 @@ import { getConfig } from "../local-cache";
|
|||||||
import { QueryOptions, Expression } from "../types";
|
import { QueryOptions, Expression } from "../types";
|
||||||
import { generateSalt, hashPassword } from "../auth";
|
import { generateSalt, hashPassword } from "../auth";
|
||||||
import { del } from "../cache";
|
import { del } from "../cache";
|
||||||
|
import Authorizer from "../common/authorizer";
|
||||||
|
import { ping } from "../ping";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
export default router;
|
export default router;
|
||||||
@@ -67,15 +69,20 @@ const resources = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
router.get(`/devices/:id.csv`, async ctx => {
|
router.get(`/devices/:id.csv`, async ctx => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
message: "Query device (CSV)",
|
message: "Query device (CSV)",
|
||||||
context: ctx,
|
context: ctx,
|
||||||
id: ctx.params.id
|
id: ctx.params.id
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = ["=", ["PARAM", RESOURCE_IDS.devices], ctx.params.id];
|
const filter = and(authorizer.getFilter("devices", 2), [
|
||||||
|
"=",
|
||||||
|
["PARAM", RESOURCE_IDS.devices],
|
||||||
|
ctx.params.id
|
||||||
|
]);
|
||||||
|
|
||||||
if (!ctx.state.authorizer.hasAccess("devices", 2)) {
|
if (!authorizer.hasAccess("devices", 2)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void (ctx.status = 404);
|
return void (ctx.status = 404);
|
||||||
}
|
}
|
||||||
@@ -92,7 +99,7 @@ router.get(`/devices/:id.csv`, async ctx => {
|
|||||||
|
|
||||||
ctx.body = new stream.PassThrough();
|
ctx.body = new stream.PassThrough();
|
||||||
ctx.body.write(
|
ctx.body.write(
|
||||||
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp\n"
|
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const k of Object.keys(res[0]).sort()) {
|
for (const k of Object.keys(res[0]).sort()) {
|
||||||
@@ -105,7 +112,11 @@ router.get(`/devices/:id.csv`, async ctx => {
|
|||||||
p.writableTimestamp,
|
p.writableTimestamp,
|
||||||
p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "",
|
p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "",
|
||||||
p.value != null ? p.value[1] : "",
|
p.value != null ? p.value[1] : "",
|
||||||
p.valueTimestamp
|
p.valueTimestamp,
|
||||||
|
p.notification,
|
||||||
|
p.notificationTimestamp,
|
||||||
|
p.accessList ? p.accessList.join(", ") : "",
|
||||||
|
p.accessListTimestamp
|
||||||
];
|
];
|
||||||
ctx.body.write(row.map(r => (r != null ? r : "")).join(",") + "\n");
|
ctx.body.write(row.map(r => (r != null ? r : "")).join(",") + "\n");
|
||||||
}
|
}
|
||||||
@@ -115,8 +126,10 @@ router.get(`/devices/:id.csv`, async ctx => {
|
|||||||
|
|
||||||
for (const [resource, flags] of Object.entries(resources)) {
|
for (const [resource, flags] of Object.entries(resources)) {
|
||||||
router.head(`/${resource}`, async (ctx, next) => {
|
router.head(`/${resource}`, async (ctx, next) => {
|
||||||
let filter: Expression = true;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
|
let filter: Expression = authorizer.getFilter(resource, 1);
|
||||||
|
if (ctx.request.query.filter)
|
||||||
|
filter = and(filter, parse(ctx.request.query.filter));
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: `Count ${resource}`,
|
message: `Count ${resource}`,
|
||||||
@@ -125,7 +138,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
count: null
|
count: null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!ctx.state.authorizer.hasAccess(resource, 1)) {
|
if (!authorizer.hasAccess(resource, 1)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
}
|
}
|
||||||
@@ -148,9 +161,11 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get(`/${resource}`, async (ctx, next) => {
|
router.get(`/${resource}`, async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const options: QueryOptions = {};
|
const options: QueryOptions = {};
|
||||||
let filter: Expression = true;
|
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||||
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
|
if (ctx.request.query.filter)
|
||||||
|
filter = and(filter, parse(ctx.request.query.filter));
|
||||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||||
if (ctx.request.query.sort)
|
if (ctx.request.query.sort)
|
||||||
@@ -171,7 +186,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
projection: options.projection
|
projection: options.projection
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
if (!authorizer.hasAccess(resource, 2)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
}
|
}
|
||||||
@@ -199,9 +214,11 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
|
|
||||||
// CSV download
|
// CSV download
|
||||||
router.get(`/${resource}.csv`, async (ctx, next) => {
|
router.get(`/${resource}.csv`, async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const options: QueryOptions = { projection: {} };
|
const options: QueryOptions = { projection: {} };
|
||||||
let filter: Expression = true;
|
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||||
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
|
if (ctx.request.query.filter)
|
||||||
|
filter = and(filter, parse(ctx.request.query.filter));
|
||||||
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
if (ctx.request.query.limit) options.limit = +ctx.request.query.limit;
|
||||||
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
if (ctx.request.query.skip) options.skip = +ctx.request.query.skip;
|
||||||
if (ctx.request.query.sort)
|
if (ctx.request.query.sort)
|
||||||
@@ -216,7 +233,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
sort: options.sort
|
sort: options.sort
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
if (!authorizer.hasAccess(resource, 2)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
}
|
}
|
||||||
@@ -285,14 +302,19 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.head(`/${resource}/:id`, async (ctx, next) => {
|
router.head(`/${resource}/:id`, async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
message: `Count ${resource}`,
|
message: `Count ${resource}`,
|
||||||
context: ctx,
|
context: ctx,
|
||||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
|
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
|
const filter = and(authorizer.getFilter(resource, 2), [
|
||||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
"=",
|
||||||
|
["PARAM", RESOURCE_IDS[resource]],
|
||||||
|
ctx.params.id
|
||||||
|
]);
|
||||||
|
if (!authorizer.hasAccess(resource, 2)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
}
|
}
|
||||||
@@ -306,14 +328,19 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get(`/${resource}/:id`, async (ctx, next) => {
|
router.get(`/${resource}/:id`, async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
message: `Query ${resource}`,
|
message: `Query ${resource}`,
|
||||||
context: ctx,
|
context: ctx,
|
||||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
|
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
|
||||||
};
|
};
|
||||||
|
|
||||||
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
|
const filter = and(authorizer.getFilter(resource, 2), [
|
||||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
"=",
|
||||||
|
["PARAM", RESOURCE_IDS[resource]],
|
||||||
|
ctx.params.id
|
||||||
|
]);
|
||||||
|
if (!authorizer.hasAccess(resource, 2)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
}
|
}
|
||||||
@@ -326,17 +353,20 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
ctx.body = res[0];
|
ctx.body = res[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO add PUT, PATCH routes
|
|
||||||
if (flags & RESOURCE_DELETE) {
|
if (flags & RESOURCE_DELETE) {
|
||||||
router.delete(`/${resource}/:id`, async (ctx, next) => {
|
router.delete(`/${resource}/:id`, async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
message: `Delete ${resource}`,
|
message: `Delete ${resource}`,
|
||||||
context: ctx,
|
context: ctx,
|
||||||
id: ctx.params.id
|
id: ctx.params.id
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizer = ctx.state.authorizer;
|
const filter = and(authorizer.getFilter(resource, 3), [
|
||||||
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
|
"=",
|
||||||
|
["PARAM", RESOURCE_IDS[resource]],
|
||||||
|
ctx.params.id
|
||||||
|
]);
|
||||||
if (!authorizer.hasAccess(resource, 3)) {
|
if (!authorizer.hasAccess(resource, 3)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
@@ -360,6 +390,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
|
|
||||||
if (flags & RESOURCE_PUT) {
|
if (flags & RESOURCE_PUT) {
|
||||||
router.put(`/${resource}/:id`, async (ctx, next) => {
|
router.put(`/${resource}/:id`, async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
@@ -368,7 +399,6 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
id: id
|
id: id
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizer = ctx.state.authorizer;
|
|
||||||
if (!authorizer.hasAccess(resource, 3)) {
|
if (!authorizer.hasAccess(resource, 3)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
@@ -399,6 +429,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
router.put("/files/:id", async (ctx, next) => {
|
router.put("/files/:id", async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const resource = "files";
|
const resource = "files";
|
||||||
const id = ctx.params.id;
|
const id = ctx.params.id;
|
||||||
|
|
||||||
@@ -409,7 +440,6 @@ router.put("/files/:id", async (ctx, next) => {
|
|||||||
metadata: null
|
metadata: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizer = ctx.state.authorizer;
|
|
||||||
if (!authorizer.hasAccess(resource, 3)) {
|
if (!authorizer.hasAccess(resource, 3)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
@@ -436,6 +466,7 @@ router.put("/files/:id", async (ctx, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post("/devices/:id/tasks", async (ctx, next) => {
|
router.post("/devices/:id/tasks", async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
message: "Commit tasks",
|
message: "Commit tasks",
|
||||||
context: ctx,
|
context: ctx,
|
||||||
@@ -443,8 +474,11 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
|
|||||||
tasks: null
|
tasks: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizer = ctx.state.authorizer;
|
const filter = and(authorizer.getFilter("devices", 3), [
|
||||||
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
|
"=",
|
||||||
|
["PARAM", "DeviceID.ID"],
|
||||||
|
ctx.params.id
|
||||||
|
]);
|
||||||
if (!authorizer.hasAccess("devices", 3)) {
|
if (!authorizer.hasAccess("devices", 3)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
@@ -481,6 +515,7 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post("/devices/:id/tags", async (ctx, next) => {
|
router.post("/devices/:id/tags", async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
message: "Update tags",
|
message: "Update tags",
|
||||||
context: ctx,
|
context: ctx,
|
||||||
@@ -488,8 +523,11 @@ router.post("/devices/:id/tags", async (ctx, next) => {
|
|||||||
tags: ctx.request.body
|
tags: ctx.request.body
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizer = ctx.state.authorizer;
|
const filter = and(authorizer.getFilter("devices", 3), [
|
||||||
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
|
"=",
|
||||||
|
["PARAM", "DeviceID.ID"],
|
||||||
|
ctx.params.id
|
||||||
|
]);
|
||||||
if (!authorizer.hasAccess("devices", 3)) {
|
if (!authorizer.hasAccess("devices", 3)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void next();
|
return void next();
|
||||||
@@ -518,16 +556,21 @@ router.post("/devices/:id/tags", async (ctx, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/ping/:host", async ctx => {
|
router.get("/ping/:host", async ctx => {
|
||||||
try {
|
return new Promise(resolve => {
|
||||||
ctx.body = await apiFunctions.ping(ctx.params.host);
|
ping(ctx.params.host, (err, parsed) => {
|
||||||
} catch (err) {
|
if (parsed) {
|
||||||
ctx.status = 404;
|
ctx.body = parsed;
|
||||||
ctx.response.headers["Cache-Control"] = "no-cache";
|
} else {
|
||||||
ctx.body = `${err.name}: ${err.message}`;
|
ctx.status = 500;
|
||||||
}
|
ctx.body = `${err.name}: ${err.message}`;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put("/users/:id/password", async (ctx, next) => {
|
router.put("/users/:id/password", async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const username = ctx.params.id;
|
const username = ctx.params.id;
|
||||||
const log = {
|
const log = {
|
||||||
message: "Change password",
|
message: "Change password",
|
||||||
@@ -535,12 +578,10 @@ router.put("/users/:id/password", async (ctx, next) => {
|
|||||||
username: username
|
username: username
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizer = ctx.state.authorizer;
|
|
||||||
|
|
||||||
if (!ctx.state.user) {
|
if (!ctx.state.user) {
|
||||||
// User not logged in
|
// User not logged in
|
||||||
if (
|
if (
|
||||||
!(await apiFunctions.authSimple(
|
!(await apiFunctions.authLocal(
|
||||||
ctx.state.configSnapshot,
|
ctx.state.configSnapshot,
|
||||||
username,
|
username,
|
||||||
ctx.request.body.authPassword
|
ctx.request.body.authPassword
|
||||||
@@ -556,7 +597,11 @@ router.put("/users/:id/password", async (ctx, next) => {
|
|||||||
return void next();
|
return void next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = ["=", ["PARAM", RESOURCE_IDS.users], username];
|
const filter = and(authorizer.getFilter("users", 3), [
|
||||||
|
"=",
|
||||||
|
["PARAM", RESOURCE_IDS.users],
|
||||||
|
username
|
||||||
|
]);
|
||||||
const res = await db.query("users", filter);
|
const res = await db.query("users", filter);
|
||||||
if (!res.length) return void next();
|
if (!res.length) return void next();
|
||||||
|
|
||||||
|
|||||||
+6
-10
@@ -26,7 +26,7 @@ import { QueryOptions } from "../types";
|
|||||||
|
|
||||||
const CACHE_TTL = 300000;
|
const CACHE_TTL = 300000;
|
||||||
|
|
||||||
let _clientPromise = null;
|
let clientPromise: Promise<MongoClient>;
|
||||||
|
|
||||||
const RESOURCE_COLLECTION = {
|
const RESOURCE_COLLECTION = {
|
||||||
files: "fs.files"
|
files: "fs.files"
|
||||||
@@ -40,8 +40,8 @@ function ensureIndexes(client): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getClient(): Promise<MongoClient> {
|
function getClient(): Promise<MongoClient> {
|
||||||
if (!_clientPromise) {
|
if (!clientPromise) {
|
||||||
_clientPromise = new Promise((resolve, reject) => {
|
clientPromise = new Promise((resolve, reject) => {
|
||||||
const CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
|
const CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
|
||||||
MongoClient.connect(
|
MongoClient.connect(
|
||||||
CONNECTION_URL,
|
CONNECTION_URL,
|
||||||
@@ -55,7 +55,7 @@ function getClient(): Promise<MongoClient> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return _clientPromise;
|
return clientPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cache<T>(key, valueGetter: () => Promise<T>, ttl): Promise<T> {
|
export function cache<T>(key, valueGetter: () => Promise<T>, ttl): Promise<T> {
|
||||||
@@ -405,10 +405,6 @@ export function deleteTask(id): Promise<void> {
|
|||||||
return deleteResource("tasks", id);
|
return deleteResource("tasks", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disconnect(): void {
|
export async function disconnect(): Promise<void> {
|
||||||
if (_clientPromise) {
|
if (clientPromise) await (await clientPromise).close();
|
||||||
_clientPromise.then(client => {
|
|
||||||
client.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Generated
+33
-3
@@ -3670,6 +3670,17 @@
|
|||||||
"chardet": "^0.7.0",
|
"chardet": "^0.7.0",
|
||||||
"iconv-lite": "^0.4.24",
|
"iconv-lite": "^0.4.24",
|
||||||
"tmp": "^0.0.33"
|
"tmp": "^0.0.33"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extglob": {
|
"extglob": {
|
||||||
@@ -4355,9 +4366,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"iconv-lite": {
|
"iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.0.tgz",
|
||||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
"integrity": "sha512-NnEhI9hIEKHOzJ4f697DMz9IQEXr/MMJ5w64vN2/4Ai+wRnvV7SBrL0KLoRlwaKVghOc7LQ5YkPLuX146b6Ydw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3"
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
}
|
}
|
||||||
@@ -5937,6 +5948,15 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"ms": "^2.1.1"
|
"ms": "^2.1.1"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -7664,6 +7684,16 @@
|
|||||||
"http-errors": "1.7.3",
|
"http-errors": "1.7.3",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"requires": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rc": {
|
"rc": {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"tools": "tools"
|
"tools": "tools"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"iconv-lite": "^0.5.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"koa": "^2.7.0",
|
"koa": "^2.7.0",
|
||||||
"koa-bodyparser": "^4.2.1",
|
"koa-bodyparser": "^4.2.1",
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ ava("mongoQueryToFilter", t => {
|
|||||||
[
|
[
|
||||||
{ test: { $gte: "test1", $ne: "test2" } },
|
{ test: { $gte: "test1", $ne: "test2" } },
|
||||||
'test >= "test1" AND test <> "test2"'
|
'test >= "test1" AND test <> "test2"'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ test: "test", test2: { $ne: "test2" } },
|
||||||
|
'test = "test" AND test2 <> "test2"'
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,9 @@ const component: ClosureComponent = (): Component => {
|
|||||||
m(
|
m(
|
||||||
"a.download-csv",
|
"a.download-csv",
|
||||||
{
|
{
|
||||||
href: `/api/devices/${device["DeviceID.ID"].value[0]}.csv`,
|
href: `/api/devices/${encodeURIComponent(
|
||||||
|
device["DeviceID.ID"].value[0]
|
||||||
|
)}.csv`,
|
||||||
download: "",
|
download: "",
|
||||||
style: "float: right;"
|
style: "float: right;"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ const component: ClosureComponent = (): Component => {
|
|||||||
p.type || "parameter",
|
p.type || "parameter",
|
||||||
Object.assign({}, p, {
|
Object.assign({}, p, {
|
||||||
device: device,
|
device: device,
|
||||||
parameter: param
|
parameter: param,
|
||||||
|
label: null
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
+45
-41
@@ -17,58 +17,62 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent, Component, VnodeDOM } from "mithril";
|
||||||
import { m } from "../components";
|
import { m } from "../components";
|
||||||
import * as store from "../store";
|
import * as store from "../store";
|
||||||
|
|
||||||
const REFRESH_INTERVAL = 3000;
|
const REFRESH_INTERVAL = 3000;
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (vn): Component => {
|
||||||
|
let interval: ReturnType<typeof setInterval>;
|
||||||
|
let host: string;
|
||||||
|
|
||||||
|
const refresh = async (): Promise<void> => {
|
||||||
|
let status = "";
|
||||||
|
if (host) {
|
||||||
|
try {
|
||||||
|
const res = await store.ping(host);
|
||||||
|
if (res["avg"] != null) status = `${Math.trunc(res["avg"])} ms`;
|
||||||
|
else status = "Unreachable";
|
||||||
|
} catch (err) {
|
||||||
|
setTimeout(() => {
|
||||||
|
throw err;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dom = (vn as VnodeDOM).dom;
|
||||||
|
|
||||||
|
if (dom) dom.innerHTML = `Pinging ${host}: ${status}`;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
oninit: vnode => {
|
onremove: () => {
|
||||||
vnode.state["timestamp"] = 0;
|
clearInterval(interval);
|
||||||
vnode.state["ping"] = null;
|
|
||||||
vnode.state["host"] = null;
|
|
||||||
vnode.state["timeout"] = null;
|
|
||||||
},
|
},
|
||||||
view: vnode => {
|
view: vnode => {
|
||||||
const refresh = (): void => {
|
const device = vnode.attrs["device"];
|
||||||
const device = vnode.attrs["device"];
|
let param =
|
||||||
let param =
|
device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"];
|
||||||
device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"];
|
if (!param)
|
||||||
if (!param)
|
param = device["Device.ManagementServer.ConnectionRequestURL"];
|
||||||
param = device["Device.ManagementServer.ConnectionRequestURL"];
|
|
||||||
if (!param || !param.value) return;
|
|
||||||
|
|
||||||
|
let h;
|
||||||
|
if (param && param.value) {
|
||||||
const url = new URL(param.value[0]);
|
const url = new URL(param.value[0]);
|
||||||
vnode.state["host"] = url.hostname;
|
h = url.hostname;
|
||||||
vnode.state["timestamp"] = Date.now();
|
|
||||||
store
|
|
||||||
.ping(vnode.state["host"])
|
|
||||||
.then(res => {
|
|
||||||
vnode.state["ping"] = res;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Do nothing
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const t = vnode.state["timestamp"] + REFRESH_INTERVAL - Date.now();
|
|
||||||
clearTimeout(vnode.state["timeout"]);
|
|
||||||
if (t <= 0) refresh();
|
|
||||||
else vnode.state["timeout"] = setTimeout(refresh, t);
|
|
||||||
|
|
||||||
if (vnode.state["host"]) {
|
|
||||||
let status = "";
|
|
||||||
if (vnode.state["ping"]) {
|
|
||||||
if (vnode.state["ping"].avg != null)
|
|
||||||
status = `${Math.trunc(vnode.state["ping"].avg)} ms`;
|
|
||||||
else status = "Unreachable";
|
|
||||||
}
|
|
||||||
|
|
||||||
return m("div", `Pinging ${vnode.state["host"]}: ${status}`);
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
if (host !== h) {
|
||||||
|
host = h;
|
||||||
|
clearInterval(interval);
|
||||||
|
if (host) {
|
||||||
|
refresh();
|
||||||
|
interval = setInterval(refresh, REFRESH_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m("div", `Pinging ${host}:`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -132,6 +132,11 @@ function queryMac(param, value): Expression {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function queryTag(tag: string): Expression {
|
||||||
|
const t = tag.replace(/[^a-zA-Z0-9-]+/g, "_");
|
||||||
|
return ["IS NOT NULL", ["PARAM", `Tags.${t}`]];
|
||||||
|
}
|
||||||
|
|
||||||
export function getTip(resource, label): string {
|
export function getTip(resource, label): string {
|
||||||
let tip;
|
let tip;
|
||||||
if (resources[resource] && resources[resource][label]) {
|
if (resources[resource] && resources[resource][label]) {
|
||||||
@@ -156,6 +161,9 @@ export function getTip(resource, label): string {
|
|||||||
case "mac":
|
case "mac":
|
||||||
tips.push("partial case insensitive MAC address");
|
tips.push("partial case insensitive MAC address");
|
||||||
break;
|
break;
|
||||||
|
case "tag":
|
||||||
|
tips.push("case sensitive string");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +198,11 @@ export function unpack(resource, label, value): Expression {
|
|||||||
if (q) res.push(q);
|
if (q) res.push(q);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type.includes("tag")) {
|
||||||
|
const q = queryTag(value);
|
||||||
|
if (q) res.push(q);
|
||||||
|
}
|
||||||
|
|
||||||
if (res.length <= 1) return null;
|
if (res.length <= 1) return null;
|
||||||
else if (res.length === 2) return res[1];
|
else if (res.length === 2) return res[1];
|
||||||
else return res;
|
else return res;
|
||||||
|
|||||||
+3
-18
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import m from "mithril";
|
import m from "mithril";
|
||||||
import { map, stringify } from "../lib/common/expression-parser";
|
import { stringify } from "../lib/common/expression-parser";
|
||||||
import { or, and, not, evaluate, subset } from "../lib/common/expression";
|
import { or, and, not, evaluate, subset } from "../lib/common/expression";
|
||||||
import memoize from "../lib/common/memoize";
|
import memoize from "../lib/common/memoize";
|
||||||
import { QueryOptions, Expression } from "../lib/types";
|
import { QueryOptions, Expression } from "../lib/types";
|
||||||
@@ -299,22 +299,6 @@ function compareFunction(sort: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findMatches(resourceType, filter, sort, limit): any[] {
|
function findMatches(resourceType, filter, sort, limit): any[] {
|
||||||
// Handle "tag =" and "tag <>" special cases
|
|
||||||
if (resourceType === "devices") {
|
|
||||||
filter = map(filter, e => {
|
|
||||||
if (
|
|
||||||
Array.isArray(e) &&
|
|
||||||
Array.isArray(e[1]) &&
|
|
||||||
e[1][0] === "PARAM" &&
|
|
||||||
e[1][1] === "tag"
|
|
||||||
) {
|
|
||||||
if (e[0] === "=") return ["IS NOT NULL", ["PARAM", `Tags.${e[2]}`]];
|
|
||||||
else if (e[0] === "<>") return ["IS NULL", ["PARAM", `Tags.${e[2]}`]];
|
|
||||||
}
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = [];
|
let value = [];
|
||||||
for (const obj of resources[resourceType].objects.values())
|
for (const obj of resources[resourceType].objects.values())
|
||||||
if (evaluate(filter, obj, fulfillTimestamp)) value.push(obj);
|
if (evaluate(filter, obj, fulfillTimestamp)) value.push(obj);
|
||||||
@@ -709,6 +693,7 @@ export function logOut(): Promise<void> {
|
|||||||
|
|
||||||
export function ping(host): Promise<{}> {
|
export function ping(host): Promise<{}> {
|
||||||
return xhrRequest({
|
return xhrRequest({
|
||||||
url: `/api/ping/${encodeURIComponent(host)}`
|
url: `/api/ping/${encodeURIComponent(host)}`,
|
||||||
|
background: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user