Compare commits

..
Author SHA1 Message Date
Zaid Abdulla 6733cef5dd Include notification and accessList in CSV 2019-12-20 13:35:57 -08:00
Zaid Abdulla 96add7efe2 API support for Get/SetParameterAttributes 2019-12-17 23:01:28 -08:00
Zaid Abdulla d311dad861 Missing arrayType in SetParameterAttributes XML 2019-11-29 10:21:40 -08:00
Zaid Abdulla 4a55f56e48 Fix handling 9005 faults 2019-11-22 16:54:11 -08:00
Zaid Abdulla 601690e3b5 Initial implementation for Get/SetParameterAttributes 2019-11-06 10:37:18 -08:00
Zaid Abdulla 765f0be7bc Fix download task object validation 2019-10-31 16:10:47 -07:00
Zaid Abdulla ab27ebfaef Fix bug in mongoQueryToFilter 2019-10-29 13:01:47 -07:00
Zaid Abdulla f135be403c Tag object should not be writable 2019-10-28 19:10:43 -07:00
Zaid Abdulla c07f57af1f Missed something in previous commit 2019-10-28 19:08:50 -07:00
Zaid Abdulla df5e4cf454 Check against invalid args in default provisions 2019-10-28 18:07:33 -07:00
Zaid Abdulla 85b5e12c94 Use alphanumeric characters for con req password 2019-10-24 17:45:39 -07:00
Zaid Abdulla 8533ee7f2e UI authentication improvements 2019-10-24 17:43:24 -07:00
Zaid Abdulla 77563fc85e Move ping code to own file 2019-10-21 22:58:22 -07:00
Mirek Laus 0e02b7c7ea Fix ping on FreeBSD 2019-10-21 22:58:12 -07:00
Shweelan Samson f966194e22 Update ping component via DOM manipulation 2019-10-19 19:10:27 -07:00
Zaid Abdulla 306015f022 Fix permission filter not being applied 2019-10-11 12:41:35 -07:00
Zaid Abdulla 20857549d0 Fix download URL when SSL is enabled 2019-10-11 11:58:20 -07:00
Zaid Abdulla e31d74bd45 Docs fixes 2019-10-11 00:40:55 -07:00
gothosting 5c6d85025c Missed "/" from line 47 for genieacs-ext path
I omitted the "/" on the initial commit for the SPAWN error on extensions
2019-10-09 14:30:17 -07:00
Zaid Abdulla 209c0f7620 Fix remoteAddress workaround
Tree shaking removed the remoteAddress workaround because property access
should have no side effect. This change prevents that.
2019-09-23 22:12:32 -07:00
Zaid Abdulla 559aa0fd36 Fix filter by tag 2019-09-23 22:02:20 -07:00
Zaid Abdulla ed0aa8ae7a Fix process shutdown issues 2019-09-23 16:03:08 -07:00
Zaid Abdulla 4bcee572f1 Fix URL-encoding of download params hyperlink 2019-09-16 14:17:27 -07:00
Zaid Abdulla 3d38de74d7 Remove label from nested parameter-table 2019-09-14 15:53:44 -07:00
Zaid Abdulla 5b5346f858 Add missing event code in bootstrap preset 2019-09-12 12:34:01 -07:00
Zaid Abdulla f57bcae70c Fix regression in GPN heuristic 2019-09-12 11:16:42 -07:00
Zaid Abdulla 1c9aa493c6 Fix occasional exceptions on SIGINT and SIGTERM 2019-09-09 02:14:12 -07:00
Zaid Abdulla 6f0e1d66ce Remove CWMP_KEEP_ALIVE_TIMEOUT
This turned out not to be necessary. Will use SESSION_TIMEOUT for
connection timeout.
2019-09-05 17:18:04 -07:00
Zaid Abdulla b5efe6c578 Fix session.serialize bug 2019-09-05 17:16:21 -07:00
Shweelan Samson fd34dd8839 Return null in case of expired cache 2019-08-19 15:28:46 -07:00
Zaid Abdulla 17889cf41c Fix typo in config option name 2019-08-15 17:41:28 -07:00
Zaid Abdulla c2e8585801 Docs updates 2019-08-15 17:40:25 -07:00
Zaid Abdulla 46e009afab Docs updates 2019-08-13 23:55:50 -07:00
Gareth Bow 78eec1b5ca Fix spawn bin/genieacs-ext 2019-08-13 09:21:05 -07:00
Zaid Abdulla 59cd930b6c Support more charsets using iconv-lite 2019-08-04 12:40:02 -07:00
Zaid Abdulla db3736e60b Fix exception when logging XML parse errors 2019-08-04 12:33:48 -07:00
Zaid Abdulla d10fa2e073 Few installation guide fixes 2019-07-30 12:13:18 -07:00
43 changed files with 1492 additions and 400 deletions

No files matched your search

+27 -22
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
});
});
} }
+1
View File
@@ -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",
+64
View File
@@ -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.
+9
View File
@@ -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``
~~~~~~~~~~ ~~~~~~~~~~
+211 -1
View File
@@ -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
+1
View File
@@ -30,6 +30,7 @@ Welcome to GenieACS's documentation!
provisions provisions
virtual-parameters virtual-parameters
administration-faq
.. raw:: latex .. raw:: latex
+19 -17
View File
@@ -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
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+28 -3
View File
@@ -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;
}
} }
+2 -2
View File
@@ -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;
-1
View File
@@ -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
View File
@@ -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;
} }
+74 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+9 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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?
]; ];
+36 -24
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
});
}
} }
+33 -3
View File
@@ -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": {
+1
View File
@@ -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",
+4
View File
@@ -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"'
] ]
]; ];
+3 -1
View File
@@ -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;"
}, },
+2 -1
View File
@@ -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
View File
@@ -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}:`);
} }
}; };
}; };
+13
View File
@@ -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
View File
@@ -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
}); });
} }