Compare commits

..
2 Commits
Author SHA1 Message Date
Zaid Abdulla 4b0fc64403 Add more upload file types 2020-12-08 17:21:21 -08:00
Zaid Abdulla dae521ae89 Implement TR-069 Upload method 2020-12-08 17:21:18 -08:00
81 changed files with 3280 additions and 3438 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
!/bin/.*.ts
!/tools/.*.ts
+5 -15
View File
@@ -8,29 +8,19 @@
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },
"plugins": ["@typescript-eslint"], "plugins": ["@typescript-eslint"],
"extends": [ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "prettier/@typescript-eslint"],
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": { "rules": {
"no-shadow": ["error", { "allow": ["err"] }], "no-shadow": ["error", { "allow": ["err"] }],
"handle-callback-err": "error", "handle-callback-err": "error",
"prefer-arrow-callback": "error", "prefer-arrow-callback": "error",
"curly": ["error", "multi-or-nest", "consistent"], "curly": ["error", "multi-or-nest", "consistent"],
"no-buffer-constructor": "error", "no-buffer-constructor": "error",
"prefer-const": ["error", { "destructuring": "all" }], "prefer-const": ["error", {"destructuring": "all"}],
"eqeqeq": ["error", "always", { "null": "ignore" }], "eqeqeq": ["error", "always", {"null": "ignore"}],
"no-return-await": "error", "no-return-await": "error",
"@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-use-before-define": [ "@typescript-eslint/no-use-before-define": ["error", { "functions": false }],
"error", "@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }],
{ "functions": false }
],
"@typescript-eslint/explicit-function-return-type": [
"error",
{ "allowExpressions": true }
],
"@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-floating-promises": "error",
"require-atomic-updates": 0, "require-atomic-updates": 0,
"no-prototype-builtins": 0 "no-prototype-builtins": 0
+2
View File
@@ -1,4 +1,6 @@
*~ *~
node_modules node_modules
dist dist
bin/.*.ts
tools/.*.ts
docs/_build docs/_build
+25
View File
@@ -0,0 +1,25 @@
*~
.npmignore
config/config.json
config/cwmp.key
config/cwmp.crt
config/cwmp.ca-bundle
config/nbi.key
config/nbi.crt
config/nbi.ca-bundle
config/fs.key
config/fs.crt
config/fs.ca-bundle
config/ext
debug/*
!debug/.gitkeep
dist
.eslintrc.json
.eslintignore
ava.config.js
tsconfig.json
bin/.*
tools/.*
-2
View File
@@ -1,2 +0,0 @@
dist
npm-shrinkwrap.json
+72 -108
View File
@@ -1,55 +1,20 @@
# Change Log # Change Log
## 1.2.4 (2021-02-24)
- The data model state of a CPE is no longer forgotten after unsuccessful
session termination (e.g. timeout). This addresses a number of undesired side
effects that arise when a CPE does not terminate the session properly.
- Executing tasks that take a long time to complete (e.g. refreshing the entire
data model) no longer shows a timeout error while the task is still being
processed.
- New function `ROUND()` available to expressions. It works similar to the
function by the same name in SQLite and PostgreSQL.
- Log access events for `genieacs-nbi` service.
- Pipe stdout/stderr from extension scripts to the `genieacs-cwmp` process log.
- Parameter values of type `xsd:dateTime` are now displayed in the UI and CSV
downloads as a date string rather than a numeric value.
- Add file download link in the files listing page.
- Display spinner loading animation throughout the UI.
- Display GenieACS version and build number underneath the logo.
- New option to specify how many parameters are displayed at a time in the
all-parameters component. Simply set `limit` property in the component config.
- Reduce overly strong Brotli compression level which was causing significant
page load slowdown when Brotli is used.
- Retire dump-data-model tool. `genieacs-sim` can now use a CSV file as its data
model.
- Reduce the number of concurrent database connections from each process.
- Remove dependency on 'hostInfo' MongoDB command which is a privileged action.
It is now possible to use shared MongoDB instances with limited privileged.
- Fix bug in NBI where querying files returns 404 error.
- Fix ping not working for devices with an IPv6 address.
- Fix an elusive memory leak in `genieacs-fs` that slowly eats up memory and can
go unnoticed for long periods of time.
- Fix a rare edge case where a `declare()` call to set a parameter value may not
work as intended if the parameter was originally received as part of the
Inform message.
- A number of other fixes and stability improvements.
## 1.2.3 (2020-10-26) ## 1.2.3 (2020-10-26)
- New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly - New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly
report writable parameters as non-writable. When set to true, the scripts will report writable parameters as non-writable. When set to true, the scripts will
no longer respect the 'writable' attribute of the CPE parameters and will send no longer respect the 'writable' attribute of the CPE parameters and will send
a SetParamteerValues, AddObject, or DeleteObject request anyway. a SetParamteerValues, AddObject, or DeleteObject request anyway.
- Tags no longer restrict what characters are allowed. Any character other than - Tags no longer restrict what characters are allowed. Any character other than
alphanumeric characters, hyphen, or underscore is now encoded in the data alphanumeric characters, hyphen, or underscore is now encoded in the data
model (i.e. Tags.\<tag>) using its hex value preceded by "0x". model (i.e. Tags.\<tag>) using its hex value preceded by "0x".
- Ask for a confirmation before closing a pop-up dialog with unsaved changes. - Ask for a confirmation before closing a pop-up dialog with unsaved changes.
- Better XML validation to avoid crashes caused by invalid CPE requests. - Better XML validation to avoid crashes caused by invalid CPE requests.
- Fix confusing 404 error message when the user attempts to modify a resource - Fix confusing 404 error message when the user attempts to modify a resource
when they don't have the necessary permissions. when they don't have the necessary permissions.
- Fix a rare issue where genieacs-cwmp stops accepting new connections after - Fix a rare issue where genieacs-cwmp stops accepting new connections after
running for a few weeks. running for a few weeks.
- Fix exception when IS NULL operator is used in certain situations. - Fix exception when IS NULL operator is used in certain situations.
## 1.2.2 (2020-10-03) ## 1.2.2 (2020-10-03)
@@ -59,80 +24,81 @@
- Fix exception that can happen and persist after a Download request. - Fix exception that can happen and persist after a Download request.
- Fix validation bug preventing running refreshObject task on data model root. - Fix validation bug preventing running refreshObject task on data model root.
- Fix invalid arguments fault in refresh preset configuration when upgrading - Fix invalid arguments fault in refresh preset configuration when upgrading
from v1.1. from v1.1.
## 1.2.1 (2020-09-08) ## 1.2.1 (2020-09-08)
- Fix bug causing faults to not be displayed in the UI. - Fix bug causing faults to not be displayed in the UI.
- Fix bug where deleting objects does not get reflected immediately in the UI. - Fix bug where deleting objects does not get reflected immediately in the UI.
- Improve conversion between filters written in the expression format and - Improve conversion between filters written in the expression format and
MongoDB queries. There should now be fewer edge cases where the two are not MongoDB queries. There should now be fewer edge cases where the two are not
equisatisfiable. equisatisfiable.
## 1.2.0 (2020-09-01) ## 1.2.0 (2020-09-01)
- Support GetParameterAttributes and SetParameterAttributes TR-069 methods. - Support GetParameterAttributes and SetParameterAttributes TR-069 methods.
- Support CASE statement and COALESCE function in expressions. - Support CASE statement and COALESCE function in expressions.
- Provision arguments can now be a list of expressions that are dynamically - Provision arguments can now be a list of expressions that are dynamically
evaluated. evaluated.
- Support Forwarded HTTP header to display in the logs the correct IP of CPEs - Support Forwarded HTTP header to display in the logs the correct IP of CPEs
behind a reverse proxy. Must be configured using FORWARDED_HEADER option. behind a reverse proxy. Must be configured using FORWARDED_HEADER option.
- Config expressions can now access all available device parameters, not only - Config expressions can now access all available device parameters, not only
serial number, product class, and OUI. serial number, product class, and OUI.
- Use relative URLs throughout the UI to allow serving from a subdirectory using - Use relative URLs throughout the UI to allow serving from a subdirectory
a reverse proxy. using a reverse proxy.
- Make Date.parse() and Date.UTC() available to provision scripts. - Make Date.parse() and Date.UTC() available to provision scripts.
- libxmljs has been entirely removed in favor of our bespoke XML parser. - libxmljs has been entirely removed in favor of our bespoke XML parser.
- Removed the config option CWMP_KEEP_ALIVE_TIMEOUT. SESSION_TIMEOUT is now used - Removed the config option CWMP_KEEP_ALIVE_TIMEOUT. SESSION_TIMEOUT is now
to determine the TCP connection timeout. used to determine the TCP connection timeout.
- The all-parameters component now limits the number of parameters displayed for - The all-parameters component now limits the number of parameters displayed
better performance. for better performance.
- The process genieacs-cwmp is now much less likely to throw exceptions as a - The process genieacs-cwmp is now much less likely to throw exceptions as a
result of invalid requests from CPE. result of invalid requests from CPE.
- A large number of bug fixes and stability improvements. - A large number of bug fixes and stability improvements.
## 1.2.0-beta.0 (2019-07-30) ## 1.2.0-beta.0 (2019-07-30)
- A brand new UI superseding genieacs-gui. - A brand new UI superseding genieacs-gui.
- New initialization wizard on first run. - New initialization wizard on first run.
- New expression/query language used in search filters and preset preconditions. - New expression/query language used in search filters and preset
preconditions.
- CPE -> ACS authentication is now supported. - CPE -> ACS authentication is now supported.
- New config option (CWMP_KEEP_ALIVE_TIMEOUT) to specify how long to wait for a - New config option (CWMP_KEEP_ALIVE_TIMEOUT) to specify how long to wait for a
reply from the CPE before closing the TCP connection. reply from the CPE before closing the TCP connection.
- Debug logging has been reimplemented utilizing YAML format for logs. - Debug logging has been reimplemented utilizing YAML format for logs.
- Handle 9005 faults (Invalid Parameter Name) gracefully by attempting to - Handle 9005 faults (Invalid Parameter Name) gracefully by attempting to
rediscover the path of the missing parameter recursively. rediscover the path of the missing parameter recursively.
- declare() statements not followed by an explicit commit() are now deferred - declare() statements not followed by an explicit commit() are now deferred
until all currently active scripts have been executed. until all currently active scripts have been executed.
- FS_HOSTNAME now defaults to the server's hostname or IP. - FS_HOSTNAME now defaults to the server's hostname or IP.
- The API now validates the structure of task objects before saving. - The API now validates the structure of task objects before saving.
- New XML parser implementation for better performance. You can revert to the - New XML parser implementation for better performance. You can revert to the
old parser by enabling the config option XML_LIBXMLJS. Requires Node.js v11 or old parser by enabling the config option XML_LIBXMLJS. Requires Node.js v11 or
v10. v10.
- Performance optimizations. While performance has improved for the majority of - Performance optimizations. While performance has improved for the majority of
use cases, there may be situations where performance has degraded. It's use cases, there may be situations where performance has degraded. It's
recommended to revisit your hardware requirements. recommended to revisit your hardware requirements.
- Connection request authentication no longer uses 'auth.js' file. Instead, the - Connection request authentication no longer uses 'auth.js' file. Instead, the
connection request authentication behavior can now be customized using an connection request authentication behavior can now be customized using an
'expression'. 'expression'.
- The config file (config.json) has been deprecated. System configuration (e.g. - The config file (config.json) has been deprecated. System configuration (e.g.
listen ports, worker count) are now recommended to be passed as environment. listen ports, worker count) are now recommended to be passed as environment.
variables. Other general configuration options are stored in the database so variables. Other general configuration options are stored in the database so as
as to not require service restart for changes to take effect. to not require service restart for changes to take effect.
- Optional redis dependency has been removed completely. - Optional redis dependency has been removed completely.
- Tags now allow only alphanumeric characters and underscore. - Tags now allow only alphanumeric characters and underscore.
- Supported versions of Node.js and MongoDB are 10.x and up and 2.6 and up - Supported versions of NodeJs and MongoDB are 10.x and up and 2.6 and up
respectively. respectively.
## 1.1.3 (2018-10-23) ## 1.1.3 (2018-10-23)
- New config option (MAX_COMMIT_ITERATIONS) to avoid max commit iterations - New config option (MAX_COMMIT_ITERATIONS) to avoid max commit iterations
faults for more complex scripts. faults for more complex scripts.
- Support base64 and hexBinary parameter types. - Support base64 and hexBinary parameter types.
- Strict parsing of number values in queries (e.g. "123abc" no longer accepted - Strict parsing of number values in queries (e.g. "123abc" no longer accepted
as 123). as 123).
- Mixing $ne and $not operators is not allowed. Now it throws an error instead - Mixing $ne and $not operators is not allowed. Now it throws an error instead
of returning incorrect results. of returning incorrect results.
- When a task expires, any associated fault is also deleted. - When a task expires, any associated fault is also deleted.
- API now accepts 'timeout' argument when posting a task. - API now accepts 'timeout' argument when posting a task.
- A number of stability fixes. - A number of stability fixes.
@@ -142,68 +108,66 @@
- A large number of bug fixes as well as stability and performance improvements. - A large number of bug fixes as well as stability and performance improvements.
- Three security vulnerabilities disclosed by Maximilian Hils have been patched. - Three security vulnerabilities disclosed by Maximilian Hils have been patched.
- New config option UDP_CONNECTION_REQUEST_PORT to specify binding port for UDP - New config option UDP_CONNECTION_REQUEST_PORT to specify binding port for UDP
connection requests. connection requests.
- New config option DATETIME_MILLISECONDS to strip milliseconds from dateTime - New config option DATETIME_MILLISECONDS to strip milliseconds from dateTime
values. values.
- New config option BOOLEAN_LITERAL to use 1/0 or true/false for boolean values. - New config option BOOLEAN_LITERAL to use 1/0 or true/false for boolean values.
- Parameter values that cannot be parsed according to the reported type now show - Parameter values that cannot be parsed according to the reported type now show
a warning message. a warning message.
- Virtual parameter scripts now use the variable 'args' instead of the special - Virtual parameter scripts now use the variable 'args' instead of the special
'TIMESTAMPS' and 'VALUES' variables. The content of the args array is: 'TIMESTAMPS' and 'VALUES' variables. The content of the args array is: {declare
{declare timestamps}, {declare values}, {current timestamps}, {current timestamps}, {declare values}, {current timestamps}, {current values}.
values}.
- Virtual parameter value types are now inferred from the JavaScript type if the - Virtual parameter value types are now inferred from the JavaScript type if the
returned value attribute is not a value-type pair. returned value attribute is not a value-type pair.
- Show a fault when a virtual parameter script doesn't return the required - Show a fault when a virtual parameter script doesn't return the required
attributes. attributes.
- Redis is now optional (and disabled by default), reducing the complexity of - Redis is now optional (and disabled by default), reducing the complexity of
scalable deployments. scalable deployments.
- Better detection of cyclical presets resulting in fewer faults for complex - Better detection of cyclical presets resulting in fewer faults for complex
provisioning scripts. provisioning scripts.
- Math.random() is now deterministic on per-device basis. A function has been - Math.random() is now deterministic on per-device basis. A function has been
added to allow specifying a seed value (e.g. Math.random.seed(Date.now())). added to allow specifying a seed value (e.g. Math.random.seed(Date.now())).
- Overload spikes are now handled gracefully by refusing to accept new sessions - Overload spikes are now handled gracefully by refusing to accept new sessions
temporarily when under abnormal load. temporarily when under abnormal load.
- Added log messages for session timeouts, connection drops, and XML parsing - Added log messages for session timeouts, connection drops, and XML parsing
errors. errors.
- Date.now() now takes an optional argument to specify "time steps" (in - Date.now() now takes an optional argument to specify "time steps" (in
milliseconds). This can be used to ensure a group of parameters are all milliseconds). This can be used to ensure a group of parameters are all
refreshed at the same time intervals. refreshed at the same time intervals.
- Only the non-default configuration options are now logged at process start. - Only the non-default configuration options are now logged at process start.
- Faults caused by errors from extensions now show a cleaner stack trace. - Faults caused by errors from extensions now show a cleaner stack trace.
- Exit main process if there are too many worker crashes (e.g. when DB is down). - Exit main process if there are too many worker crashes (e.g. when DB is down).
- Updated dependencies and included a lockfile to ensure installations get the - Updated dependencies and included a lockfile to ensure installations get the
exact dependencies it was tested against. exact dependencies it was tested against.
## 1.1.1 (2017-03-23) ## 1.1.1 (2017-03-23)
- Avoid crashing when connection request credentials are missing. - Avoid crashing when connection request credentials are missing.
- Show a warning instead of crashing when failing to parse parameter values - Show a warning instead of crashing when failing to parse parameter values
according to the expected value type. according to the expected value type.
- Add missing "Registered" event. - Add missing "Registered" event.
- Fix bug where in certain cases many more instances than declared are created. - Fix bug where in certain cases many more instances than declared are created.
- Fix parameter discovery bug when declared path timestamp is 1 or is not set. - Fix parameter discovery bug when declared path timestamp is 1 or is not set.
- Fix preset precondition failing when testing against datetime parameters and - Fix preset precondition failing when testing against datetime parameters and
certain other parameters like \_deviceId.\_ProductClass. certain other parameters like \_deviceId.\_ProductClass.
## 1.1.0 (2017-03-10) ## 1.1.0 (2017-03-10)
- Provisions enable implementing dynamic device configuration or complex device - Provisions enable implementing dynamic device configuration or complex device
provisioning work flow using arbitrary scripts. provisioning work flow using arbitrary scripts.
- Virtual parameters are user-defined parameters whose values are evaluated from - Virtual parameters are user-defined parameters whose values are evaluated from
a custom script. a custom script.
- Extensions are sandboxed Node.js scripts that are accessible from provision - Extensions are sandboxed Node.js scripts that are accessible from provision
and virtual parameter scripts to facilitate integration with external and virtual parameter scripts to facilitate integration with external entities.
entities. - Support for UDP/STUN based connection requests for reaching devices behind
- Support for UDP/STUN based connection requests for reaching devices behind NAT NAT (TR-069 Annex G).
(TR-069 Annex G).
- Presets can now be scheduled using a cron-like expression. - Presets can now be scheduled using a cron-like expression.
- Presets can now be tied to specific device events (e.g. boot). - Presets can now be tied to specific device events (e.g. boot).
- Presets precondition queries no longer support "\$or" or other MongoDB logical - Presets precondition queries no longer support "$or" or other MongoDB logical
operators. operators.
- Faults are no longer a part of tasks but are now first class objects. - Faults are no longer a part of tasks but are now first class objects.
- Presets are now assigned to channels. A fault in one channel only blocks - Presets are now assigned to channels. A fault in one channel only blocks
presets in that channel. presets in that channel.
- New API CRUD functions for provisions, virtual parameters, and faults. - New API CRUD functions for provisions, virtual parameters, and faults.
- New config options for XML output. - New config options for XML output.
- API responses now include "GenieACS-Version" header. - API responses now include "GenieACS-Version" header.
@@ -212,19 +176,19 @@
- Supported Node.js versions are 6.x and 7.x. - Supported Node.js versions are 6.x and 7.x.
- Supported MongoDB versions are 2.6 through 3.4. - Supported MongoDB versions are 2.6 through 3.4.
- Expect performance differences due to major under the hood changes. Some - Expect performance differences due to major under the hood changes. Some
operations are faster and some are slower. Overall performance is improved. operations are faster and some are slower. Overall performance is improved.
- GenieACS will no longer fetch the entire device data model upon first contact - GenieACS will no longer fetch the entire device data model upon first contact
but will instead only fetch the parameters it needs to fulfill the presets. but will instead only fetch the parameters it needs to fulfill the presets.
- Logs have been overhauled and split into two streams: process log (stderr) and - Logs have been overhauled and split into two streams: process log (stderr) and
access log (stdout). Also added config options to dump logs to files rather access log (stdout). Also added config options to dump logs to files rather than
than standard streams. standard streams.
- Connection request authentication credentials are picked up from the device - Connection request authentication credentials are picked up from the device
data model if available. config/auth.js is still supported as a fallback and data model if available. config/auth.js is still supported as a fallback and now
now supports an optional callback argument. supports an optional callback argument.
- Custom commands have been removed. Use virtual parameters and/or extensions. - Custom commands have been removed. Use virtual parameters and/or extensions.
- Aliases and value normalizers (config/parameters.json) have been removed. Use - Aliases and value normalizers (config/parameters.json) have been removed. Use
virtual parameters. virtual parameters.
- The API /devices/<device_id>/preset has been removed. - The API /devices/<device_id>/preset has been removed.
- Rarely used RequestDownload method no longer supported. - Rarely used RequestDownload method no longer supported.
- The TR-069 client simulator has moved to its own repo at - The TR-069 client simulator has moved to its own repo at
https://github.com/zaidka/genieacs-sim https://github.com/zaidka/genieacs-sim
+9 -10
View File
@@ -5,8 +5,8 @@ tracker is strictly for bug reports.
# Issues # Issues
We use GitHub issues to track bugs. Please ensure your bug description is clear We use GitHub issues to track bugs. Please ensure your bug description is
and has sufficient instructions to be able to reproduce the issue. clear and has sufficient instructions to be able to reproduce the issue.
If you face interoperability issues with your CPE, more often than not it's a If you face interoperability issues with your CPE, more often than not it's a
device related issue. Please consult the [forum] before submitting an Issue. device related issue. Please consult the [forum] before submitting an Issue.
@@ -26,14 +26,13 @@ By submitting a pull request to this repository, you acknowledge that, while
maintaining copyright, you grant Zaid Abdulla a perpetual, worldwide, non- maintaining copyright, you grant Zaid Abdulla a perpetual, worldwide, non-
exclusive, no-charge, royalty-free, irrevocable license to reproduce, prepare exclusive, no-charge, royalty-free, irrevocable license to reproduce, prepare
derivative works of, publicly display, publicly perform, sublicense, and derivative works of, publicly display, publicly perform, sublicense, and
distribute your contributions and such derivative works under the AGPLv3 license distribute your contributions and such derivative works under the AGPLv3
or any other license terms, including, but not limited to, proprietary or license or any other license terms, including, but not limited to, proprietary
commercial license terms. or commercial license terms.
You confirm that you own or have rights to distribute and sublicense the source You confirm that you own or have rights to distribute and sublicense the
code contained therein, and that your content does not infringe upon the source code contained therein, and that your content does not infringe upon
intellectual property rights of a third party. the intellectual property rights of a third party.
[commit-guidelines]: [commit-guidelines]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
[forum]: https://forum.genieacs.com [forum]: https://forum.genieacs.com
+9 -8
View File
@@ -20,7 +20,7 @@ Install GenieACS from NPM:
sudo npm install -g --unsafe-perm genieacs sudo npm install -g --unsafe-perm genieacs
To build from source instead, clone this repo or download the source archive To build from source instead, clone this repo or download the source archive
then _cd_ into the source directory then run: then *cd* into the source directory then run:
npm install npm install
npm run build npm run build
@@ -30,8 +30,8 @@ source):
### genieacs-cwmp ### genieacs-cwmp
This is the service that the CPEs will communicate with. It listens on port 7547 This is the service that the CPEs will communicate with. It listens on port
by default. Configure the ACS URL in your devices accordingly. 7547 by default. Configure the ACS URL in your devices accordingly.
You may optionally use [genieacs-sim](https://github.com/genieacs/genieacs-sim) You may optionally use [genieacs-sim](https://github.com/genieacs/genieacs-sim)
as a dummy TR-069 simulator if you don't have a CPE at hand. as a dummy TR-069 simulator if you don't have a CPE at hand.
@@ -50,7 +50,7 @@ such. It listens on port 7567 by default.
### genieacs-ui ### genieacs-ui
This serves the web based user interface. It listens on port 3000 by default. This serves the web based user interface. It listens on port 3000 by default.
You must pass _--ui-jwt-secret_ argument to supply the secret key used for You must pass *--ui-jwt-secret* argument to supply the secret key used for
signing browser cookies: signing browser cookies:
genieacs-ui --ui-jwt-secret secret genieacs-ui --ui-jwt-secret secret
@@ -64,13 +64,14 @@ a complete installation guide for production deployments.
## Support ## Support
The [forum](https://forum.genieacs.com) is a good place to get guidance and help The [forum](https://forum.genieacs.com) is a good place to get guidance and
from the community. Head on over and join the conversation! help from the community. Head on over and join the conversation!
For commercial support options, please visit For commercial support options, please visit
[genieacs.com](https://genieacs.com/support/). [genieacs.com](https://genieacs.com/support/).
## License ## License
Copyright 2013-2020 GenieACS Inc. GenieACS is released under the Copyright 2013-2020 GenieACS Inc. GenieACS is released under the [AGPLv3
[AGPLv3 license terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE). license
terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
+3 -2
View File
@@ -1,4 +1,5 @@
export default { export default {
extensions: ["ts"], "extensions": ["ts"],
require: ["esm", "ts-node/register/transpile-only"], "require": ["esm", "ts-node/register/transpile-only"]
}; };
+3 -1
View File
@@ -94,7 +94,9 @@ if (!cluster.worker) {
const _listener = (req, res): void => { const _listener = (req, res): void => {
if (stopping) res.setHeader("Connection", "close"); if (stopping) res.setHeader("Connection", "close");
void listener(req, res); listener(req, res).catch((err) => {
throw err;
});
}; };
const initPromise = db const initPromise = db
+112 -39
View File
@@ -25,11 +25,12 @@ import typescript from "@rollup/plugin-typescript";
import { terser } from "rollup-plugin-terser"; import { terser } from "rollup-plugin-terser";
import nodeResolve from "@rollup/plugin-node-resolve"; import nodeResolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs"; import commonjs from "@rollup/plugin-commonjs";
import webpack, { Configuration } from "webpack";
import postcss from "postcss"; import postcss from "postcss";
import postcssImport from "postcss-import"; import postcssImport from "postcss-import";
import postcssPresetEnv from "postcss-preset-env"; import postcssPresetEnv from "postcss-preset-env";
import cssnano from "cssnano"; import cssnano from "cssnano";
import { optimize, extendDefaultPlugins } from "svgo"; import SVGO from "svgo";
import * as xmlParser from "../lib/xml-parser"; import * as xmlParser from "../lib/xml-parser";
const MODE = process.env["NODE_ENV"] || "production"; const MODE = process.env["NODE_ENV"] || "production";
@@ -42,7 +43,7 @@ const BUILD_METADATA = new Date()
const INPUT_DIR = path.resolve(__dirname, ".."); const INPUT_DIR = path.resolve(__dirname, "..");
const OUTPUT_DIR = path.resolve(__dirname, "../dist"); const OUTPUT_DIR = path.resolve(__dirname, "../dist");
const builtins = [ const externals = [
"path", "path",
"fs", "fs",
"cluster", "cluster",
@@ -53,13 +54,33 @@ const builtins = [
"zlib", "zlib",
"crypto", "crypto",
"util", "util",
"mongodb",
"vm", "vm",
"later",
"parsimmon",
"seedrandom",
"querystring", "querystring",
"child_process", "child_process",
"dgram", "dgram",
"url", "url",
"readline", "iconv-lite",
"koa",
"koa-router",
"koa-compress",
"koa-bodyparser",
"koa-jwt",
"koa-static",
"jsonwebtoken",
"stream", "stream",
"mithril",
"parsimmon",
"yaml",
"codemirror",
"codemirror/mode/javascript/javascript",
"codemirror/mode/yaml/yaml",
"ipaddr.js",
"jsbi",
"espresso-iisojs",
]; ];
function rmDirSync(dirPath): void { function rmDirSync(dirPath): void {
@@ -109,7 +130,7 @@ function generateSymbol(id: string, svgStr: string): string {
return `<symbol id="icon-${id}" ${viewBox}>${symbolBody}</symbol>`; return `<symbol id="icon-${id}" ${viewBox}>${symbolBody}</symbol>`;
} }
async function init(): Promise<string[]> { async function init(): Promise<void> {
// Delete any old output directory // Delete any old output directory
rmDirSync(OUTPUT_DIR); rmDirSync(OUTPUT_DIR);
@@ -117,13 +138,13 @@ async function init(): Promise<string[]> {
fs.mkdirSync(OUTPUT_DIR); fs.mkdirSync(OUTPUT_DIR);
fs.mkdirSync(OUTPUT_DIR + "/bin"); fs.mkdirSync(OUTPUT_DIR + "/bin");
fs.mkdirSync(OUTPUT_DIR + "/public"); fs.mkdirSync(OUTPUT_DIR + "/public");
fs.mkdirSync(OUTPUT_DIR + "/tools");
// Create package.json // Create package.json
const packageJson = JSON.parse( const packageJson = JSON.parse(
fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString() fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString()
); );
delete packageJson["devDependencies"]; delete packageJson["devDependencies"];
delete packageJson["private"];
packageJson["scripts"] = { packageJson["scripts"] = {
install: packageJson["scripts"].install, install: packageJson["scripts"].install,
configure: packageJson["scripts"].configure, configure: packageJson["scripts"].configure,
@@ -144,7 +165,6 @@ async function init(): Promise<string[]> {
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"), path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
JSON.stringify(npmShrinkwrapJson, null, 2) JSON.stringify(npmShrinkwrapJson, null, 2)
); );
return Object.keys(packageJson["dependencies"]);
} }
async function copyStatic(): Promise<void> { async function copyStatic(): Promise<void> {
@@ -182,7 +202,41 @@ async function generateCss(): Promise<void> {
fs.writeFileSync(cssOutPath, cssOut.css); fs.writeFileSync(cssOutPath, cssOut.css);
} }
async function generateBackendJs(externals: string[]): Promise<void> { async function generateToolsJs(): Promise<void> {
for (const bin of ["dump-data-model"]) {
const inputFile = path.resolve(INPUT_DIR, `tools/${bin}.ts`);
const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`);
const bundle = await rollup({
input: inputFile,
external: externals,
acorn: {
allowHashBang: true,
},
plugins: [
typescript({
tsconfig: "./tsconfig.json",
include: [`tools/${bin}.ts`, "lib/**/*.ts"],
}),
MODE === "production" ? terser() : null,
],
});
await bundle.write({
format: "cjs",
preferConst: true,
sourcemap: "inline",
sourcemapExcludeSources: true,
banner: "#!/usr/bin/env node",
file: outputFile,
});
// Mark as executable
const mode = fs.statSync(outputFile).mode;
fs.chmodSync(outputFile, mode | 73);
}
}
async function generateBackendJs(): Promise<void> {
for (const bin of [ for (const bin of [
"genieacs-cwmp", "genieacs-cwmp",
"genieacs-ext", "genieacs-ext",
@@ -194,7 +248,7 @@ async function generateBackendJs(externals: string[]): Promise<void> {
const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`); const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`);
const bundle = await rollup({ const bundle = await rollup({
input: inputFile, input: inputFile,
external: [...builtins, ...externals], external: externals,
acorn: { acorn: {
allowHashBang: true, allowHashBang: true,
}, },
@@ -238,26 +292,26 @@ async function generateBackendJs(externals: string[]): Promise<void> {
} }
} }
async function generateFrontendJs(externals: string[]): Promise<void> { async function generateFrontendJs(): Promise<void> {
const inputFile = path.resolve(INPUT_DIR, "ui/app.ts"); const inputFile = path.resolve(INPUT_DIR, "ui/app.ts");
const outputDir = path.resolve(OUTPUT_DIR, "public"); const outputFile = path.resolve(OUTPUT_DIR, "public/app.js");
const inlineDeps = ["parsimmon", "espresso-iisojs"]; const inlineDeps = ["mithril", "parsimmon", "jsbi", "espresso-iisojs"];
const bundle = await rollup({ const bundle = await rollup({
input: inputFile, input: inputFile,
external: [ external: externals.filter((e) => !inlineDeps.includes(e)),
...builtins,
...externals.filter((e) => !inlineDeps.includes(e)),
],
plugins: [ plugins: [
rollupJson({ preferConst: true }), rollupJson({ preferConst: true }),
{ {
name: "", name: "",
resolveId: function (importee, importer) { resolveId: function (importee, importer) {
if (importee.endsWith("/package.json")) { if (importee.endsWith("/bigint")) {
const p = path.resolve(path.dirname(importer), importee); return this.resolve(importee + "-jsbi", importer);
if (p === path.resolve(INPUT_DIR, "package.json")) } else if (importee === "espresso-iisojs") {
return path.resolve(OUTPUT_DIR, "package.json"); return this.resolve(
"espresso-iisojs/dist/espresso-iisojs-jsbi.mjs",
importer
);
} }
return null; return null;
}, },
@@ -265,9 +319,8 @@ async function generateFrontendJs(externals: string[]): Promise<void> {
typescript({ tsconfig: "./tsconfig.json" }), typescript({ tsconfig: "./tsconfig.json" }),
nodeResolve(), nodeResolve(),
commonjs(), commonjs(),
MODE === "production" ? terser() : null,
], ],
preserveEntrySignatures: false, inlineDynamicImports: true,
treeshake: { treeshake: {
propertyReadSideEffects: false, propertyReadSideEffects: false,
moduleSideEffects: false, moduleSideEffects: false,
@@ -279,33 +332,52 @@ async function generateFrontendJs(externals: string[]): Promise<void> {
}); });
await bundle.write({ await bundle.write({
manualChunks: (id) => {
if (id.includes("node_modules/codemirror")) return "codemirror";
else if (id.includes("node_modules/yaml")) return "yaml";
return "app";
},
preferConst: true, preferConst: true,
format: "es", format: "es",
sourcemap: true, sourcemap: "inline",
sourcemapExcludeSources: true, sourcemapExcludeSources: true,
dir: outputDir, file: outputFile,
});
const webpackConf: Configuration = {
mode: MODE as "development" | "production",
entry: outputFile,
resolve: {
aliasFields: ["module"],
},
devtool: "nosources-source-map",
module: {
rules: [
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre",
},
],
},
output: {
path: path.resolve(OUTPUT_DIR, "public"),
filename: "app.js",
},
};
return new Promise((resolve, reject) => {
webpack(webpackConf, (err, stats) => {
if (err) return reject(err);
process.stdout.write(stats.toString({ colors: true }) + "\n");
resolve();
});
}); });
} }
async function generateIconsSprite(): Promise<void> { async function generateIconsSprite(): Promise<void> {
const svgo = new SVGO({ plugins: [{ removeViewBox: false }] });
const symbols = []; const symbols = [];
const iconsDir = path.resolve(INPUT_DIR, "ui/icons"); const iconsDir = path.resolve(INPUT_DIR, "ui/icons");
for (const file of fs.readdirSync(iconsDir)) { for (const file of fs.readdirSync(iconsDir)) {
const id = path.parse(file).name; const id = path.parse(file).name;
const filePath = path.join(iconsDir, file); const filePath = path.join(iconsDir, file);
const { data } = await optimize(fs.readFileSync(filePath).toString(), { const { data } = await svgo.optimize(fs.readFileSync(filePath).toString());
plugins: extendDefaultPlugins([
{
name: "removeViewBox",
active: false,
},
]),
});
symbols.push(generateSymbol(id, data)); symbols.push(generateSymbol(id, data));
} }
fs.writeFileSync( fs.writeFileSync(
@@ -315,13 +387,14 @@ async function generateIconsSprite(): Promise<void> {
} }
init() init()
.then((externals) => { .then(() => {
Promise.all([ Promise.all([
copyStatic(), copyStatic(),
generateCss(), generateCss(),
generateIconsSprite(), generateIconsSprite(),
generateBackendJs(externals), generateToolsJs(),
generateFrontendJs(externals), generateBackendJs(),
generateFrontendJs(),
]) ])
.then(() => { .then(() => {
// Ignore // Ignore
-167
View File
@@ -1,167 +0,0 @@
personal_ws-1.1 en 0
genieacs
GenieACS
javascript
sudo
config
cwmp
CPE
systemctl
nbi
CWMP
NBI
ACS
SSL
xsd
args
DeviceID
GENIEACS
env
param
arg
AUTH
igd
TCP
toctree
auth
EnvironmentFile
JSON
json
parameterValues
setParameterValues
stderr
stdout
attr
cfg
CPEs
ExecStart
faq
FileType
GenieACS's
JWT
maxdepth
npm
objectName
refreshObject
SerialNumber
systemd
usr
WantedBy
yaml
chown
Config
getParameterValues
https
InternetGatewayDevice
mkdir
OUI
sql
TODO
UDP
addObject
AGPLv
const
cpe
cron
deleteObject
FactoryReset
factoryReset
FileName
fileType
Github
hostname
HTTPS
jwt
latlong
NPM
oui
parameterNames
ProductClass
productClass
repo
SSID
sublicense
TLS
WANIPConnection
WPA
YourProvisionName
abc
Abdulla
accessors
AddObject
AddressingType
api
APIs
chmod
cwmp's
dateext
DATETIME
dateTime
datetime
declaratively
delaycompress
DeleteObject
deviceId
DHCP
dir
distro
equisatisfiable
ExternalIPAddress
failover
favor
func
GetParameterAttributes
getPassword
gte
gui
hexBinary
Hils
HOSTNAME
init
journalctl
journald
LastFileName
LastFileType
lastInform
LIBXMLJS
libxmljs
literalinclude
lockfile
logrotate
lte
mipsbe
normalizers
pre
quickstart
README
redis
reimplemented
RequestDownload
RESTful
rollout
sandboxed
scalable
SetParameterAttributes
SetParamteerValues
SIGINT
SIGTERM
skipWritableCheck
stringify
subdirectory
TargetFileName
useradd
VirtualParameters
vparam
xml
YAML
Zaid
cacheExpire
iss
http
statusCode
rawData
pos
Brotli
CSV
hostInfo
IPv
-12
View File
@@ -1,12 +0,0 @@
#! /bin/sh
cd "$(dirname "$0")"
FILES=`ls ../docs/*.rst ../docs/*.js ../*.md`
for FILE in $FILES
do
echo $FILE
cat $FILE | aspell list --lang=en --add-extra-dicts=./spellcheck-dict.pws --ignore 2
echo
done
+16 -16
View File
@@ -1,7 +1,7 @@
API Reference API Reference
============= =============
GenieACS exposes a rich RESTful API through its NBI component. This document GenieACS expoeses a rich RESTful API through its NBI component. This document
serves as a reference for the available APIs. serves as a reference for the available APIs.
This API makes use of MongoDB's query language in some of its endpoints. Refer This API makes use of MongoDB's query language in some of its endpoints. Refer
@@ -16,7 +16,7 @@ to MongoDB's documentation for details.
.. warning:: .. warning::
A common pitfall is not properly percent-encoding special characters in the A common pitfll is not properly percent-encoding special characters in the
device ID or query in the URL. device ID or query in the URL.
Endpoints Endpoints
@@ -41,11 +41,11 @@ Examples
.. code:: javascript .. code:: javascript
query = {"_id": "202BC1-BM632w-000000"} query = {"_id": "202BC1-BM632w-0000000"}
.. code:: bash .. code:: bash
curl -i 'http://localhost:7557/devices/?query=%7B%22_id%22%3A%22202BC1-BM632w-000000%22%7D' curl -i 'http://localhost:7557/devices/?query=%7B%22_id%22%3A%22202BC1-BM632w-0000000%22%7D'
- Find a device by its MAC address: - Find a device by its MAC address:
@@ -77,21 +77,21 @@ Examples
.. code:: javascript .. code:: javascript
query = {"device": "202BC1-BM632w-000000"} query = {"device": "202BC1-BM632w-0000000"}
.. code:: bash .. code:: bash
curl -i 'http://localhost:7557/tasks/?query=%7B%22device%22%3A%22202BC1-BM632w-000000%22%7D' curl -i 'http://localhost:7557/tasks/?query=%7B%22device%22%3A%22202BC1-BM632w-0000000%22%7D'
- Return specific parameters for a given device: - Return specific parameters for a given device:
.. code:: javascript .. code:: javascript
query = {"_id": "202BC1-BM632w-000000"} query = {"_id": "202BC1-BM632w-0000000"}
.. code:: bash .. code:: bash
curl -i 'http://localhost:7557/devices?query=%7B%22_id%22%3A%22202BC1-BM632w-000000%22%7D&projection=InternetGatewayDevice.DeviceInfo.ModelName,InternetGatewayDevice.DeviceInfo.Manufacturer' curl -i 'http://localhost:7557/devices?query=%7B%22_id%22%3A%22202BC1-BM632w-0000000%22%7D&projection=InternetGatewayDevice.DeviceInfo.ModelName,InternetGatewayDevice.DeviceInfo.Manufacturer'
The ``projection`` URL param is a comma-separated list of the parameters to receive. The ``projection`` URL param is a comma-separated list of the parameters to receive.
@@ -106,7 +106,7 @@ if the tasks have been queued to be executed at the next inform.
*device_id*: The ID of the device. *device_id*: The ID of the device.
*connection_request*: Indicates that a connection request will be triggered to *connection_request*: Indicates that a connection request will be triggered to
execute the tasks immediately. Otherwise, the tasks will be queued and be execute the tasks immediatly. Otherwise, the tasks will be queued and be
processed at the next inform. processed at the next inform.
The response body is the task object as it is inserted in the database. The The response body is the task object as it is inserted in the database. The
@@ -120,7 +120,7 @@ Examples
.. code:: bash .. code:: bash
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/tasks?connection_request' \ curl -i 'http://localhost:7557/devices/202BC1-BM632w-0000000/tasks?connection_request' \
-X POST \ -X POST \
--data '{"name": "refreshObject", "objectName": ""}' --data '{"name": "refreshObject", "objectName": ""}'
@@ -138,7 +138,7 @@ Examples
.. code:: bash .. code:: bash
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/tasks?connection_request' \ curl -i 'http://localhost:7557/devices/202BC1-BM632w-0000000/tasks?connection_request' \
-X POST \ -X POST \
--data '{"name":"setParameterValues", "parameterValues": [["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", "GenieACS", "xsd:string"],["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey", "hello world", "xsd:string"]]}' --data '{"name":"setParameterValues", "parameterValues": [["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", "GenieACS", "xsd:string"],["InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.PreSharedKey", "hello world", "xsd:string"]]}'
@@ -183,7 +183,7 @@ Example
.. code:: bash .. code:: bash
curl -i 'http://localhost:7557/faults/202BC1-BM632w-000000:default' -X DELETE curl -i 'http://localhost:7557/faults/202BC1-BM632w-0000000:default' -X DELETE
DELETE /devices/\<device_id\> DELETE /devices/\<device_id\>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -218,7 +218,7 @@ Assign the tag "testing" to a device:
.. code:: bash .. code:: bash
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/tags/testing' -X POST curl -i 'http://localhost:7557/devices/202BC1-BM632w-0000000/tags/testing' -X POST
DELETE /devices/\<device_id\>/tags/\<tag\> DELETE /devices/\<device_id\>/tags/\<tag\>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -236,7 +236,7 @@ Remove the tag "testing" from a device:
.. code:: bash .. code:: bash
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/tags/testing' -X DELETE curl -i 'http://localhost:7557/devices/202BC1-BM632w-0000000/tags/testing' -X DELETE
PUT /presets/\<preset_name\> PUT /presets/\<preset_name\>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -299,7 +299,7 @@ The following file metadata may be sent as request headers:
- ``fileType``: For firmware images it should be "1 Firmware Upgrade Image". - ``fileType``: For firmware images it should be "1 Firmware Upgrade Image".
Other common types are "2 Web Content" and "3 Vendor Configuration File". Other common types are "2 Web Content" and "3 Vendor Configuration File".
- ``oui``: The OUI of the device model that this file belongs to. - ``oui``: The OUI of the device model that this file belogs to.
- ``productClass``: The product class of the device. - ``productClass``: The product class of the device.
@@ -344,7 +344,7 @@ Find files using a query.
Tasks Tasks
----- -----
Find the different available tasks and their object structure. Find the different availabe tasks and their object structure.
``getParameterValues`` ``getParameterValues``
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
-66
View File
@@ -3,70 +3,4 @@
CPE Authentication CPE Authentication
================== ==================
CPE to ACS
----------
.. note::
By default GenieACS will accept any incoming connection via HTTP/HTTPS and
respond to it.
The following parameters are used to set and get (password is redacted but
can be set) the username/password used to authenticate against the ACS:
Username: ``Device.ManagementServer.Username`` or ``InternetGatewayDevice.ManagementServer.Username``
Password: ``Device.ManagementServer.Password`` or ``InternetGatewayDevice.ManagementServer.Password``
Enable CPE to ACS Authentication
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CPE to ACS authentication can be configured in the web interface by using the
`Config` option in the `Admin` tab.
Go to the `Admin` -> `Config` page and click on `New config` button at the
bottom of the page. This will open pop-up which requires you to fill in a key
and value. The key should be ``cwmp.auth``. The value accepts a boolean.
Setting the value to ``true`` makes it so that GenieACS accepts any incoming
connection, setting it to ``false`` makes GenieACS deny all incoming
connections. This can be further configured using the ``AUTH()`` and ``EXT()``
functions.
The ``AUTH()`` function
~~~~~~~~~~~~~~~~~~~~~~~
The ``AUTH()`` function accepts two parameters, username and password. It
checks the given username and password with the incoming request to determine
whether to return true or false.
Basic usage of the ``AUTH()`` function could be as follows:
.. code:: sql
AUTH("fixed-username", "fixed-password")
This will only accept incoming request who authenticate with
"fixed-username" and "fixed-password".
The various device parameters can be referenced from within the ``cwmp.auth``
expression. For example:
.. code:: sql
AUTH(Device.ManagementServer.Username, Device.ManagementServer.Password)
The ``EXT()`` function
~~~~~~~~~~~~~~~~~~~~~~
The ``EXT()`` function makes it possible to call an :ref:`extension
<extensions>` script from the auth expression. This can be used to fetch
the credentials from an external source:
.. code:: sql
AUTH(DeviceID.SerialNumber, EXT("authenticate", "getPassword", DeviceID.SerialNumber))
ACS to CPE
----------
TODO TODO
+2 -2
View File
@@ -86,7 +86,7 @@ CWMP_ACCESS_LOG_FILE
File to log incoming requests for genieacs-cwmp. If omitted, logs will go to File to log incoming requests for genieacs-cwmp. If omitted, logs will go to
stdout. stdout.
Default: unset Default: unest
NBI_WORKER_PROCESSES NBI_WORKER_PROCESSES
The number of worker processes to spawn for genieacs-nbi. A value of 0 means The number of worker processes to spawn for genieacs-nbi. A value of 0 means
@@ -107,7 +107,7 @@ NBI_INTERFACE
NBI_SSL_CERT NBI_SSL_CERT
Path to certificate file. If omitted, non-secure HTTP will be used. Path to certificate file. If omitted, non-secure HTTP will be used.
Default: unset Default: unest
NBI_SSL_KEY NBI_SSL_KEY
Path to certificate key file. If omitted, non-secure HTTP will be used. Path to certificate key file. If omitted, non-secure HTTP will be used.
+16 -19
View File
@@ -1,4 +1,4 @@
// This is an example GenieACS extension to get the current latitude/longitude // This is an exmaple GenieACS extension to get the current latitude/longitude
// of the International Space Station. Why, you ask? Because why not. // of the International Space Station. Why, you ask? Because why not.
// To install, copy this file to config/ext/iss.js. // To install, copy this file to config/ext/iss.js.
@@ -10,28 +10,25 @@ let cache = null;
let cacheExpire = 0; let cacheExpire = 0;
function latlong(args, callback) { function latlong(args, callback) {
if (Date.now() < cacheExpire) return callback(null, cache); if (Date.now() < cacheExpire)
return callback(null, cache);
http http.get("http://api.open-notify.org/iss-now.json", (res) => {
.get("http://api.open-notify.org/iss-now.json", (res) => { if (res.statusCode !== 200)
if (res.statusCode !== 200) return callback(new Error(`Request failed (status code: ${res.statusCode})`));
return callback(
new Error(`Request failed (status code: ${res.statusCode})`)
);
let rawData = ""; let rawData = "";
res.on("data", (chunk) => (rawData += chunk)); res.on("data", (chunk) => rawData += chunk);
res.on("end", () => { res.on("end", () => {
let pos = JSON.parse(rawData)["iss_position"]; let pos = JSON.parse(rawData)["iss_position"];
cache = [+pos["latitude"], +pos["longitude"]]; cache = [+pos["latitude"], +pos["longitude"]];
cacheExpire = Date.now() + 10000; cacheExpire = Date.now() + 10000;
callback(null, cache); callback(null, cache);
});
})
.on("error", (err) => {
callback(err);
}); });
}).on("error", (err) => {
callback(err);
});
} }
exports.latlong = latlong; exports.latlong = latlong;
+1 -2
View File
@@ -4,7 +4,7 @@ Installation Guide
This guide is for installing GenieACS on a single server on any Linux distro This guide is for installing GenieACS on a single server on any Linux distro
that uses *systemd* as its init system. that uses *systemd* as its init system.
The various GenieACS services are independent of each other and may be The various GenieACS services are independant of each other and may be
installed on different servers. You may also run multiple instances of each in installed on different servers. You may also run multiple instances of each in
a load-balancing/failover setup. a load-balancing/failover setup.
@@ -73,7 +73,6 @@ Configure systemd
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
NODE_OPTIONS=--enable-source-maps
GENIEACS_EXT_DIR=/opt/genieacs/ext GENIEACS_EXT_DIR=/opt/genieacs/ext
GENIEACS_UI_JWT_SECRET=secret GENIEACS_UI_JWT_SECRET=secret
+1 -9
View File
@@ -296,15 +296,6 @@ function sanitizeTask(task): void {
) )
throw new Error("Invalid 'provisions' property"); throw new Error("Invalid 'provisions' property");
break; break;
case "reboot":
break;
case "factoryReset":
break;
default:
throw new Error("Invalid task name");
} }
return task; return task;
@@ -342,5 +333,6 @@ export async function deleteDevice(deviceId: string): Promise<void> {
}, },
}), }),
cache.del(`${deviceId}_tasks_faults_operations`), cache.del(`${deviceId}_tasks_faults_operations`),
db.deleteDeviceUploads(deviceId),
]); ]);
} }
+108
View File
@@ -0,0 +1,108 @@
/**
* Copyright 2013-2020 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 JSBI from "jsbi";
export function and(a: JSBI, b: JSBI): JSBI {
return JSBI.bitwiseAnd(a, b);
}
export function or(a: JSBI, b: JSBI): JSBI {
return JSBI.bitwiseOr(a, b);
}
export function xor(a: JSBI, b: JSBI): JSBI {
return JSBI.bitwiseXor(a, b);
}
export function not(a: JSBI): JSBI {
return JSBI.bitwiseNot(a);
}
export function lshift(a: JSBI, b: JSBI): JSBI {
return JSBI.leftShift(a, b);
}
export function rshift(a: JSBI, b: JSBI): JSBI {
return JSBI.signedRightShift(a, b);
}
export function add(a: JSBI, b: JSBI): JSBI {
return JSBI.add(a, b);
}
export function sub(a: JSBI, b: JSBI): JSBI {
return JSBI.subtract(a, b);
}
export function mul(a: JSBI, b: JSBI): JSBI {
return JSBI.multiply(a, b);
}
export function div(a: JSBI, b: JSBI): JSBI {
return JSBI.divide(a, b);
}
export function exp(a: JSBI, b: JSBI): JSBI {
return JSBI.exponentiate(a, b);
}
export function rem(a: JSBI, b: JSBI): JSBI {
return JSBI.remainder(a, b);
}
export function toNumber(a: JSBI): number {
return JSBI.toNumber(a);
}
export function eq(a: JSBI, b: JSBI): boolean {
return JSBI.equal(a, b);
}
export function ne(a: JSBI, b: JSBI): boolean {
return JSBI.notEqual(a, b);
}
export function lt(a: JSBI, b: JSBI): boolean {
return JSBI.lessThan(a, b);
}
export function lte(a: JSBI, b: JSBI): boolean {
return JSBI.lessThanOrEqual(a, b);
}
export function gt(a: JSBI, b: JSBI): boolean {
return JSBI.greaterThan(a, b);
}
export function gte(a: JSBI, b: JSBI): boolean {
return JSBI.greaterThanOrEqual(a, b);
}
export function asUintN(a: number, b: JSBI): JSBI {
return JSBI.asUintN(a, b);
}
export function asIntN(a: number, b: JSBI): JSBI {
return JSBI.asIntN(a, b);
}
const _BigInt = JSBI.BigInt;
type _bigint = JSBI;
export { _BigInt as BigInt, _bigint as bigint };
-9
View File
@@ -130,15 +130,6 @@ export function evaluateCallback(exp: Expression): Expression {
} else if (exp[1] === "LOWER") { } else if (exp[1] === "LOWER") {
if (exp[2] == null) return null; if (exp[2] == null) return null;
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase(); if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
} else if (exp[1] === "ROUND") {
const n = exp[2];
const p = exp.length > 3 ? exp[3] : 0;
if (n == null || p == null) return null;
if (!isArray(n) && !isArray(p)) {
const d = 10 ** p;
const m = n * d * (1 + Number.EPSILON);
return Math.round(m) / d;
}
} }
} else if (exp[0] === "PARAM") { } else if (exp[0] === "PARAM") {
if (exp[1] == null) return null; if (exp[1] == null) return null;
+3 -12
View File
@@ -47,18 +47,9 @@ export default function memoize<T extends (...args: any[]) => any>(func: T): T {
if (cache1.has(key)) return cache1.get(key); if (cache1.has(key)) return cache1.get(key);
let r; let r;
if (cache2.has(key)) { if (cache2.has(key)) r = cache2.get(key);
cache1.set(key, (r = cache2.get(key))); else r = func(...args);
} else { cache1.set(key, r);
cache1.set(key, (r = func(...args)));
// Evict rejected promises
if (r instanceof Promise) {
r.catch(() => {
cache1.delete(key);
cache2.delete(key);
});
}
}
return r; return r;
}) as any; }) as any;
} }
-203
View File
@@ -1,203 +0,0 @@
const LINE_WIDTH = 80;
const INDENTATION = " ";
const STRING_RESERVED = new Set([
"true",
"True",
"TRUE",
"false",
"False",
"FALSE",
"null",
"Null",
"NULL",
]);
function isPrintable(str: string): boolean {
return !/[^\t\n\x20-\x7e\x85\u{a0}-\u{d7ff}\u{e000}-\u{fffd}\u{10000}-\u{10ffff}]/u.test(
str
);
}
function stringifyKey(str: string): string {
if (!str || !isPrintable(str)) return JSON.stringify(str);
if (/^[\s-?:,[\]{}#&$!|>'"%@`]|: | #|[\n,[\]{}]|\s$/.test(str))
return JSON.stringify(str);
return str;
}
function foldString(str: string): string[] {
if (str.length <= LINE_WIDTH) return [str];
if (str.startsWith(" ")) return [str];
const words = str.split(/(?<=[^ ]) (?=[^ ])/);
const lines: string[] = [];
let idx = 0;
let len = 0;
for (const [i, word] of words.entries()) {
len += word.length + 1;
if (len >= LINE_WIDTH && len !== word.length) {
lines.push(words.slice(idx, i).join(" "));
idx = i;
len = 0;
}
}
if (idx < words.length) lines.push(words.slice(idx).join(" "));
return lines;
}
function stringifyString(str: string, res: string[], prefix1, prefix2): void {
if (/^\s*$/.test(str) || STRING_RESERVED.has(str) || !isPrintable(str)) {
res.push(prefix1 + JSON.stringify(str));
return;
}
if (!prefix2) prefix2 = INDENTATION;
const lines = str.split("\n");
if (lines.length > 1) {
let idt = "";
let chmp = "-";
if ((lines.find((l) => l) || "").startsWith(" "))
idt = `${INDENTATION.length}`;
if (!lines[lines.length - 1]) {
lines.pop();
if (lines[lines.length - 1]) chmp = "";
else chmp = "+";
}
if (/^\s+$/.test(lines[lines.length - 1])) {
res.push(prefix1 + JSON.stringify(str));
return;
}
let isFolded = false;
const folded = lines.map((l) => {
const ls = foldString(l);
if (ls.length > 1) isFolded = true;
return ls;
});
if (!isFolded) {
res.push(
`${prefix1}|${idt}${chmp}`,
...lines.map((l) => (l ? prefix2 + l : l))
);
return;
}
res.push(`${prefix1}>${idt}${chmp}`);
res.push(...folded[0].map((f) => prefix2 + f));
for (let i = 1; i < folded.length; ++i) {
const prevLine = folded[i - 1][0];
if (prevLine && !folded[i - 1][0].startsWith(" ")) res.push("");
res.push(...folded[i].map((f) => prefix2 + f));
}
return;
}
if (
/^[\s-?:,[\]{}#&$!|>'"%@`]|: | #|\s$/.test(str) ||
parseFloat(str) === +str
) {
res.push(prefix1 + JSON.stringify(str));
return;
}
res.push(prefix1 + str);
}
function stringifyAny(
obj: unknown,
res: string[],
prefix1 = "",
prefix2 = ""
): void {
if (obj == null) {
res.push(`${prefix1}null`);
return;
}
if (typeof obj === "number" || typeof obj === "boolean") {
res.push(`${prefix1}${JSON.stringify(obj)}`);
return;
}
if (obj instanceof Date) {
res.push(`${prefix1}${obj.toJSON()}`);
return;
}
if (typeof obj === "string") {
stringifyString(obj, res, prefix1, prefix2);
return;
}
if (Array.isArray(obj)) {
if (!obj.length) {
res.push(prefix1 + "[]");
return;
}
if (!prefix1 || prefix1.endsWith("- ")) {
stringifyAny(obj[0], res, prefix1 + "- ", prefix2 + INDENTATION);
prefix1 = prefix2 + "- ";
prefix2 = prefix2 + INDENTATION;
for (let i = 1; i < obj.length; ++i)
stringifyAny(obj[i], res, prefix1, prefix2);
} else {
res.push(prefix1);
prefix1 = prefix2 + "- ";
prefix2 = prefix2 + INDENTATION;
for (let i = 0; i < obj.length; ++i)
stringifyAny(obj[i], res, prefix1, prefix2);
}
return;
}
const entries = Object.entries(obj).filter((e) => e[1] !== undefined);
if (!entries.length) {
res.push(prefix1 + "{}");
return;
}
if (!prefix1 || prefix1.endsWith("- ")) {
stringifyAny(
entries[0][1],
res,
prefix1 + `${stringifyKey(entries[0][0])}: `,
prefix2 + INDENTATION
);
prefix1 = prefix2;
prefix2 = prefix2 + INDENTATION;
for (let i = 1; i < entries.length; ++i) {
stringifyAny(
entries[i][1],
res,
prefix1 + `${stringifyKey(entries[i][0])}: `,
prefix2
);
}
} else {
res.push(prefix1);
prefix1 = prefix2;
prefix2 = prefix2 + INDENTATION;
for (let i = 0; i < entries.length; ++i) {
stringifyAny(
entries[i][1],
res,
prefix1 + `${stringifyKey(entries[i][0])}: `,
prefix2
);
}
}
}
export function stringify(obj: unknown): string {
if (obj === undefined) return undefined;
const lines: string[] = [];
stringifyAny(obj, lines);
return lines.join("\n") + "\n";
}
+1 -1
View File
@@ -221,7 +221,7 @@ export async function udpConnectionRequest(
const message = Buffer.from(msg); const message = Buffer.from(msg);
for (let i = 0; i < 3; ++i) { for (let i = 0; i < 3; ++i) {
await new Promise<void>((resolve, reject) => { await new Promise((resolve, reject) => {
client.send(message, 0, message.length, port, host, (err: Error) => { client.send(message, 0, message.length, port, host, (err: Error) => {
if (err) reject(err); if (err) reject(err);
else resolve(); else resolve();
+27 -33
View File
@@ -79,7 +79,7 @@ async function authenticate(
sessionContext.cacheSnapshot, sessionContext.cacheSnapshot,
"cwmp.auth" "cwmp.auth"
); );
if (authExpression == null) return true; if (!authExpression) return true;
let authentication; let authentication;
@@ -208,7 +208,7 @@ async function writeResponse(
sessionContext.extendLock = now + 10000; sessionContext.extendLock = now + 10000;
await cache.acquireLock( await cache.acquireLock(
`cwmp_session_${sessionContext.deviceId}`, `cwmp_session_${sessionContext.deviceId}`,
sessionContext.timeout + 15000, sessionContext.timeout + 10000,
0, 0,
sessionContext.sessionId sessionContext.sessionId
); );
@@ -705,6 +705,11 @@ async function nextRpc(sessionContext: SessionContext): Promise<void> {
["download", task.fileType, task.fileName, task.targetFileName || ""], ["download", task.fileType, task.fileName, task.targetFileName || ""],
]); ]);
break; break;
case "upload":
session.addProvisions(sessionContext, `task_${task._id}`, [
["upload", task.fileType, task.fileName],
]);
break;
case "addObject": case "addObject":
alias = (task.parameterValues || []) alias = (task.parameterValues || [])
.map((p) => `${p[0]}:${JSON.stringify(p[1])}`) .map((p) => `${p[0]}:${JSON.stringify(p[1])}`)
@@ -726,15 +731,7 @@ async function nextRpc(sessionContext: SessionContext): Promise<void> {
); );
break; break;
default: default:
if (!sessionContext.doneTasks) sessionContext.doneTasks = []; throw new Error("Task name not recognized");
sessionContext.doneTasks.push(task._id);
sessionContext.tasks = sessionContext.tasks.filter((t) => t !== task);
logger.accessWarn({
sessionContext: sessionContext,
message: "Invalid task",
taskId: task._id,
});
} }
return nextRpc(sessionContext); return nextRpc(sessionContext);
@@ -858,6 +855,21 @@ async function sendAcsRequest(
} }
} }
if (acsRequest.name === "Upload") {
let prefix = "" + config.get("FS_URL_PREFIX");
if (!prefix) {
const FS_PORT = +config.get("FS_PORT");
const ssl = !!config.get("FS_SSL_CERT");
const origin = getRequestOrigin(sessionContext.httpRequest);
let hostname = origin.localAddress;
if (origin.host) [hostname] = origin.host.split(":", 1);
prefix = (ssl ? "https" : "http") + `://${hostname}:${FS_PORT}/`;
}
acsRequest.url = prefix + encodeURI(acsRequest.fileName);
}
const rpc = { const rpc = {
id: id, id: id,
acsRequest: acsRequest, acsRequest: acsRequest,
@@ -909,14 +921,6 @@ export function onConnection(socket: Socket): void {
const sessionContext = currentSessions.get(socket); const sessionContext = currentSessions.get(socket);
if (!sessionContext) return; if (!sessionContext) return;
currentSessions.delete(socket); currentSessions.delete(socket);
if (sessionContext.authState !== 2) {
logger.accessError({
message: "Authentication failure",
sessionContext: sessionContext,
});
return;
}
const now = Date.now(); const now = Date.now();
const lastActivity = sessionContext.lastActivity; const lastActivity = sessionContext.lastActivity;
@@ -1146,7 +1150,7 @@ async function processRequest(
sessionContext.extendLock = sessionContext.timestamp + 10000; sessionContext.extendLock = sessionContext.timestamp + 10000;
await cache.acquireLock( await cache.acquireLock(
`cwmp_session_${sessionContext.deviceId}`, `cwmp_session_${sessionContext.deviceId}`,
sessionContext.timeout + 15000, sessionContext.timeout + 10000,
0, 0,
sessionContext.sessionId sessionContext.sessionId
); );
@@ -1244,15 +1248,7 @@ async function processRequest(
} else if (rpc.cpeResponse) { } else if (rpc.cpeResponse) {
if (sessionContext.state !== 2) return reportBadState(sessionContext); if (sessionContext.state !== 2) return reportBadState(sessionContext);
const fault = await session.rpcResponse( await session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse);
sessionContext,
rpc.id,
rpc.cpeResponse
);
if (fault) {
recordFault(sessionContext, fault);
session.clearProvisions(sessionContext);
}
return nextRpc(sessionContext); return nextRpc(sessionContext);
} else if (rpc.cpeFault) { } else if (rpc.cpeFault) {
if (sessionContext.state !== 2) return reportBadState(sessionContext); if (sessionContext.state !== 2) return reportBadState(sessionContext);
@@ -1594,10 +1590,8 @@ async function listenerAsync(
return e; return e;
} }
); );
if (d) { if (d)
debug.incomingHttpRequest(httpRequest, null, bodyStr); debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, _body);
debug.outgoingHttpResponse(httpResponse, null, _body);
}
httpResponse.end(_body); httpResponse.end(_body);
return; return;
} }
+35 -18
View File
@@ -17,7 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { MongoClient, ObjectID, Collection, Db } from "mongodb"; import { MongoClient, ObjectID, Collection, Db, GridFSBucket } from "mongodb";
import { get } from "./config"; import { get } from "./config";
import { encodeTag, escapeRegExp } from "./common"; import { encodeTag, escapeRegExp } from "./common";
import { parse } from "./common/expression-parser"; import { parse } from "./common/expression-parser";
@@ -39,10 +39,12 @@ export let tasksCollection: Collection,
virtualParametersCollection: Collection, virtualParametersCollection: Collection,
faultsCollection: Collection, faultsCollection: Collection,
filesCollection: Collection, filesCollection: Collection,
uploadsCollection: Collection,
operationsCollection: Collection, operationsCollection: Collection,
permissionsCollection: Collection, permissionsCollection: Collection,
usersCollection: Collection, usersCollection: Collection,
configCollection: Collection; configCollection: Collection,
uploadsBucket: GridFSBucket;
let clientPromise: Promise<MongoClient>; let clientPromise: Promise<MongoClient>;
@@ -77,6 +79,7 @@ onConnect(async (db) => {
presetsCollection = db.collection("presets"); presetsCollection = db.collection("presets");
objectsCollection = db.collection("objects"); objectsCollection = db.collection("objects");
filesCollection = db.collection("fs.files"); filesCollection = db.collection("fs.files");
uploadsCollection = db.collection("uploads.files");
provisionsCollection = db.collection("provisions"); provisionsCollection = db.collection("provisions");
virtualParametersCollection = db.collection("virtualParameters"); virtualParametersCollection = db.collection("virtualParameters");
faultsCollection = db.collection("faults"); faultsCollection = db.collection("faults");
@@ -84,6 +87,7 @@ onConnect(async (db) => {
permissionsCollection = db.collection("permissions"); permissionsCollection = db.collection("permissions");
usersCollection = db.collection("users"); usersCollection = db.collection("users");
configCollection = db.collection("config"); configCollection = db.collection("config");
uploadsBucket = new GridFSBucket(db, { bucketName: "uploads" });
}); });
export async function disconnect(): Promise<void> { export async function disconnect(): Promise<void> {
@@ -131,12 +135,7 @@ export async function fetchDevice(
const device = await devicesCollection.findOne({ _id: id }); const device = await devicesCollection.findOne({ _id: id });
if (!device) return null; if (!device) return null;
function storeParams( function storeParams(obj, path: string, pathLength: number, ts): void {
obj,
path: string,
pathLength: number,
ts: number
): void {
if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"]; if (obj["_timestamp"]) obj["_timestamp"] = +obj["_timestamp"];
if (obj["_attributesTimestamp"]) if (obj["_attributesTimestamp"])
obj["_attributesTimestamp"] = +obj["_attributesTimestamp"]; obj["_attributesTimestamp"] = +obj["_attributesTimestamp"];
@@ -167,22 +166,19 @@ export async function fetchDevice(
if (obj["_accessList"] != null) if (obj["_accessList"] != null)
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]]; attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
res.push([Path.parse(path), t, attrs]); res.push([Path.parse(path.slice(0, -1)), t, attrs]);
for (const [k, v] of Object.entries(obj)) { for (const [k, v] of Object.entries(obj)) {
if (!k.startsWith("_")) { if (!k.startsWith("_")) {
obj["_object"] = true; obj["_object"] = true;
storeParams(v, `${path}.${k}`, pathLength + 1, obj["_timestamp"]); storeParams(v, path + k + ".", pathLength + 1, obj["_timestamp"]);
} }
} }
if (obj["_object"] && obj["_timestamp"]) if ((obj["_object"] || !pathLength) && obj["_timestamp"])
res.push([Path.parse(path + ".*"), obj["_timestamp"]]); res.push([Path.parse(path + "*"), obj["_timestamp"]]);
} }
const ts: number = device["_timestamp"] || 0;
if (ts) res.push([Path.parse("*"), ts]);
for (const [k, v] of Object.entries(device)) { for (const [k, v] of Object.entries(device)) {
switch (k) { switch (k) {
case "_lastInform": case "_lastInform":
@@ -310,11 +306,10 @@ export async function fetchDevice(
}, },
]); ]);
} }
break;
default:
if (!k.startsWith("_")) storeParams(v, k, 1, ts);
} }
} }
storeParams(device, "", 0, 0);
return res; return res;
} }
@@ -448,6 +443,17 @@ export async function saveDevice(
break; break;
default: default:
if (
diff[0].segments[0] === "Uploads" &&
diff[0].segments[2] === "LastFileName" &&
value1 &&
value1 !== value2
) {
uploadsBucket.delete(value1 as any, () => {
// Ignore error due to mising files
});
}
if (!diff[2]) { if (!diff[2]) {
update["$unset"][path.toString()] = 1; update["$unset"][path.toString()] = 1;
continue; continue;
@@ -797,3 +803,14 @@ interface User {
export async function getUsers(): Promise<User[]> { export async function getUsers(): Promise<User[]> {
return usersCollection.find().toArray(); return usersCollection.find().toArray();
} }
export async function deleteDeviceUploads(deviceId: string): Promise<void> {
const files = await uploadsCollection
.find({
_id: {
$regex: `^${escapeRegExp(deviceId)}\\/`,
},
})
.toArray();
await Promise.all(files.map((f) => uploadsBucket.delete(f["_id"])));
}
+7 -7
View File
@@ -25,7 +25,7 @@ import {
} from "http"; } from "http";
import { Socket } from "net"; import { Socket } from "net";
import { appendFileSync } from "fs"; import { appendFileSync } from "fs";
import { stringify } from "./common/yaml"; import * as yaml from "yaml";
import * as config from "./config"; import * as config from "./config";
const DEBUG_FILE = "" + config.get("DEBUG_FILE"); const DEBUG_FILE = "" + config.get("DEBUG_FILE");
@@ -64,7 +64,7 @@ export function incomingHttpRequest(
}; };
if (DEBUG_FORMAT === "yaml") if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
else if (DEBUG_FORMAT === "json") else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`); else throw new Error(`Unrecognized DEBUG_FORMAT option`);
@@ -90,7 +90,7 @@ export function outgoingHttpResponse(
}; };
if (DEBUG_FORMAT === "yaml") if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
else if (DEBUG_FORMAT === "json") else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`); else throw new Error(`Unrecognized DEBUG_FORMAT option`);
@@ -119,7 +119,7 @@ export function outgoingHttpRequest(
}; };
if (DEBUG_FORMAT === "yaml") if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
else if (DEBUG_FORMAT === "json") else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`); else throw new Error(`Unrecognized DEBUG_FORMAT option`);
@@ -147,7 +147,7 @@ export function outgoingHttpRequestError(
}; };
if (DEBUG_FORMAT === "yaml") if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
else if (DEBUG_FORMAT === "json") else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`); else throw new Error(`Unrecognized DEBUG_FORMAT option`);
@@ -173,7 +173,7 @@ export function incomingHttpResponse(
}; };
if (DEBUG_FORMAT === "yaml") if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
else if (DEBUG_FORMAT === "json") else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`); else throw new Error(`Unrecognized DEBUG_FORMAT option`);
@@ -197,7 +197,7 @@ export function outgoingUdpMessage(
}; };
if (DEBUG_FORMAT === "yaml") if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg)); appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
else if (DEBUG_FORMAT === "json") else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n"); appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`); else throw new Error(`Unrecognized DEBUG_FORMAT option`);
+38
View File
@@ -229,6 +229,44 @@ export function download(
return true; return true;
} }
export function upload(
sessionContext: SessionContext,
provision: (string | number | boolean)[],
declarations: Declaration[]
): boolean {
if (
provision.length !== 3 ||
typeof provision[1] !== "string" ||
typeof provision[2] !== "string"
)
throw new Error("Invalid arguments");
const alias = [
`FileType:${JSON.stringify(provision[1] || "")}`,
`FileName:${JSON.stringify(provision[2] || "")}`,
].join(",");
declarations.push({
path: Path.parse(`Uploads.[${alias}]`),
pathGet: 1,
pathSet: 1,
attrGet: null,
attrSet: null,
defer: true,
});
declarations.push({
path: Path.parse(`Uploads.[${alias}].Upload`),
pathGet: 1,
pathSet: null,
attrGet: { value: 1 },
attrSet: { value: [sessionContext.timestamp] },
defer: true,
});
return true;
}
export function instances( export function instances(
sessionContext: SessionContext, sessionContext: SessionContext,
provision: (string | number | boolean)[], provision: (string | number | boolean)[],
+1 -13
View File
@@ -22,8 +22,6 @@ 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"; import { ROOT_DIR } from "./config";
import * as logger from "./logger";
import readline from "readline";
const TIMEOUT = +config.get("EXT_TIMEOUT"); const TIMEOUT = +config.get("EXT_TIMEOUT");
@@ -47,7 +45,7 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
if (!processes[scriptName]) { if (!processes[scriptName]) {
const p = spawn(ROOT_DIR + "/bin/genieacs-ext", [scriptName], { const p = spawn(ROOT_DIR + "/bin/genieacs-ext", [scriptName], {
stdio: ["ignore", "pipe", "pipe", "ipc"], stdio: ["inherit", "inherit", "inherit", "ipc"],
}); });
processes[scriptName] = p; processes[scriptName] = p;
@@ -71,16 +69,6 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
}); });
p.on("message", messageHandler); p.on("message", messageHandler);
const rlstdout = readline.createInterface(p.stdout);
rlstdout.on("line", (line) => {
logger.info({ message: `Ext ${scriptName}(${p.pid}): ${line}` });
});
const rlstderr = readline.createInterface(p.stderr);
rlstderr.on("line", (line) => {
logger.warn({ message: `Ext ${scriptName}(${p.pid}): ${line}` });
});
} }
setTimeout(() => { setTimeout(() => {
+82 -51
View File
@@ -20,75 +20,106 @@
import * as url from "url"; import * as url from "url";
import * as querystring from "querystring"; import * as querystring from "querystring";
import { IncomingMessage, ServerResponse } from "http"; import { IncomingMessage, ServerResponse } from "http";
import { Collection, GridFSBucket } from "mongodb"; import { Collection, GridFSBucket, ObjectId } from "mongodb";
import { onConnect } from "./db"; import { onConnect, getOperations } from "./db";
import * as logger from "./logger"; import * as logger from "./logger";
import { getRequestOrigin } from "./forwarded"; import { getRequestOrigin } from "./forwarded";
import memoize from "./common/memoize";
let filesCollection: Collection; let filesCollection: Collection;
let filesBucket: GridFSBucket; let filesBucket: GridFSBucket;
let uploadsBucket: GridFSBucket;
onConnect(async (db) => { onConnect(async (db) => {
filesCollection = db.collection("fs.files"); filesCollection = db.collection("fs.files");
filesBucket = new GridFSBucket(db); filesBucket = new GridFSBucket(db);
uploadsBucket = new GridFSBucket(db, { bucketName: "uploads" });
}); });
const getFile = memoize( async function canUpload(
(md5: string, size: number, filename: string): Promise<Buffer> => { deviceId: string,
return new Promise((resolve, reject) => { fileName: string,
const buffer = Buffer.allocUnsafe(size); timeout = Date.now() + 5000
let i = 0; ): Promise<boolean> {
const downloadStream = filesBucket.openDownloadStreamByName(filename); const operations = Object.values(await getOperations(deviceId));
downloadStream.on("error", reject); for (const operation of operations) {
downloadStream.on("data", (data: Buffer) => { if (operation.name === "Upload" && operation.args.fileName === fileName)
data.copy(buffer, i); return true;
i += data.length;
});
downloadStream.on("end", () => {
if (i !== size) reject(new Error("File size mismatch"));
else resolve(buffer);
});
});
} }
);
if (Date.now() >= timeout) return false;
// Need to wait and retry in case upload was initiated before session was closed
await new Promise((resolve) => setTimeout(resolve, 1000));
return canUpload(deviceId, fileName, timeout);
}
export async function listener( export async function listener(
request: IncomingMessage, request: IncomingMessage,
response: ServerResponse response: ServerResponse
): Promise<void> { ): Promise<void> {
if (request.method !== "GET") { const urlParts = url.parse(request.url, true);
if (request.method === "GET") {
const filename = querystring.unescape(urlParts.pathname.substring(1));
const log = {
message: "Fetch file",
filename: filename,
remoteAddress: getRequestOrigin(request).remoteAddress,
};
const file = await filesCollection.findOne({ _id: filename });
if (!file) {
response.writeHead(404);
response.end();
log.message += " not found";
logger.accessError(log);
return;
}
response.writeHead(200, {
"Content-Type": file.contentType || "application/octet-stream",
"Content-Length": file.length,
});
const downloadStream = filesBucket.openDownloadStreamByName(filename);
downloadStream.pipe(response);
logger.accessInfo(log);
} else if (request.method === "PUT") {
const [, deviceId, ...filePath] = urlParts.pathname
.split("/")
.map(querystring.unescape);
const fileName = `${deviceId}/${filePath.join("/")}`;
if (!(await canUpload(deviceId, fileName))) {
response.writeHead(403);
response.end("403 Forbidden");
return;
}
try {
await uploadsBucket.delete((fileName as unknown) as ObjectId);
} catch (err) {
// File not found, do nothing
}
const uploadStream = uploadsBucket.openUploadStreamWithId(
fileName,
fileName
);
uploadStream.on("finish", () => {
response.writeHead(200);
response.end();
});
uploadStream.on("error", (err) => {
response.writeHead(500);
response.end(err.message);
});
request.pipe(uploadStream);
} else {
response.writeHead(405, { Allow: "GET" }); response.writeHead(405, { Allow: "GET" });
response.end("405 Method Not Allowed"); response.end("405 Method Not Allowed");
return;
} }
const urlParts = url.parse(request.url, true);
const filename = querystring.unescape(urlParts.pathname.substring(1));
const log = {
message: "Fetch file",
filename: filename,
remoteAddress: getRequestOrigin(request).remoteAddress,
};
const file = await filesCollection.findOne({ _id: filename });
if (!file) {
response.writeHead(404);
response.end();
log.message += " not found";
logger.accessError(log);
return;
}
const buffer = await getFile(file.md5, file.length, filename);
response.writeHead(200, {
"Content-Type": file.contentType || "application/octet-stream",
"Content-Length": file.length,
});
response.end(buffer);
logger.accessInfo(log);
} }
+1
View File
@@ -108,6 +108,7 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
{ role: "admin", resource: "devices", access: 3, validate: "true" }, { role: "admin", resource: "devices", access: 3, validate: "true" },
{ role: "admin", resource: "faults", access: 3, validate: "true" }, { role: "admin", resource: "faults", access: 3, validate: "true" },
{ role: "admin", resource: "files", access: 3, validate: "true" }, { role: "admin", resource: "files", access: 3, validate: "true" },
{ role: "admin", resource: "uploads", access: 3, validate: "true" },
{ role: "admin", resource: "presets", access: 3, validate: "true" }, { role: "admin", resource: "presets", access: 3, validate: "true" },
{ role: "admin", resource: "provisions", access: 3, validate: "true" }, { role: "admin", resource: "provisions", access: 3, validate: "true" },
{ role: "admin", resource: "config", access: 3, validate: "true" }, { role: "admin", resource: "config", access: 3, validate: "true" },
+3 -3
View File
@@ -50,8 +50,8 @@ interface Snapshot {
ui: UiConfig; ui: UiConfig;
} }
const REFRESH = 5000; const REFRESH = 3000;
const EVICT_TIMEOUT = 120000; const EVICT_TIMEOUT = 60000;
const snapshots = new Map<string, Snapshot>(); const snapshots = new Map<string, Snapshot>();
let currentSnapshot: string = null; let currentSnapshot: string = null;
@@ -398,7 +398,7 @@ async function refresh(): Promise<void> {
return; return;
} }
const lockToken = await cache.acquireLock("presets_hash_lock", 5000, 5000); const lockToken = await cache.acquireLock("presets_hash_lock", 4000, 4000);
const res = await Promise.all([ const res = await Promise.all([
fetchPresets(), fetchPresets(),
+8
View File
@@ -640,6 +640,14 @@ export function flattenFile(
return f; return f;
} }
export function flattenUpload(
file: Record<string, unknown>
): Record<string, unknown> {
const f = {};
f["_id"] = file["_id"];
return f;
}
export function preProcessPreset( export function preProcessPreset(
data: Record<string, unknown> data: Record<string, unknown>
): Record<string, unknown> { ): Record<string, unknown> {
+2 -14
View File
@@ -29,7 +29,6 @@ import { IncomingMessage, ServerResponse } from "http";
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"; import { ping } from "./ping";
import * as logger from "./logger";
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_]*)?$/;
@@ -49,7 +48,7 @@ const collections = {
devices: null as Collection, devices: null as Collection,
presets: null as Collection, presets: null as Collection,
objects: null as Collection, objects: null as Collection,
files: null as Collection, "fs.files": null as Collection,
provisions: null as Collection, provisions: null as Collection,
virtualParameters: null as Collection, virtualParameters: null as Collection,
faults: null as Collection, faults: null as Collection,
@@ -57,10 +56,7 @@ const collections = {
let filesBucket: GridFSBucket; let filesBucket: GridFSBucket;
onConnect(async (db) => { onConnect(async (db) => {
for (const k of Object.keys(collections)) { for (const k of Object.keys(collections)) collections[k] = db.collection(k);
if (k === "files") collections[k] = db.collection("fs.files");
else collections[k] = db.collection(k);
}
filesBucket = new GridFSBucket(db); filesBucket = new GridFSBucket(db);
}); });
@@ -99,14 +95,6 @@ export function listener(
request.addListener("end", () => { request.addListener("end", () => {
const body = getBody(); const body = getBody();
const urlParts = url.parse(request.url, true); const urlParts = url.parse(request.url, true);
logger.accessInfo(
Object.assign({}, urlParts.query, {
remoteAddress: request.connection.remoteAddress,
message: `${request.method} ${urlParts.pathname}`,
})
);
if (PRESETS_REGEX.test(urlParts.pathname)) { if (PRESETS_REGEX.test(urlParts.pathname)) {
const presetName = querystring.unescape( const presetName = querystring.unescape(
PRESETS_REGEX.exec(urlParts.pathname)[1] PRESETS_REGEX.exec(urlParts.pathname)[1]
-1
View File
@@ -35,7 +35,6 @@ export function ping(
callback: (err: Error, res?: PingResult, stdout?: string) => void callback: (err: Error, res?: PingResult, stdout?: string) => void
): void { ): void {
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp; let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
host = host.replace("[", "").replace("]", "");
switch (platform()) { switch (platform()) {
case "linux": case "linux":
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`; cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
+473 -68
View File
@@ -53,6 +53,7 @@ import {
AddObject, AddObject,
DeleteObject, DeleteObject,
Download, Download,
Upload,
Reboot, Reboot,
FactoryReset, FactoryReset,
AddObjectResponse, AddObjectResponse,
@@ -292,14 +293,15 @@ export async function transferComplete(
}; };
} }
const instance = operation.args.instance;
delete sessionContext.operations[commandKey]; delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {}; if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1; sessionContext.operationsTouched[commandKey] = 1;
if (rpcReq.faultStruct && rpcReq.faultStruct.faultCode !== "0") { if (rpcReq.faultStruct && rpcReq.faultStruct.faultCode !== "0") {
revertDownloadParameters(sessionContext, operation.args.instance); if (operation.name === "Download")
revertDownloadParameters(sessionContext, operation.args.instance);
else if (operation.name === "Upload")
revertUploadParameters(sessionContext, operation.args.instance);
const fault: Fault = { const fault: Fault = {
code: `cwmp.${rpcReq.faultStruct.faultCode}`, code: `cwmp.${rpcReq.faultStruct.faultCode}`,
@@ -318,6 +320,44 @@ export async function transferComplete(
let toClear = null; let toClear = null;
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
if (operation.name === "Download") {
toClear = commitDownloadOperation(
sessionContext,
operation,
rpcReq,
timestamp,
toClear
);
} else if (operation.name === "Upload") {
toClear = commitUploadOperation(
sessionContext,
operation,
rpcReq,
timestamp,
toClear
);
}
if (toClear) {
for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
}
return {
acsResponse: { name: "TransferCompleteResponse" },
operation: operation,
fault: null,
};
}
function commitDownloadOperation(
sessionContext: SessionContext,
operation: Operation,
rpcReq: TransferCompleteRequest,
timestamp: number,
toClear: Clear[]
): Clear[] {
const instance = operation.args.instance;
let p; let p;
p = sessionContext.deviceData.paths.add( p = sessionContext.deviceData.paths.add(
@@ -386,16 +426,110 @@ export async function transferComplete(
toClear toClear
); );
return toClear;
}
function commitUploadOperation(
sessionContext: SessionContext,
operation: Operation,
rpcReq: TransferCompleteRequest,
timestamp: number,
toClear: Clear[]
): Clear[] {
const instance = operation.args.instance;
let p;
p = sessionContext.deviceData.paths.add(
Path.parse(`Uploads.${instance}.LastUpload`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.timestamp, "xsd:dateTime"]] },
toClear
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Uploads.${instance}.LastFileType`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.args.fileType, "xsd:string"]] },
toClear
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Uploads.${instance}.LastFileName`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [operation.args.fileName, "xsd:string"]] },
toClear
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Uploads.${instance}.StartTime`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [+rpcReq.startTime, "xsd:dateTime"]] },
toClear
);
p = sessionContext.deviceData.paths.add(
Path.parse(`Uploads.${instance}.CompleteTime`)
);
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, [+rpcReq.completeTime, "xsd:dateTime"]] },
toClear
);
return toClear;
}
function revertUploadParameters(
sessionContext: SessionContext,
instance
): void {
const timestamp = sessionContext.timestamp + sessionContext.iteration + 1;
let p;
p = sessionContext.deviceData.paths.add(
Path.parse(`Uploads.${instance}.LastUpload`)
);
const lastUpload = sessionContext.deviceData.attributes.get(p);
p = sessionContext.deviceData.paths.add(
Path.parse(`Uploads.${instance}.Upload`)
);
const toClear = device.set(sessionContext.deviceData, p, timestamp, {
value: [
timestamp,
[
lastUpload && lastUpload.value[1] ? lastUpload.value[1][0] : 0,
"xsd:dateTime",
],
],
});
if (toClear) { if (toClear) {
for (const c of toClear) for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]); device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
} }
return {
acsResponse: { name: "TransferCompleteResponse" },
operation: operation,
fault: null,
};
} }
function revertDownloadParameters( function revertDownloadParameters(
@@ -445,33 +579,62 @@ export async function timeoutOperations(
for (const [commandKey, operation] of Object.entries( for (const [commandKey, operation] of Object.entries(
sessionContext.operations sessionContext.operations
)) { )) {
if (operation.name !== "Download") if (operation.name === "Download") {
const DOWNLOAD_TIMEOUT =
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.downloadTimeout",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
) * 1000;
if (sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT) {
delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
faults.push({
code: "timeout",
message: "Download operation timed out",
timestamp: operation.timestamp,
});
operations.push(operation);
revertDownloadParameters(sessionContext, operation.args.instance);
}
} else if (operation.name === "Upload") {
const UPLOAD_TIMEOUT =
+(
localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.uploadTimeout",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
) || 3600
) * 1000;
if (sessionContext.timestamp > operation.timestamp + UPLOAD_TIMEOUT) {
delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
faults.push({
code: "timeout",
message: "Upload operation timed out",
timestamp: operation.timestamp,
});
operations.push(operation);
revertUploadParameters(sessionContext, operation.args.instance);
}
} else {
throw new Error(`Unknown operation name ${operation.name}`); throw new Error(`Unknown operation name ${operation.name}`);
const DOWNLOAD_TIMEOUT =
+localCache.getConfig(
sessionContext.cacheSnapshot,
"cwmp.downloadTimeout",
{},
sessionContext.timestamp,
(e) => configContextCallback(sessionContext, e)
) * 1000;
if (sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT) {
delete sessionContext.operations[commandKey];
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[commandKey] = 1;
faults.push({
code: "timeout",
message: "Download operation timed out",
timestamp: operation.timestamp,
});
operations.push(operation);
revertDownloadParameters(sessionContext, operation.args.instance);
} }
} }
@@ -793,6 +956,10 @@ function runDeclarations(
downloadsToCreate: new InstanceSet(), downloadsToCreate: new InstanceSet(),
downloadsValues: new Map(), downloadsValues: new Map(),
downloadsDownload: new Map(), downloadsDownload: new Map(),
uploadsToDelete: new Set(),
uploadsToCreate: new InstanceSet(),
uploadsValues: new Map(),
uploadsUpload: new Map(),
reboot: 0, reboot: 0,
factoryReset: 0, factoryReset: 0,
}; };
@@ -910,15 +1077,10 @@ function runDeclarations(
if (!unpacked) if (!unpacked)
unpacked = device.unpack(sessionContext.deviceData, path); unpacked = device.unpack(sessionContext.deviceData, path);
for (const u of unpacked) { for (const u of unpacked)
mergeAttributeValues(u, declaration.attrSet, declaration.defer); mergeAttributeValues(u, declaration.attrSet, declaration.defer);
// Ensure writable attr is available
mergeAttributeTimestamps(u, { writable: 1 });
}
} else { } else {
mergeAttributeValues(path, declaration.attrSet, declaration.defer); mergeAttributeValues(path, declaration.attrSet, declaration.defer);
// Ensure writable attr is available
mergeAttributeTimestamps(path, { writable: 1 });
} }
} }
@@ -942,15 +1104,14 @@ function runDeclarations(
keys = {}; keys = {};
} }
if (!parent.wildcard && !parent.alias) { if (
((path.wildcard | path.alias) & ((1 << (path.length - 1)) - 1)) ===
0
) {
parent = sessionContext.deviceData.paths.add(parent); parent = sessionContext.deviceData.paths.add(parent);
if (!unpacked) if (!unpacked)
unpacked = device.unpack(sessionContext.deviceData, path); unpacked = device.unpack(sessionContext.deviceData, path);
// Ensure writable attr is available
mergeAttributeTimestamps(parent, { writable: 1 });
for (const u of unpacked) mergeAttributeTimestamps(u, { writable: 1 });
processInstances( processInstances(
sessionContext, sessionContext,
parent, parent,
@@ -966,19 +1127,13 @@ function runDeclarations(
parent parent
); );
for (const par of parentsUnpacked) { for (const par of parentsUnpacked) {
const up = device.unpack(
sessionContext.deviceData,
par.concat(path.slice(-1))
);
// Ensure writable attr is available
mergeAttributeTimestamps(par, { writable: 1 });
for (const u of up) mergeAttributeTimestamps(u, { writable: 1 });
processInstances( processInstances(
sessionContext, sessionContext,
par, par,
up, device.unpack(
sessionContext.deviceData,
par.concat(path.slice(-1))
),
keys, keys,
minInstances, minInstances,
maxInstances, maxInstances,
@@ -1316,7 +1471,7 @@ export async function rpcRequest(
} }
// Downloads // Downloads
let index; let index: number;
for (const instance of sessionContext.syncState.downloadsToCreate) { for (const instance of sessionContext.syncState.downloadsToCreate) {
if (index == null) { if (index == null) {
index = 0; index = 0;
@@ -1438,6 +1593,9 @@ export async function rpcRequest(
} }
} }
// Uploads
toClear = syncUploads(sessionContext, timestamp, toClear);
if (toClear || sessionContext.deviceData.changes.has("prerequisite")) { if (toClear || sessionContext.deviceData.changes.has("prerequisite")) {
if (toClear) { if (toClear) {
for (const c of toClear) for (const c of toClear)
@@ -1515,6 +1673,129 @@ export async function rpcRequest(
return rpcRequest(sessionContext, null); return rpcRequest(sessionContext, null);
} }
function syncUploads(
sessionContext: SessionContext,
timestamp: number,
toClear: Clear[]
): Clear[] {
let index: number;
for (const instance of sessionContext.syncState.uploadsToCreate) {
if (index == null) {
index = 0;
for (const p of sessionContext.deviceData.paths.find(
Path.parse("Uploads.*"),
false,
true
)) {
if (
+p.segments[1] > index &&
sessionContext.deviceData.attributes.has(p)
)
index = +p.segments[1];
}
}
++index;
toClear = device.set(
sessionContext.deviceData,
Path.parse("Uploads"),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Uploads.${index}`),
timestamp,
{ object: [timestamp, 1], writable: [timestamp, 1] },
toClear
);
const params = {
FileType: {
writable: 1,
value: [instance.FileType || "", "xsd:string"],
},
FileName: {
writable: 1,
value: [instance.FileName || "", "xsd:string"],
},
Upload: {
writable: 1,
value: [instance.Upload || 0, "xsd:dateTime"],
},
LastFileType: { writable: 0, value: ["", "xsd:string"] },
LastFileName: { writable: 0, value: ["", "xsd:string"] },
LastTargetFileName: { writable: 0, value: ["", "xsd:string"] },
LastUpload: { writable: 0, value: [0, "xsd:dateTime"] },
StartTime: { writable: 0, value: [0, "xsd:dateTime"] },
CompleteTime: { writable: 0, value: [0, "xsd:dateTime"] },
};
for (const [k, v] of Object.entries(params)) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Uploads.${index}.${k}`),
timestamp,
{
object: [timestamp, 0],
writable: [timestamp, v.writable as 0 | 1],
value: [timestamp, v.value as [string | number | boolean, string]],
},
toClear
);
}
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Uploads.${index}.*`),
timestamp,
null,
toClear
);
}
sessionContext.syncState.uploadsToCreate.clear();
for (const instance of sessionContext.syncState.uploadsToDelete) {
toClear = device.set(
sessionContext.deviceData,
instance,
timestamp,
null,
toClear
);
for (const p of sessionContext.syncState.uploadsValues.keys()) {
if (p.segments[1] === instance.segments[1])
sessionContext.syncState.uploadsValues.delete(p);
}
}
sessionContext.syncState.uploadsToDelete.clear();
for (const [p, v] of sessionContext.syncState.uploadsValues) {
const attrs = sessionContext.deviceData.attributes.get(p);
if (attrs) {
if (attrs.writable && attrs.writable[1] && attrs.value) {
const val = device.sanitizeParameterValue([v, attrs.value[1][1]]);
if (val[0] !== attrs.value[1][0]) {
toClear = device.set(
sessionContext.deviceData,
p,
timestamp,
{ value: [timestamp, val] },
toClear
);
}
}
}
}
return toClear;
}
function generateGetRpcRequest( function generateGetRpcRequest(
sessionContext: SessionContext sessionContext: SessionContext
): GetParameterNames | GetParameterValues | GetParameterAttributes { ): GetParameterNames | GetParameterValues | GetParameterAttributes {
@@ -1740,6 +2021,7 @@ function generateSetRpcRequest(
| FactoryReset | FactoryReset
| Reboot | Reboot
| Download | Download
| Upload
) & { next?: string } { ) & { next?: string } {
const syncState = sessionContext.syncState; const syncState = sessionContext.syncState;
if (!syncState) return null; if (!syncState) return null;
@@ -1918,6 +2200,36 @@ function generateSetRpcRequest(
} }
} }
// Uploads
for (const [p, t] of syncState.uploadsUpload) {
const attrs = deviceData.attributes.get(p);
if (!(attrs && attrs.value && t <= attrs.value[1][0])) {
const fileTypeAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType")))
);
const fileNameAttrs = deviceData.attributes.get(
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileName")))
);
if (fileNameAttrs && fileNameAttrs.value) {
return {
name: "Upload",
commandKey: generateRpcId(sessionContext),
instance: p.segments[1] as string,
fileType: fileTypeAttrs
? fileTypeAttrs.value
? (fileTypeAttrs.value[1][0] as string)
: null
: null,
fileName:
sessionContext.deviceId +
"/" +
(fileNameAttrs.value[1][0] as string),
};
}
}
}
// Reboot // Reboot
if (syncState.reboot) { if (syncState.reboot) {
const p = sessionContext.deviceData.paths.get(Path.parse("Reboot")); const p = sessionContext.deviceData.paths.get(Path.parse("Reboot"));
@@ -2199,6 +2511,26 @@ function processDeclarations(
} }
} }
break; break;
case "Uploads":
if (
currentPath.length === 3 &&
currentPath.wildcard === 0 &&
declareAttributeValues &&
declareAttributeValues.value
) {
if (currentPath.segments[2] === "Upload") {
syncState.uploadsUpload.set(
currentPath,
declareAttributeValues.value[0]
);
} else {
syncState.uploadsValues.set(
currentPath,
declareAttributeValues.value[0]
);
}
}
break;
case "VirtualParameters": case "VirtualParameters":
if (currentPath.length <= 2) { if (currentPath.length <= 2) {
let d; let d;
@@ -2369,6 +2701,10 @@ function processInstances(
if (parent.length !== 1) return; if (parent.length !== 1) return;
instancesToDelete = sessionContext.syncState.downloadsToDelete; instancesToDelete = sessionContext.syncState.downloadsToDelete;
instancesToCreate = sessionContext.syncState.downloadsToCreate; instancesToCreate = sessionContext.syncState.downloadsToCreate;
} else if (parent.segments[0] === "Uploads") {
if (parent.length !== 1) return;
instancesToDelete = sessionContext.syncState.uploadsToDelete;
instancesToCreate = sessionContext.syncState.uploadsToCreate;
} else { } else {
instancesToDelete = sessionContext.syncState.instancesToDelete.get(parent); instancesToDelete = sessionContext.syncState.instancesToDelete.get(parent);
if (instancesToDelete == null) { if (instancesToDelete == null) {
@@ -2419,7 +2755,7 @@ export async function rpcResponse(
sessionContext: SessionContext, sessionContext: SessionContext,
id: string, id: string,
_rpcRes: CpeResponse _rpcRes: CpeResponse
): Promise<Fault> { ): Promise<void> {
if (id !== generateRpcId(sessionContext)) if (id !== generateRpcId(sessionContext))
throw new Error("Request ID not recognized"); throw new Error("Request ID not recognized");
@@ -2514,14 +2850,6 @@ export async function rpcResponse(
if (rpcReq.name !== "GetParameterValues") if (rpcReq.name !== "GetParameterValues")
throw new Error("Response name does not match request name"); throw new Error("Response name does not match request name");
if (rpcRes.parameterList.length !== rpcReq.parameterNames.length) {
return {
code: `invalid_response`,
message:
"The list of parameters returned in the GetParameterValuesResponse message does not match the requested parameters",
};
}
for (const p of rpcRes.parameterList) { for (const p of rpcRes.parameterList) {
toClear = device.set( toClear = device.set(
sessionContext.deviceData, sessionContext.deviceData,
@@ -2598,6 +2926,7 @@ export async function rpcResponse(
"FactoryReset", "FactoryReset",
"VirtualParameters", "VirtualParameters",
"Downloads", "Downloads",
"Uploads",
]) { ]) {
const p = sessionContext.deviceData.paths.get(Path.parse(n)); const p = sessionContext.deviceData.paths.get(Path.parse(n));
if (p && sessionContext.deviceData.attributes.has(p)) if (p && sessionContext.deviceData.attributes.has(p))
@@ -2808,6 +3137,84 @@ export async function rpcResponse(
operation.retries[channel] = sessionContext.retries[channel]; operation.retries[channel] = sessionContext.retries[channel];
} }
sessionContext.operations[rpcReq.commandKey] = operation;
if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {};
sessionContext.operationsTouched[rpcReq.commandKey] = 1;
}
} else if (rpcRes.name === "UploadResponse") {
if (rpcReq.name !== "Upload")
throw new Error("Response name does not match request name");
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Uploads.${rpcReq.instance}.Upload`),
timestamp + 1,
{ value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]] },
toClear
);
if (rpcRes.status === 0) {
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Uploads.${rpcReq.instance}.LastUpload`),
timestamp + 1,
{
value: [timestamp + 1, [sessionContext.timestamp, "xsd:dateTime"]],
},
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Uploads.${rpcReq.instance}.LastFileType`),
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Uploads.${rpcReq.instance}.LastFileName`),
timestamp + 1,
{ value: [timestamp + 1, [rpcReq.fileType, "xsd:string"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Uploads.${rpcReq.instance}.StartTime`),
timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.startTime, "xsd:dateTime"]] },
toClear
);
toClear = device.set(
sessionContext.deviceData,
Path.parse(`Uploads.${rpcReq.instance}.CompleteTime`),
timestamp + 1,
{ value: [timestamp + 1, [+rpcRes.completeTime, "xsd:dateTime"]] },
toClear
);
} else {
const operation = {
name: "Upload",
timestamp: sessionContext.timestamp,
provisions: sessionContext.provisions,
channels: sessionContext.channels,
retries: {},
args: {
instance: rpcReq.instance,
fileType: rpcReq.fileType,
fileName: rpcReq.fileName,
},
};
for (const channel of Object.keys(sessionContext.channels)) {
if (sessionContext.retries[channel] != null)
operation.retries[channel] = sessionContext.retries[channel];
}
sessionContext.operations[rpcReq.commandKey] = operation; sessionContext.operations[rpcReq.commandKey] = operation;
if (!sessionContext.operationsTouched) if (!sessionContext.operationsTouched)
sessionContext.operationsTouched = {}; sessionContext.operationsTouched = {};
@@ -2821,8 +3228,6 @@ export async function rpcResponse(
for (const c of toClear) for (const c of toClear)
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]); device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
} }
return null;
} }
export async function rpcFault( export async function rpcFault(
+54
View File
@@ -45,6 +45,7 @@ import {
RebootResponse, RebootResponse,
FactoryResetResponse, FactoryResetResponse,
DownloadResponse, DownloadResponse,
UploadResponse,
GetRPCMethodsRequest, GetRPCMethodsRequest,
RequestDownloadRequest, RequestDownloadRequest,
AcsResponse, AcsResponse,
@@ -496,6 +497,24 @@ function Download(methodRequest): string {
)}</FailureURL></cwmp:Download>`; )}</FailureURL></cwmp:Download>`;
} }
function Upload(methodRequest): string {
return `<cwmp:Upload><CommandKey>${
methodRequest.commandKey || ""
}</CommandKey><FileType>${methodRequest.fileType}</FileType><URL>${
methodRequest.url
}</URL><Username>${encodeEntities(
methodRequest.username || ""
)}</Username><Password>${encodeEntities(
methodRequest.password || ""
)}</Password><DelaySeconds>${
methodRequest.delaySeconds || "0"
}</DelaySeconds><SuccessURL>${encodeEntities(
methodRequest.successUrl || ""
)}</SuccessURL><FailureURL>${encodeEntities(
methodRequest.failureUrl || ""
)}</FailureURL></cwmp:Upload>`;
}
function DownloadResponse(xml: Element): DownloadResponse { function DownloadResponse(xml: Element): DownloadResponse {
let status: number, startTime: number, completeTime: number; let status: number, startTime: number, completeTime: number;
for (const c of xml.children) { for (const c of xml.children) {
@@ -544,6 +563,35 @@ function DownloadResponse(xml: Element): DownloadResponse {
}; };
} }
function UploadResponse(xml: Element): UploadResponse {
let status: number, startTime: number, completeTime: number;
for (const c of xml.children) {
switch (c.localName) {
case "Status":
status = parseInt(c.text);
break;
case "StartTime":
startTime = Date.parse(c.text);
break;
case "CompleteTime":
completeTime = Date.parse(c.text);
break;
}
}
if (!(status >= 0)) {
warnings.push({ message: "Invalid UploadResponse status" });
status = 0;
}
return {
name: "UploadResponse",
status: status,
startTime: startTime,
completeTime: completeTime,
};
}
function Inform(xml: Element): InformRequest { function Inform(xml: Element): InformRequest {
let retryCount: number, evnt: string[]; let retryCount: number, evnt: string[];
let parameterList: [Path, string | number | boolean, string][]; let parameterList: [Path, string | number | boolean, string][];
@@ -927,6 +975,9 @@ export function request(
case "DownloadResponse": case "DownloadResponse":
rpc.cpeResponse = DownloadResponse(methodElement); rpc.cpeResponse = DownloadResponse(methodElement);
break; break;
case "UploadResponse":
rpc.cpeResponse = UploadResponse(methodElement);
break;
case "Fault": case "Fault":
rpc.cpeFault = fault(methodElement); rpc.cpeFault = fault(methodElement);
break; break;
@@ -1022,6 +1073,9 @@ export function response(rpc: {
case "Download": case "Download":
body = Download(rpc.acsRequest); body = Download(rpc.acsRequest);
break; break;
case "Upload":
body = Upload(rpc.acsRequest);
break;
default: default:
throw new Error( throw new Error(
`Unknown method request ${(rpc.acsRequest as AcsRequest).name}` `Unknown method request ${(rpc.acsRequest as AcsRequest).name}`
+30 -3
View File
@@ -121,6 +121,10 @@ export interface SyncState {
downloadsToCreate: InstanceSet; downloadsToCreate: InstanceSet;
downloadsValues: Map<Path, string | number>; downloadsValues: Map<Path, string | number>;
downloadsDownload: Map<Path, number>; downloadsDownload: Map<Path, number>;
uploadsToDelete: Set<Path>;
uploadsToCreate: InstanceSet;
uploadsValues: Map<Path, string | number>;
uploadsUpload: Map<Path, number>;
reboot: number; reboot: number;
factoryReset: number; factoryReset: number;
} }
@@ -193,7 +197,7 @@ export interface Operation {
instance: string; instance: string;
fileType: string; fileType: string;
fileName: string; fileName: string;
targetFileName: string; targetFileName?: string;
}; };
} }
@@ -207,7 +211,8 @@ export type AcsRequest =
| DeleteObject | DeleteObject
| FactoryReset | FactoryReset
| Reboot | Reboot
| Download; | Download
| Upload;
export interface GetParameterNames { export interface GetParameterNames {
name: "GetParameterNames"; name: "GetParameterNames";
@@ -277,6 +282,20 @@ export interface Download {
failureUrl?: string; failureUrl?: string;
} }
export interface Upload {
name: "Upload";
commandKey: string;
instance: string;
fileType: string;
fileName?: string;
url?: string;
username?: string;
password?: string;
delaySecods?: number;
successUrl?: string;
failureUrl?: string;
}
export interface SpvFault { export interface SpvFault {
parameterName: string; parameterName: string;
faultCode: string; faultCode: string;
@@ -305,7 +324,8 @@ export type CpeResponse =
| DeleteObjectResponse | DeleteObjectResponse
| RebootResponse | RebootResponse
| FactoryResetResponse | FactoryResetResponse
| DownloadResponse; | DownloadResponse
| UploadResponse;
export interface GetParameterNamesResponse { export interface GetParameterNamesResponse {
name: "GetParameterNamesResponse"; name: "GetParameterNamesResponse";
@@ -357,6 +377,13 @@ export interface DownloadResponse {
completeTime?: number; completeTime?: number;
} }
export interface UploadResponse {
name: "UploadResponse";
status: number;
startTime?: number;
completeTime?: number;
}
export type CpeRequest = export type CpeRequest =
| InformRequest | InformRequest
| TransferCompleteRequest | TransferCompleteRequest
+1 -6
View File
@@ -245,7 +245,7 @@ router.get("/", async (ctx) => {
)}; )};
window.permissionSets = ${JSON.stringify(permissionSets)}; window.permissionSets = ${JSON.stringify(permissionSets)};
</script> </script>
<script type="module" src="app.js"></script>${wizard} <script src="app.js"></script>${wizard}
</body> </body>
</html> </html>
`; `;
@@ -259,11 +259,6 @@ koa.use(
deflate: { deflate: {
flush: constants.Z_SYNC_FLUSH, flush: constants.Z_SYNC_FLUSH,
}, },
br: {
params: {
[constants.BROTLI_PARAM_QUALITY]: 5,
},
},
}) })
); );
+4
View File
@@ -58,6 +58,10 @@ export async function deleteResource(
await db.deleteFile(id); await db.deleteFile(id);
break; break;
case "uploads":
await db.deleteUpload(id);
break;
case "faults": case "faults":
await deleteFault(id); await deleteFault(id);
break; break;
+58 -78
View File
@@ -32,7 +32,6 @@ import Authorizer from "../common/authorizer";
import { ping } from "../ping"; import { ping } from "../ping";
import * as url from "url"; import * as url from "url";
import { decodeTag } from "../common"; import { decodeTag } from "../common";
import { stringify as yamlStringify } from "../common/yaml";
const router = new Router(); const router = new Router();
export default router; export default router;
@@ -50,6 +49,7 @@ const RESOURCE_IDS = {
presets: "_id", presets: "_id",
provisions: "_id", provisions: "_id",
files: "_id", files: "_id",
uploads: "_id",
virtualParameters: "_id", virtualParameters: "_id",
config: "_id", config: "_id",
permissions: "_id", permissions: "_id",
@@ -63,6 +63,7 @@ const resources = {
presets: 0 | RESOURCE_DELETE | RESOURCE_PUT, presets: 0 | RESOURCE_DELETE | RESOURCE_PUT,
provisions: 0 | RESOURCE_DELETE | RESOURCE_PUT, provisions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
files: 0 | RESOURCE_DELETE, files: 0 | RESOURCE_DELETE,
uploads: 0 | RESOURCE_DELETE,
virtualParameters: 0 | RESOURCE_DELETE | RESOURCE_PUT, virtualParameters: 0 | RESOURCE_DELETE | RESOURCE_PUT,
config: 0 | RESOURCE_DELETE | RESOURCE_PUT, config: 0 | RESOURCE_DELETE | RESOURCE_PUT,
permissions: 0 | RESOURCE_DELETE | RESOURCE_PUT, permissions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
@@ -71,10 +72,6 @@ const resources = {
tasks: 0, tasks: 0,
}; };
function singleParam(p: string | string[]): string {
return Array.isArray(p) ? p[p.length - 1] : p;
}
router.get(`/devices/:id.csv`, async (ctx) => { router.get(`/devices/:id.csv`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const log = { const log = {
@@ -111,28 +108,19 @@ router.get(`/devices/:id.csv`, async (ctx) => {
for (const k of Object.keys(res[0]).sort()) { for (const k of Object.keys(res[0]).sort()) {
const p = res[0][k]; const p = res[0][k];
let value = "";
let type = "";
if (p.value) {
value = p.value[0];
type = p.value[1];
if (type === "xsd:dateTime" && typeof value === "number")
value = new Date(value).toJSON();
}
const row = [ const row = [
k, k,
p.object, p.object,
new Date(p.objectTimestamp).toJSON(), p.objectTimestamp,
p.writable, p.writable,
new Date(p.writableTimestamp).toJSON(), p.writableTimestamp,
`"${value.toString().replace(/"/g, '""')}"`, p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "",
type, p.value != null ? p.value[1] : "",
new Date(p.valueTimestamp).toJSON(), p.valueTimestamp,
p.notification, p.notification,
new Date(p.notificationTimestamp).toJSON(), p.notificationTimestamp,
p.accessList ? p.accessList.join(", ") : "", p.accessList ? p.accessList.join(", ") : "",
new Date(p.accessListTimestamp).toJSON(), p.accessListTimestamp,
]; ];
ctx.body.write(row.map((r) => (r != null ? r : "")).join(",") + "\n"); ctx.body.write(row.map((r) => (r != null ? r : "")).join(",") + "\n");
} }
@@ -145,7 +133,7 @@ for (const [resource, flags] of Object.entries(resources)) {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
let filter: Expression = authorizer.getFilter(resource, 1); let filter: Expression = authorizer.getFilter(resource, 1);
if (ctx.request.query.filter) if (ctx.request.query.filter)
filter = and(filter, parse(singleParam(ctx.request.query.filter))); filter = and(filter, parse(ctx.request.query.filter));
const log = { const log = {
message: `Count ${resource}`, message: `Count ${resource}`,
@@ -182,13 +170,13 @@ for (const [resource, flags] of Object.entries(resources)) {
const options: QueryOptions = {}; const options: QueryOptions = {};
let filter: Expression = authorizer.getFilter(resource, 2); let filter: Expression = authorizer.getFilter(resource, 2);
if (ctx.request.query.filter) if (ctx.request.query.filter)
filter = and(filter, parse(singleParam(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)
options.sort = JSON.parse(singleParam(ctx.request.query.sort)); options.sort = JSON.parse(ctx.request.query.sort);
if (ctx.request.query.projection) { if (ctx.request.query.projection) {
options.projection = singleParam(ctx.request.query.projection) options.projection = ctx.request.query.projection
.split(",") .split(",")
.reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {}); .reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {});
} }
@@ -236,11 +224,11 @@ for (const [resource, flags] of Object.entries(resources)) {
const options: QueryOptions = { projection: {} }; const options: QueryOptions = { projection: {} };
let filter: Expression = authorizer.getFilter(resource, 2); let filter: Expression = authorizer.getFilter(resource, 2);
if (ctx.request.query.filter) if (ctx.request.query.filter)
filter = and(filter, parse(singleParam(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)
options.sort = JSON.parse(singleParam(ctx.request.query.sort)); options.sort = JSON.parse(ctx.request.query.sort);
const log = { const log = {
message: `Query ${resource} (CSV)`, message: `Query ${resource} (CSV)`,
@@ -257,7 +245,7 @@ for (const [resource, flags] of Object.entries(resources)) {
} }
const columns: Record<string, Expression> = JSON.parse( const columns: Record<string, Expression> = JSON.parse(
singleParam(ctx.request.query.columns) ctx.request.query.columns
); );
const now = Date.now(); const now = Date.now();
@@ -299,23 +287,11 @@ for (const [resource, flags] of Object.entries(resources)) {
return tags.join(", "); return tags.join(", ");
} }
if (e === exp) {
const p = obj[e[1]];
if (
p &&
p.value &&
p.value[1] === "xsd:dateTime" &&
typeof p.value[0] === "number"
)
return new Date(p.value[0]).toJSON();
}
} else if (resource === "faults") {
if (e[1] === "detail") return yamlStringify(obj["detail"]);
} }
} else if (e[0] === "FUNC") { } else if (e[0] === "FUNC") {
if (e[1] === "DATE_STRING") { if (e[1] === "DATE_STRING") {
if (e[2] && !Array.isArray(e[2])) if (e[2] && !Array.isArray(e[2]))
return new Date(e[2]).toJSON(); return new Date(e[2]).toISOString();
} }
} }
} }
@@ -387,7 +363,7 @@ for (const [resource, flags] of Object.entries(resources)) {
}); });
if (flags & RESOURCE_DELETE) { if (flags & RESOURCE_DELETE) {
router.delete(`/${resource}/:id`, async (ctx) => { router.delete(`/${resource}/:id(.+)`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const log = { const log = {
message: `Delete ${resource}`, message: `Delete ${resource}`,
@@ -461,36 +437,6 @@ for (const [resource, flags] of Object.entries(resources)) {
} }
} }
router.get("/blob/files/:id", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const resource = "files";
const id = ctx.params.id;
const log = {
message: `Download ${resource}`,
context: ctx,
id: id,
};
const filter = and(authorizer.getFilter(resource, 2), [
"=",
["PARAM", RESOURCE_IDS[resource]],
ctx.params.id,
]);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
const count = await db.count(resource, filter);
if (!count) return void (ctx.status = 404);
logger.accessInfo(log);
ctx.body = db.downloadFile(id);
ctx.attachment(id);
});
router.put("/files/:id", async (ctx) => { router.put("/files/:id", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const resource = "files"; const resource = "files";
@@ -509,11 +455,10 @@ router.put("/files/:id", async (ctx) => {
} }
const metadata = { const metadata = {
fileType: singleParam(ctx.request.headers["metadata.filetype"]) || "", fileType: ctx.request.headers["metadata.filetype"] || "",
oui: singleParam(ctx.request.headers["metadata.oui"]) || "", oui: ctx.request.headers["metadata.oui"] || "",
productClass: productClass: ctx.request.headers["metadata.productclass"] || "",
singleParam(ctx.request.headers["metadata.productclass"]) || "", version: ctx.request.headers["metadata.version"] || "",
version: singleParam(ctx.request.headers["metadata.version"]) || "",
}; };
const validate = authorizer.getValidator(resource, metadata); const validate = authorizer.getValidator(resource, metadata);
@@ -535,6 +480,41 @@ router.put("/files/:id", async (ctx) => {
ctx.body = ""; ctx.body = "";
}); });
router.get("/uploads/blob/:id(.+)", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const resource = "uploads";
const id = ctx.params.id;
const log = {
message: `Download ${resource} blob`,
context: ctx,
id: id,
};
1;
const filter = and(authorizer.getFilter(resource, 2), [
"=",
["PARAM", RESOURCE_IDS[resource]],
id,
]);
if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
const res = await db.query(resource, filter);
if (!res[0]) return void (ctx.status = 404);
ctx.type = "application/octet-stream";
ctx.attachment(id.replace(/\//g, "_"));
ctx.body = new stream.PassThrough();
const downloadStream = await db.getUploadBlob(id);
downloadStream.pipe(ctx.body);
logger.accessInfo(log);
});
router.post("/devices/:id/tasks", async (ctx) => { router.post("/devices/:id/tasks", async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const log = { const log = {
@@ -653,7 +633,7 @@ router.post("/devices/:id/tags", async (ctx) => {
}); });
router.get("/ping/:host", async (ctx) => { router.get("/ping/:host", async (ctx) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
ping(ctx.params.host, (err, parsed) => { ping(ctx.params.host, (err, parsed) => {
if (parsed) { if (parsed) {
ctx.body = parsed; ctx.body = parsed;
+22 -39
View File
@@ -28,6 +28,7 @@ import { minimize } from "../common/boolean-expression";
const RESOURCE_COLLECTION = { const RESOURCE_COLLECTION = {
files: "fs.files", files: "fs.files",
uploads: "uploads.files",
}; };
let db: Db; let db: Db;
@@ -36,23 +37,12 @@ onConnect(async (_db) => {
db = _db; db = _db;
}); });
export function query(
resource: string,
filter: Expression,
options?: QueryOptions
): Promise<any[]>;
export function query(
resource: string,
filter: Expression,
options: QueryOptions,
callback: (doc: any) => void
): Promise<void>;
export function query( export function query(
resource: string, resource: string,
filter: Expression, filter: Expression,
options?: QueryOptions, options?: QueryOptions,
callback?: (doc: any) => void callback?: (doc: any) => void
): Promise<void | any[]> { ): Promise<any[]> {
options = options || {}; options = options || {};
let q; let q;
filter = expression.evaluate(filter, null, Date.now()); filter = expression.evaluate(filter, null, Date.now());
@@ -123,6 +113,8 @@ export function query(
docs = docs.map((d) => mongodbFunctions.flattenPreset(d)); docs = docs.map((d) => mongodbFunctions.flattenPreset(d));
else if (resource === "files") else if (resource === "files")
docs = docs.map((d) => mongodbFunctions.flattenFile(d)); docs = docs.map((d) => mongodbFunctions.flattenFile(d));
else if (resource === "uploads")
docs = docs.map((d) => mongodbFunctions.flattenUpload(d));
return resolve(docs); return resolve(docs);
}); });
} else { } else {
@@ -137,6 +129,8 @@ export function query(
doc = mongodbFunctions.flattenPreset(doc); doc = mongodbFunctions.flattenPreset(doc);
else if (resource === "files") else if (resource === "files")
doc = mongodbFunctions.flattenFile(doc); doc = mongodbFunctions.flattenFile(doc);
else if (resource === "uploads")
doc = mongodbFunctions.flattenUpload(doc);
callback(doc); callback(doc);
}, },
(err) => { (err) => {
@@ -237,17 +231,9 @@ export function putProvision(
): Promise<void> { ): Promise<void> {
if (!object.script) object.script = ""; if (!object.script) object.script = "";
try { try {
new Script(`"use strict";(function(){\n${object.script}\n})();`, { new Script(`"use strict";(function(){\n${object.script}\n})();`);
filename: id, } catch (error) {
lineOffset: -1, return Promise.reject(error);
});
} catch (err) {
if (err.stack && err.stack.startsWith(`${id}:`)) {
return Promise.reject(
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
);
}
return Promise.reject(err);
} }
return putResource("provisions", id, object); return putResource("provisions", id, object);
} }
@@ -262,17 +248,9 @@ export function putVirtualParameter(
): Promise<void> { ): Promise<void> {
if (!object.script) object.script = ""; if (!object.script) object.script = "";
try { try {
new Script(`"use strict";(function(){\n${object.script}\n})();`, { new Script(`"use strict";(function(){\n${object.script}\n})();`);
filename: id, } catch (error) {
lineOffset: -1, return Promise.reject(error);
});
} catch (err) {
if (err.stack && err.stack.startsWith(`${id}:`)) {
return Promise.reject(
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
);
}
return Promise.reject(err);
} }
return putResource("virtualParameters", id, object); return putResource("virtualParameters", id, object);
} }
@@ -326,11 +304,6 @@ export function deleteUser(id: string): Promise<void> {
return deleteResource("users", id); return deleteResource("users", id);
} }
export function downloadFile(filename: string): Readable {
const bucket = new GridFSBucket(db);
return bucket.openDownloadStreamByName(filename);
}
export function putFile( export function putFile(
filename: string, filename: string,
metadata: Record<string, string>, metadata: Record<string, string>,
@@ -358,6 +331,16 @@ export function deleteFile(filename: string): Promise<void> {
}); });
} }
export async function deleteUpload(filename: string): Promise<void> {
const bucket = new GridFSBucket(db, { bucketName: "uploads" });
await bucket.delete(filename as any);
}
export async function getUploadBlob(filename: string): Promise<Readable> {
const bucket = new GridFSBucket(db, { bucketName: "uploads" });
return bucket.openDownloadStreamByName(filename);
}
export function deleteFault(id: string): Promise<void> { export function deleteFault(id: string): Promise<void> {
return deleteResource("faults", id); return deleteResource("faults", id);
} }
+1209 -910
View File
File diff suppressed because it is too large. Load diff
+36 -32
View File
@@ -1,6 +1,6 @@
{ {
"name": "genieacs", "name": "genieacs",
"version": "1.2.4", "version": "1.2.3",
"description": "A TR-069 Auto Configuration Server (ACS)", "description": "A TR-069 Auto Configuration Server (ACS)",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -14,69 +14,73 @@
], ],
"author": "Zaid Abdulla <zaid@genieacs.com>", "author": "Zaid Abdulla <zaid@genieacs.com>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "preferGlobal": true,
"directories": { "directories": {
"bin": "bin" "bin": "bin",
"tools": "tools"
}, },
"dependencies": { "dependencies": {
"espresso-iisojs": "^1.0.3", "espresso-iisojs": "^1.0.3",
"iconv-lite": "^0.6.2", "iconv-lite": "^0.6.2",
"ipaddr.js": "^2.0.0", "ipaddr.js": "^2.0.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.13.1", "koa": "^2.13.0",
"koa-bodyparser": "^4.3.0", "koa-bodyparser": "^4.3.0",
"koa-compress": "^5.0.1", "koa-compress": "^5.0.1",
"koa-jwt": "^4.0.0", "koa-jwt": "^4.0.0",
"koa-router": "^10.0.0", "koa-router": "^9.4.0",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"later": "^1.2.0", "later": "^1.2.0",
"mongodb": "^3.6.3", "mongodb": "^3.6.2",
"parsimmon": "^1.16.0", "parsimmon": "^1.16.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5",
"yaml": "^1.10.0"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.2.0", "@rollup/plugin-node-resolve": "^10.0.0",
"@rollup/plugin-typescript": "^8.2.0", "@rollup/plugin-typescript": "^6.0.0",
"@types/codemirror": "0.0.108",
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.0",
"@types/koa": "^2.13.0", "@types/koa": "^2.11.6",
"@types/koa-compress": "^4.0.1", "@types/koa-compress": "^4.0.1",
"@types/koa-router": "^7.4.1", "@types/koa-router": "^7.4.1",
"@types/koa-static": "^4.0.1", "@types/koa-static": "^4.0.1",
"@types/mithril": "^2.0.6", "@types/mithril": "^2.0.5",
"@types/mongodb": "^3.6.8", "@types/mongodb": "^3.5.31",
"@types/node": "^14.14.31", "@types/node": "^14.14.5",
"@types/parsimmon": "^1.10.6", "@types/parsimmon": "^1.10.3",
"@types/seedrandom": "^2.4.28", "@types/seedrandom": "^2.4.28",
"@typescript-eslint/eslint-plugin": "^4.15.2", "@types/svgo": "^1.3.3",
"@typescript-eslint/parser": "^4.15.2", "@typescript-eslint/eslint-plugin": "^4.6.0",
"ava": "^3.15.0", "@typescript-eslint/parser": "^4.6.0",
"codemirror": "^5.59.4", "ava": "^3.13.0",
"codemirror": "^5.58.2",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"eslint": "^7.20.0", "eslint": "^7.12.1",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^6.14.0",
"esm": "^3.2.25", "esm": "^3.2.25",
"jsbi": "^3.1.4",
"mithril": "^2.0.4", "mithril": "^2.0.4",
"postcss": "^8.2.6", "postcss": "^8.1.4",
"postcss-import": "^14.0.0", "postcss-import": "^13.0.0",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.2.1", "prettier": "^2.1.2",
"rollup": "^2.39.1", "rollup": "^2.32.1",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"svgo": "^2.1.0", "source-map-loader": "^1.1.1",
"ts-node": "^9.1.1", "svgo": "^1.3.2",
"typescript": "^4.1.5", "ts-node": "^9.0.0",
"yaml": "^1.10.0" "typescript": "^4.0.5",
"webpack": "^5.2.0"
}, },
"engines": { "engines": {
"node": ">=12.13.0" "node": ">=12.13.0"
}, },
"scripts": { "scripts": {
"test": "ava --verbose --color", "test": "ava --verbose --color",
"lint-eslint": "eslint --fix \"bin/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\"", "lint-eslint": "eslint --fix \"bin/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/*.ts\"",
"lint-prettier": "prettier --prose-wrap always --write .", "lint-prettier": "prettier --parser typescript --write \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"",
"lint-tsc": "tsc --noEmit", "lint-tsc": "tsc --noEmit",
"lint": "npm run lint-eslint && npm run lint-prettier && npm run lint-tsc", "lint": "npm run lint-eslint && npm run lint-prettier && npm run lint-tsc",
"build": "node -r esm -r ts-node/register/transpile-only build/build" "build": "node -r esm -r ts-node/register/transpile-only build/build"
-861
View File
@@ -1,861 +0,0 @@
[
[
{
"name": "Mark McGwire",
"hr": 65,
"avg": 0.278
},
{
"name": "Sammy Sosa",
"hr": 63,
"avg": 0.288
}
],
{
"top1": {
"key1": "scalar1"
},
"top2": {
"key2": "scalar2"
},
"top3": {
"scalar1": "scalar3"
},
"top4": {
"scalar2": "scalar4"
},
"top5": "scalar5",
"top6": {
"key6": "scalar6"
}
},
"text",
["a", "b", 42, "d"],
{
"a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~": "safe",
"?foo": "safe question mark",
":foo": "safe colon",
"-foo": "safe dash",
"this is#not": "a comment"
},
{
"": "a"
},
"foo",
{
"key": "value",
"foo": "key"
},
[1, -2, 33],
{
"plain": "a b\nc"
},
[["s1_i1", "s1_i2"], "s2"],
{
"First occurrence": "Foo",
"Second occurrence": "Foo",
"Override anchor": "Bar",
"Reuse anchor": "Bar"
},
"k:#foo &a !t s",
["a"],
{
"escaped slash": "a/b"
},
{
"plain": "This unquoted scalar spans many lines.",
"quoted": "So does this quoted scalar.\n"
},
"here's to \"quotes\"",
{
"foo": "bar"
},
"ab cd\nef\n\ngh\n",
["detected\n", "\n\n# detected\n", " explicit\n", "detected\n"],
"foo: bar\": baz",
"plain\\value\\with\\backslashes",
{
"plain": "text lines",
"quoted": "text lines",
"block": "text\n \tlines\n"
},
"a",
{
"foo": "you",
"bar": "far"
},
{
"sequence": ["entry", ["nested"]],
"mapping": {
"foo": "bar"
}
},
{
"literal": "some\ntext\n",
"folded": "some text\n"
},
[
{
"one": "two",
"three": "four"
},
{
"five": "six",
"seven": "eight"
}
],
{
"Folding": "Empty line\nas a line feed",
"Chomping": "Clipped empty lines\n"
},
[
["one", "two"],
["three", "four"]
],
{
"foo": "bar"
},
{
"key": "value"
},
{
"explicit key": null,
"block key\n": ["one", "two"]
},
["foo"],
[
{
"foo": "bar"
},
["baz", "baz"]
],
"ab\n\n \n",
{
"foo: bar\\": "baz'"
},
{
"Not indented": {
"By one space": "By four\n spaces\n",
"Flow style": ["By two", "Also by two", "Still by two"]
}
},
"\\//||\\/||\n// || ||__\n",
{
"foo": [
"a",
{
"key": "value"
}
]
},
{
"a": null,
"b": null
},
"foo",
{
"a": "b",
"": "a"
},
{
"foo\nbar:baz\tx \\$%^&*()x": 23,
"x\\ny:z\\tx $%^&*()x": 24
},
"Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n",
" foo\nbar\nbaz ",
{ "matches %": 20 },
[
"flow in block",
"Block scalar\n",
{
"foo": "bar"
}
],
{
"a": "b",
"c": 42,
"e": "f",
"g": "h",
"23": false
},
"ab",
" 1st non-empty\n2nd non-empty 3rd non-empty ",
{
"top1": {
"key1": "one"
},
"top2": {
"key2": "two"
},
"top3": {
"key3": "three"
},
"top4": {
"key4": "four"
},
"top5": {
"key5": "five"
},
"top6": "six",
"top7": "seven"
},
{
"hr": ["Mark McGwire", "Sammy Sosa"],
"rbi": ["Sammy Sosa", "Ken Griffey"]
},
"\nfolded line\nnext line\n * bullet\n\n * list\n * lines\n\nlast line\n",
["word1", "word2"],
{
"a": null,
"b": null,
"c": null
},
{
"nested sequences": [[[[]]], [[{}]]],
"key1": [],
"key2": {}
},
"---word1 word2",
{
"implicit block key": [
{
"implicit flow key": "value"
}
]
},
{
"key ends with two colons::": "value"
},
[
{
"single line": null,
"a": "b"
},
{
"multi line": null,
"a": "b"
}
],
"a",
{
"key": ["item1", "item2"]
},
[
"double quoted",
"single quoted",
"plain text",
["nested"],
{
"single": "pair"
}
],
["unicode anchor"],
[
{
"key": "value",
"key2": "value2"
},
{
"key3": "value3"
}
],
"trimmed\n\n\nas space",
"Mark McGwire's year was crippled by a knee injury.\n",
[
{
"single line": null,
"a": "b"
},
{
"multi line": null,
"a": "b"
}
],
{
"a": {
"b": {
"c": "d"
},
"e": {
"f": "g"
}
},
"h": "i"
},
{
"foo": {
"bar": "baz"
}
},
[
{
"single line": "value"
},
{
"multi line": "value"
}
],
{
"single": "text",
"double": "text"
},
" 1st non-empty\n2nd non-empty 3rd non-empty ",
[
{
"item": "Super Hoop",
"quantity": 1
},
{
"item": "Basketball",
"quantity": 4
},
{
"item": "Big Shoes",
"quantity": 1
}
],
"a b c d\ne",
{
"a": ["b", ["c", "d"]]
},
{
"strip": "text",
"clip": "text\n",
"keep": "text\n"
},
{
"a": "b c",
"d": "e f"
},
["single multiline - sequence entry"],
{
"1": [2, 3],
"4": 5
},
[
{
"bla\"keks": "foo"
},
{
"bla]keks": "foo"
}
],
"folded text\n",
"foo",
{
"key": {
"a": "b"
}
},
{
"adjacent": "value",
"readable": "value",
"empty": null
},
[
{
"a": "b"
},
{
"c": "d"
},
{
"e": "f"
},
{
"g": "h"
}
],
{
"tab": "\tstring"
},
[
{
"foo bar": "baz"
}
],
{
"anchored": "value",
"alias": "value"
},
["explicit indent and chomp", "chomp and explicit indent"],
{
"a": ["b", "c"]
},
{
"foo": "bar"
},
[
"::vector",
": - ()",
"Up, up, and away!",
-123,
"http://example.com/foo#bar",
[
"::vector",
": - ()",
"Up, up and away!",
-123,
"http://example.com/foo#bar"
]
],
{
"a": "b",
"seq": ["a"],
"c": "d"
},
["foo", "bar", 42],
"line1 # no comment line3\n",
"\n\nliteral\n \n\ntext\n",
{
"a": "b"
},
{
"k": ["a", "b"]
},
"a b c d\ne",
"---word1 word2",
["a", 2, 4, "d"],
{
"a": [
"b",
"c",
{
"d": ["e", "f"]
}
]
},
{
"a": " more indented\nregular\n",
"b": "\n\n more indented\nregular\n"
},
{
"strip": "# text",
"clip": "# text\n",
"keep": "# text\n\n"
},
{
"safe": "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~ !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~",
"safe question mark": "?foo",
"safe colon": ":foo",
"safe dash": "-foo"
},
"line1 line2 line3\n",
["Mark McGwire", "Sammy Sosa", "Ken Griffey"],
["a"],
["a", ["b", "c"]],
{
"unicode": "Sosa did fine.☺",
"control": "\b1998\t1999\t2000\n",
"hex esc": "\r\n is \r\n",
"single": "\"Howdy!\" he cried.",
"quoted": " # Not a 'comment'.",
"tie-fighter": "|\\-*-/|"
},
[["-", "-"]],
"folded text\n",
{
"a": 13,
"1.5": "d"
},
{
"foo": 1,
"bar": 2,
"text": "a\n \nb\n\nc\n\nd\n"
},
{
"wanted": "love ♥ and peace ☮"
},
{
"name": "Mark McGwire",
"accomplishment": "Mark set a major league home run record in 1998.\n",
"stats": "65 Home Runs\n0.278 Batting Average\n"
},
{
"foo": "bar",
"baz": "foo"
},
"1st non-empty\n2nd non-empty 3rd non-empty",
{
"quoted": "Quoted \t",
"block": "void main() {\n\tprintf(\"Hello, world!\\n\");\n}\n"
},
{
"foo": "blue",
"bar": "arrr",
"baz": "jazz"
},
[
{
"Mark McGwire": 65
},
{
"Sammy Sosa": 63
},
{
"Ken Griffy": 58
}
],
{
"1": 2,
"3": 4
},
{
"hr": ["Mark McGwire", "Sammy Sosa"],
"rbi": ["Sammy Sosa", "Ken Griffey"]
},
"k:#foo &a !t s",
{
"block sequence": [
"one",
{
"two": "three"
}
]
},
{
"First occurrence": "Value",
"Second occurrence": "Value"
},
{
"a true": "null d",
"e 42": null
},
{
"foo": "bar"
},
["foo", "bar", 42],
"trimmed\n\n\nas space",
"scalar",
{
"strip": "",
"clip": "",
"keep": "\n"
},
{
"foo": {
"bar": 1
},
"baz": 2
},
{
"a": 47,
"c": "d"
},
{
"implicit block key": [
{
"implicit flow key": "value"
}
]
},
["a", "b", "c", "c", ""],
[
["a", "b", "c"],
{
"a": "b",
"c": "d",
"e": "f"
},
[]
],
{
"implicit block key": [
{
"implicit flow key": "value"
}
]
},
{
"a": "ab\n\ncd\nef\n"
},
{
"literal": "value\n",
"folded": "value\n"
},
{
"a": [
"b",
"c",
{
"d": ["e", "f"]
}
]
},
"literal\n\ttext\n",
"foo \n\n\t bar\n\nbaz\n",
[
{
"a": "b"
}
],
"ab",
["plain", "double quoted", "single quoted", "block\n", "plain again"],
{
"a": " ",
"b": " ",
"c": " ",
"d": " ",
"e": "\n",
"f": "\n",
"g": "\n\n",
"h": "\n\n"
},
{
"key": "value with\ntabs"
},
{
"": null
},
[
{
"single line": "value"
},
{
"multi line": "value"
}
],
"folded to a space,\nto a line feed, or \t \tnon-content",
["literal\n", " folded\n", "keep\n\n", " strip"],
{
"key": "value"
},
{
"american": ["Boston Red Sox", "Detroit Tigers", "New York Yankees"],
"national": ["New York Mets", "Chicago Cubs", "Atlanta Braves"]
},
" 1st non-empty\n2nd non-empty 3rd non-empty ",
{},
[
["a", "b"],
{
"a": "b"
},
"a",
"b",
"c"
],
"folded to a space,\nto a line feed, or \t \tnon-content",
[
{
"foo": "bar"
}
],
["detected\n", "\n\n# detected\n", " explicit\n", "\t\ndetected\n"],
{
"top1": [
"item1",
{
"key2": "value2"
},
"item3"
],
"top2": "value2"
},
{
"foo": [42],
"bar": [44]
},
{
"23": "d",
"a": 4.2
},
"Document",
{
"Date": "2001-11-23 15:03:17 -5",
"User": "ed",
"Fatal": "Unknown variable \"bar\"",
"Stack": [
{
"file": "TopClass.py",
"line": 23,
"code": "x = MoreObject(\"345\\n\")\n"
},
{ "file": "MoreClass.py", "line": 58, "code": "foo = bar" }
]
},
{
"plain key": "in-line value",
"": null,
"quoted key": ["entry"]
},
["12", 12, "12"],
{
"aaa": "bbb"
},
[":,"],
{
"sequence": ["one", "two"],
"mapping": {
"sky": "blue",
"sea": "green"
}
},
{
"seq": ["a", "b"]
},
"here's to \"quotes\"",
{
"hr": 65,
"avg": 0.278,
"rbi": 147
},
"\n\nliteral\n \n\ntext\n",
" 1st non-empty\n2nd non-empty 3rd non-empty ",
"literal\n\ttext\n",
{
"block mapping": {
"key": "value"
}
},
" foo\nbar\nbaz ",
"ab cd\nef\n\ngh\n",
"foo",
{
"top1": {
"key1": "one"
},
"top2": {
"key2": "two"
},
"top3": {
"key3": "three"
},
"top4": {
"key4": "four"
},
"top5": {
"key5": "five"
},
"top6": "six",
"top7": "seven"
},
[
{
"url": "http://example.org"
}
],
{
"sequence": ["one", "two"],
"mapping": {
"sky": "blue",
"sea": "green"
}
},
{
"invoice": 34843,
"date": "2001-01-23",
"bill-to": {
"given": "Chris",
"family": "Dumars",
"address": {
"lines": "458 Walkman Dr.\nSuite #292\n",
"city": "Royal Oak",
"state": "MI",
"postal": 48046
}
},
"ship-to": {
"given": "Chris",
"family": "Dumars",
"address": {
"lines": "458 Walkman Dr.\nSuite #292\n",
"city": "Royal Oak",
"state": "MI",
"postal": 48046
}
},
"product": [
{
"sku": "BL394D",
"quantity": 4,
"description": "Basketball",
"price": 450
},
{
"sku": "BL4438H",
"quantity": 1,
"description": "Super Hoop",
"price": 2392
}
],
"tax": 251.42,
"total": 4443.52,
"comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338."
},
["a", "b", "a", "b"],
[
null,
"block node\n",
["one", "two"],
{
"one": "two"
}
],
{
"a": "scalar a",
"b": "scalar a"
},
{
"foo": "",
"": "bar"
},
{
"key": "value"
},
"scalar %YAML 1.2",
{
"Folding": "Empty line\nas a line feed",
"Chomping": "Clipped empty lines\n"
},
{
"key": "value"
},
[
["name", "hr", "avg"],
["Mark McGwire", 65, 0.278],
["Sammy Sosa", 63, 0.288]
],
{
"literal": "value\n",
"folded": "value\n"
},
{
"Mark McGwire": {
"hr": 65,
"avg": 0.278
},
"Sammy Sosa": {
"hr": 63,
"avg": 0.288
}
},
{
"a": "b",
"c": "d"
},
{
"key": [[["value"]]]
},
{
"a": 1,
"b": null,
"c": 3
},
{
"event": "outgoing HTTP response",
"timestamp": "2021-01-21T18:44:57.116Z",
"remoteAddress": "127.0.0.1",
"deviceId": "202BC1-BM632w-000000",
"connection": "2021-01-21T18:44:57.105Z",
"statusCode": 200,
"headers": {
"content-length": 528,
"server": "GenieACS/1.2.3+20210121000910",
"soapserver": "GenieACS/1.2.3+20210121000910",
"content-type": "text/xml; charset=\"utf-8\"",
"set-cookie": "session=ea31d97ccf6832fb"
},
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<soap-env:Envelope xmlns:soap-enc=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:cwmp=\"urn:dslforum-org:cwmp-1-0\"><soap-env:Header><cwmp:ID soap-env:mustUnderstand=\"1\">wdsx50vq</cwmp:ID></soap-env:Header><soap-env:Body><cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse></soap-env:Body></soap-env:Envelope>"
}
]
-14
View File
@@ -1,14 +0,0 @@
import ava from "ava";
import * as yaml from "yaml";
import { stringify } from "../lib/common/yaml";
import testCases from "./yaml-tests.json";
ava("stringify", (t) => {
for (const testCase of testCases) {
let str = stringify(testCase);
if (str.startsWith(">2")) str = ">3" + str.slice(2);
if (str.startsWith("|2")) str = "|3" + str.slice(2);
t.deepEqual(yaml.parse(yaml.stringify(testCase)), yaml.parse(str));
}
});
+82
View File
@@ -0,0 +1,82 @@
/**
* 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 * as fs from "fs";
import * as path from "path";
import { connect, devicesCollection } from "../lib/db";
const outputFilename = path.resolve(`data_model_${process.argv[2]}.json`);
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
const dataModel = {};
function recursive(object, p = ""): void {
for (const key in object) {
if (key[0] === "_" || !object[key]) continue;
const isObject = object[key]["_object"];
const isValue = "_value" in object[key];
if ((isObject && isValue) || (!isObject && !isValue))
throw new Error("Unexpected object");
if (isValue) {
let value = object[key]["_value"];
if (typeof value === "boolean") value = +value;
const values = [object[key]["_writable"] || false, value.toString()];
if (object[key]["_type"]) values.push(object[key]["_type"]);
dataModel[`${p}${key}`] = values;
}
if (isObject) {
const values = [object[key]["_writable"] || false];
dataModel[`${p}${key}.`] = values;
recursive(object[key], `${p}${key}.`);
}
}
}
connect()
.then(() => {
devicesCollection.findOne(
{
_id: process.argv[2],
},
{},
(err, device) => {
if (err) throw err;
if (!device) throw new Error("Device not found");
recursive(device);
fs.writeFile(outputFilename, JSON.stringify(dataModel, null, 2), () => {
if (err) throw err;
process.stdout.write(`${outputFilename} saved\n`);
process.exit(0);
});
}
);
})
.catch((err) => {
throw err;
});
+9 -6
View File
@@ -1,6 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["esnext", "dom"], "lib": [
"esnext", "dom"
],
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"target": "esnext", "target": "esnext",
@@ -12,10 +14,11 @@
"allowJs": true "allowJs": true
}, },
"include": [ "include": [
"./bin/*.ts", "./bin/*.ts",
"./lib/**/*", "./lib/**/*",
"./ui/**/*", "./ui/**/*",
"./test/**/*", "./test/**/*",
"./build/**/*" "./build/**/*",
"./tools/*.ts"
] ]
} }
+19 -6
View File
@@ -64,37 +64,50 @@ const adminPages = [
let state; let state;
let lastRenderTimestamp = 0;
let pageVisitTimestamp = 0;
let fulfillTimeout = null;
function fulfill(): void {
clearTimeout(fulfillTimeout);
fulfillTimeout = setTimeout(() => {
store.fulfill(lastRenderTimestamp, pageVisitTimestamp, (updated) => {
if (updated) m.redraw();
});
}, 100);
}
function pagify(pageName, page): RouteResolver { function pagify(pageName, page): RouteResolver {
const component: RouteResolver = { const component: RouteResolver = {
render: () => { render: () => {
const lastRenderTimestamp = Date.now(); lastRenderTimestamp = Date.now();
let p; let p;
if (state && state.error) p = m(errorPage.component, state); if (state && state.error) p = m(errorPage.component, state);
else p = m(contextifyComponent(page.component), state); else p = m(contextifyComponent(page.component), state);
fulfill();
const attrs = {}; const attrs = {};
attrs["page"] = pageName; attrs["page"] = pageName;
attrs["oncreate"] = attrs["onupdate"] = () => {
store.fulfill(lastRenderTimestamp);
};
return m(layout, attrs, p); return m(layout, attrs, p);
}, },
onmatch: null, onmatch: null,
}; };
component.onmatch = (args, requestedPath) => { component.onmatch = (args, requestedPath) => {
store.setTimestamp(Date.now()); pageVisitTimestamp = Date.now();
if (!page.init) { if (!page.init) {
state = null; state = null;
fulfill();
return null; return null;
} }
return new Promise<void>((resolve) => { return new Promise((resolve) => {
page page
.init(args) .init(args)
.then((st) => { .then((st) => {
if (!st) return void m.route.set("/"); if (!st) return void m.route.set("/");
state = st; state = st;
resolve(); resolve();
fulfill();
}) })
.catch((err) => { .catch((err) => {
if (!window.username && err.message.indexOf("authorized") >= 0) { if (!window.username && err.message.indexOf("authorized") >= 0) {
+14 -17
View File
@@ -28,24 +28,21 @@ const component: ClosureComponent = (): Component => {
name: vnode.attrs["id"], name: vnode.attrs["id"],
value: vnode.attrs["value"], value: vnode.attrs["value"],
oncreate: (_vnode) => { oncreate: (_vnode) => {
const editor = codeMirror.fromTextArea( const editor = codeMirror.fromTextArea(_vnode.dom, {
_vnode.dom as HTMLTextAreaElement, mode: vnode.attrs["mode"],
{ lineNumbers: true,
mode: vnode.attrs["mode"], readOnly: vnode.attrs["readOnly"],
lineNumbers: true, extraKeys: {
readOnly: vnode.attrs["readOnly"], "Ctrl-Enter": () => {
extraKeys: { if (vnode.attrs["onSubmit"])
"Ctrl-Enter": () => { vnode.attrs["onSubmit"](_vnode.dom);
if (vnode.attrs["onSubmit"])
vnode.attrs["onSubmit"](_vnode.dom);
},
"Cmd-Enter": () => {
if (vnode.attrs["onSubmit"])
vnode.attrs["onSubmit"](_vnode.dom);
},
}, },
} "Cmd-Enter": () => {
); if (vnode.attrs["onSubmit"])
vnode.attrs["onSubmit"](_vnode.dom);
},
},
});
if (vnode.attrs["onChange"]) { if (vnode.attrs["onChange"]) {
editor.on("change", (e) => { editor.on("change", (e) => {
+5 -9
View File
@@ -39,7 +39,7 @@ import tags from "./components/tags";
import ping from "./components/ping"; import ping from "./components/ping";
import deviceLink from "./components/device-link"; import deviceLink from "./components/device-link";
import longTextComponent from "./long-text-component"; import longTextComponent from "./long-text-component";
import loading from "./components/loading"; import deviceUploads from "./components/device-uploads";
const comps = { const comps = {
parameter, parameter,
@@ -55,7 +55,7 @@ const comps = {
ping, ping,
"device-link": deviceLink, "device-link": deviceLink,
"long-text": longTextComponent, "long-text": longTextComponent,
loading: loading, "device-uploads": deviceUploads,
}; };
const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>(); const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
@@ -139,13 +139,9 @@ export function contextifyComponent(component: ComponentTypes): ComponentTypes {
const view = state.view; const view = state.view;
state.view = function (vnode) { state.view = function (vnode) {
const context = vnodeContext.get(vnode) || {}; const context = vnodeContext.get(vnode) || {};
try { const res = Reflect.apply(view, this, [vnode]);
const res = Reflect.apply(view, this, [vnode]); applyContext(res, context);
applyContext(res, context); return res;
return res;
} catch (err) {
return m("p.error", { title: err.message }, "Error!");
}
}; };
return state; return state;
}; };
+18 -24
View File
@@ -55,8 +55,6 @@ const component: ClosureComponent = (): Component => {
view: (vnode) => { view: (vnode) => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const limit = vnode.attrs["limit"] > 0 ? vnode.attrs["limit"] : 100;
const search = m("input", { const search = m("input", {
type: "text", type: "text",
placeholder: "Search parameters", placeholder: "Search parameters",
@@ -88,7 +86,7 @@ const component: ClosureComponent = (): Component => {
const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k; const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k;
if (re && !re.test(str)) continue; if (re && !re.test(str)) continue;
++c; ++c;
if (count < limit) filteredKeys.push(k); if (count < 100) filteredKeys.push(k);
} }
count += c; count += c;
} }
@@ -171,29 +169,25 @@ const component: ClosureComponent = (): Component => {
}); });
return m( return m(
"loading", ".all-parameters",
{ queries: [vnode.attrs["deviceQuery"]] },
m( m(
".all-parameters", "a.download-csv",
{
href: `api/devices/${encodeURIComponent(
device["DeviceID.ID"].value[0]
)}.csv`,
download: "",
style: "float: right;",
},
"Download"
),
search,
m(
".parameter-list",
m("table", m("tbody", rows)),
m( m(
"a.download-csv", "m",
{ `Displaying ${filteredKeys.length} out of ${count} parameters.`
href: `api/devices/${encodeURIComponent(
device["DeviceID.ID"].value[0]
)}.csv`,
download: "",
style: "float: right;",
},
"Download"
),
search,
m(
".parameter-list",
m("table", m("tbody", rows)),
m(
"m",
`Displaying ${filteredKeys.length} out of ${count} parameters.`
)
) )
) )
); );
+25
View File
@@ -22,6 +22,7 @@ import { m } from "../components";
import * as taskQueue from "../task-queue"; import * as taskQueue from "../task-queue";
import * as notifications from "../notifications"; import * as notifications from "../notifications";
import * as store from "../store"; import * as store from "../store";
import * as overlay from "../overlay";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
@@ -78,6 +79,30 @@ const component: ClosureComponent = (): Component => {
) )
); );
buttons.push(
m(
"button.primary",
{
title: "Fetch files from device",
onclick: () => {
const deviceId = device["DeviceID.ID"].value[0];
overlay.open(() => {
const dev = store.fetch("devices", [
"=",
["PARAM", "DeviceID.ID"],
deviceId,
]);
return [
m("h3", `Uploads from ${deviceId}`),
m("device-uploads", { device: dev.value[0] }),
];
});
},
},
"Uploads"
)
);
buttons.push( buttons.push(
m( m(
"button.primary", "button.primary",
+4 -12
View File
@@ -22,7 +22,6 @@ import { m } from "../components";
import * as store from "../store"; import * as store from "../store";
import * as notifications from "../notifications"; import * as notifications from "../notifications";
import { getIcon } from "../icons"; import { getIcon } from "../icons";
import { stringify } from "../../lib/common/yaml";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
@@ -39,7 +38,6 @@ const component: ClosureComponent = (): Component => {
"Channel", "Channel",
"Code", "Code",
"Message", "Message",
"Detail",
"Retries", "Retries",
"Timestamp", "Timestamp",
].map((l) => m("th", l)); ].map((l) => m("th", l));
@@ -50,9 +48,7 @@ const component: ClosureComponent = (): Component => {
rows.push([ rows.push([
m("td", f["channel"]), m("td", f["channel"]),
m("td", f["code"]), m("td", f["code"]),
m("td", m("long-text", { text: f["message"] })), m("td", f["message"]),
m("td", m("long-text", { text: stringify(f["detail"]) })),
m("td", f["retries"]), m("td", f["retries"]),
m("td", new Date(f["timestamp"]).toLocaleString()), m("td", new Date(f["timestamp"]).toLocaleString()),
m( m(
@@ -67,12 +63,12 @@ const component: ClosureComponent = (): Component => {
.deleteResource("faults", f["_id"]) .deleteResource("faults", f["_id"])
.then(() => { .then(() => {
notifications.push("success", "Fault deleted"); notifications.push("success", "Fault deleted");
store.setTimestamp(Date.now()); store.fulfill(Date.now(), Date.now());
m.redraw(); m.redraw();
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
store.setTimestamp(Date.now()); store.fulfill(Date.now(), Date.now());
}); });
}, },
}, },
@@ -95,11 +91,7 @@ const component: ClosureComponent = (): Component => {
); );
} }
return m( return m("table.table", thead, tbody);
"loading",
{ queries: [faults] },
m("table.table", thead, tbody)
);
}, },
}; };
}; };
+155
View File
@@ -0,0 +1,155 @@
/**
* Copyright 2013-2020 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 { ClosureComponent, Component } from "mithril";
import { m } from "../components";
import * as store from "../store";
import * as notifications from "../notifications";
import { getIcon } from "../icons";
import * as taskQueue from "../task-queue";
const component: ClosureComponent = (): Component => {
return {
view: (vnode) => {
const device = vnode.attrs["device"];
const deviceId = device["DeviceID.ID"].value[0];
const files = store.fetch("uploads", [
"AND",
[">", ["PARAM", "_id"], `${deviceId}/`],
["<", ["PARAM", "_id"], `${deviceId}/zzzz`],
]);
const uploads: Record<string, any> = {};
for (const [k, v] of Object.entries(device)) {
if (k.startsWith("Uploads.")) {
const parts = k.split(".");
if (parts.length !== 3) continue;
uploads[parts[1]] = uploads[parts[1]] || {};
uploads[parts[1]][parts[2]] = v["value"][0];
}
}
const headers = ["Filename", "Type", "Timestamp", "Status"].map((l) =>
m("th", l)
);
const thead = m("thead", m("tr", headers));
let minTime = 999999;
const timestamp = store.getTimestamp();
const rows = [];
const uploadsSorted = Object.values(uploads).sort(
(a, b) => a["Upload"] - b["Upload"]
);
for (const u of Object.values(uploadsSorted)) {
if (!u["Upload"]) continue;
const ready = u["LastUpload"] >= u["Upload"];
if (!ready) {
minTime = Math.min(minTime, u["Upload"] - timestamp);
rows.push([
m("td", u["FileName"]),
m("td", u["FileType"]),
m("td", new Date(u["Upload"]).toLocaleString()),
m("td", "Waiting for upload"),
]);
} else {
const filePath = `${deviceId}/${u["FileName"]}`;
if (!Array.isArray(files.value)) continue;
if (!files.value.some((f) => f["_id"] === filePath)) continue;
const deleteButton = m(
"button",
{
title: "Delete file",
onclick: (e) => {
e.redraw = false;
store
.deleteResource("uploads", filePath)
.then(() => {
notifications.push("success", "File deleted");
store.fulfill(Date.now(), Date.now());
m.redraw();
})
.catch((err) => {
notifications.push("error", err.message);
store.fulfill(Date.now(), Date.now());
});
},
},
getIcon("delete-instance")
);
rows.push([
m("td", u["FileName"]),
m("td", u["FileType"]),
m("td", new Date(u["LastUpload"]).toLocaleString()),
m(
"td",
m(
"a",
{
href: `api/uploads/blob/${encodeURIComponent(filePath)}`,
},
"Ready"
)
),
m("td", deleteButton),
]);
}
}
if (minTime < 60000) {
setTimeout(() => {
store.fulfill(0, timestamp + 5000);
}, 10000);
}
const addButton = m(
"button",
{
title: "Fetch a new file",
onclick: () => {
taskQueue.stageUpload({
name: "upload",
devices: [deviceId],
});
},
},
getIcon("add-instance")
);
const empty = !rows.length;
rows.push([m("td", { colspan: headers.length }), m("td", addButton)]);
const tbody = m(
"tbody",
rows.map((r) => m("tr", r)),
empty
? m("tr.empty", m("td", { colspan: headers.length }, "No uploads"))
: null
);
return m("table.table", thead, tbody);
},
};
};
export default component;
-80
View File
@@ -1,80 +0,0 @@
/**
* Copyright 2013-2021 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 { VnodeDOM, ClosureComponent, Component } from "mithril";
const component: ClosureComponent = (): Component => {
let overlay: HTMLElement;
let dom: Element;
let loading = false;
function apply(vnode: VnodeDOM): void {
if (!loading) {
if (overlay) overlay.parentElement.remove();
if (dom) dom.classList.remove("loading");
overlay = null;
dom = null;
return;
}
if (dom && dom !== vnode.dom) dom.classList.remove("loading");
dom = vnode.dom;
dom.classList.add("loading");
if (!overlay) {
const wrapper = document.createElement("div");
wrapper.style.position = "relative";
wrapper.style.pointerEvents = "none";
overlay = document.createElement("div");
overlay.classList.add("loading-overlay");
overlay.style.position = "absolute";
wrapper.appendChild(overlay);
}
const wrapper = overlay.parentElement;
if (wrapper.parentElement !== dom.parentElement)
dom.parentNode.appendChild(wrapper);
const wrapperRect = wrapper.getBoundingClientRect();
const domRect = dom.getBoundingClientRect();
overlay.style.width = `${dom.scrollWidth}px`;
overlay.style.height = `${dom.scrollHeight}px`;
overlay.style.left = `${domRect.left - wrapperRect.left}px`;
overlay.style.top = `${domRect.top - wrapperRect.top}px`;
}
return {
view: (vnode) => {
const queries = vnode.attrs["queries"];
loading = queries.some((q) => q.fulfilling);
return vnode.children;
},
oncreate: apply,
onupdate: apply,
onremove: () => {
if (overlay) overlay.parentElement.remove();
if (dom) dom.classList.remove("loading");
overlay = null;
dom = null;
},
};
};
export default component;
+1 -10
View File
@@ -38,11 +38,6 @@ const component: ClosureComponent = (): Component => {
return m( return m(
"tr", "tr",
{ {
oncreate: (vn) => {
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
? ""
: "none";
},
onupdate: (vn) => { onupdate: (vn) => {
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom (vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
? "" ? ""
@@ -54,11 +49,7 @@ const component: ClosureComponent = (): Component => {
); );
}); });
return m( return m("table.parameter-list", rows);
"loading",
{ queries: [vnode.attrs["deviceQuery"]] },
m("table.parameter-list", rows)
);
}, },
}; };
}; };
+1 -8
View File
@@ -149,14 +149,7 @@ const component: ClosureComponent = (): Component => {
let label; let label;
if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]); if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
return [ return [label, m("table.table", thead, m("tbody", rows))];
label,
m(
"loading",
{ queries: [vnode.attrs["deviceQuery"]] },
m("table.table", thead, m("tbody", rows))
),
];
}, },
}; };
}; };
-2
View File
@@ -62,8 +62,6 @@ const evaluateParam = memoize((exp, obj, now: number) => {
timestamp = p.valueTimestamp; timestamp = p.valueTimestamp;
value = p.value[0]; value = p.value[0];
parameter = exp[1]; parameter = exp[1];
if (p.value[1] === "xsd:dateTime" && typeof value === "number")
value = new Date(value).toLocaleString();
} }
} }
+2 -4
View File
@@ -63,11 +63,9 @@ const component: ClosureComponent = (vn): Component => {
param = device["Device.ManagementServer.ConnectionRequestURL"]; param = device["Device.ManagementServer.ConnectionRequestURL"];
let h; let h;
try { if (param && param.value) {
const url = new URL(param.value[0]); const url = new URL(param.value[0]);
h = url.hostname; h = url.hostname;
} catch (err) {
// Ignore
} }
if (host !== h) { if (host !== h) {
@@ -79,7 +77,7 @@ const component: ClosureComponent = (vn): Component => {
} }
} }
return m("div", host ? `Pinging ${host}:` : ""); return m("div", `Pinging ${host}:`);
}, },
}; };
}; };
+1 -1
View File
@@ -78,7 +78,7 @@ const component: ClosureComponent = (): Component => {
) )
.then(() => { .then(() => {
e.target.disabled = false; e.target.disabled = false;
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
e.target.disabled = false; e.target.disabled = false;
+2 -2
View File
@@ -64,7 +64,7 @@ const component: ClosureComponent = (): Component => {
"success", "success",
`${deviceId}: Tags updated` `${deviceId}: Tags updated`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
e.target.disabled = false; e.target.disabled = false;
@@ -99,7 +99,7 @@ const component: ClosureComponent = (): Component => {
.then(() => { .then(() => {
e.target.disabled = false; e.target.disabled = false;
notifications.push("success", `${deviceId}: Tags updated`); notifications.push("success", `${deviceId}: Tags updated`);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
e.target.disabled = false; e.target.disabled = false;
+20 -24
View File
@@ -60,12 +60,12 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
.resourceExists("config", id) .resourceExists("config", id)
.then((exists) => { .then((exists) => {
if (exists && isNew) { if (exists && isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "Config already exists" }); return void resolve({ _id: "Config already exists" });
} }
if (!exists && !isNew) { if (!exists && !isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "Config does not exist" }); return void resolve({ _id: "Config does not exist" });
} }
@@ -76,8 +76,8 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
"success", "success",
`Config ${exists ? "updated" : "created"}` `Config ${exists ? "updated" : "created"}`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch(reject); .catch(reject);
}) })
@@ -87,11 +87,11 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
.deleteResource("config", object["_id"]) .deleteResource("config", object["_id"])
.then(() => { .then(() => {
notifications.push("success", "Config deleted"); notifications.push("success", "Config deleted");
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch((err) => { .catch((err) => {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
reject(err); reject(err);
}); });
} else { } else {
@@ -164,11 +164,11 @@ function renderTable(confsResponse, searchString): Children {
} else { } else {
overlay.close(cb); overlay.close(cb);
} }
resolve(null); resolve();
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
resolve(null); resolve();
}); });
}); });
}, },
@@ -255,7 +255,7 @@ export const component: ClosureComponent = (): Component => {
Object.assign( Object.assign(
{ {
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object, true) putActionHandler(action, object, true)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -267,7 +267,7 @@ export const component: ClosureComponent = (): Component => {
} else { } else {
overlay.close(cb); overlay.close(cb);
} }
resolve(null); resolve();
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
@@ -337,11 +337,11 @@ export const component: ClosureComponent = (): Component => {
); );
overlay.close(cb); overlay.close(cb);
} }
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}, },
onError: (err) => { onError: (err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
overlay.close(cb); overlay.close(cb);
}, },
}, },
@@ -366,18 +366,14 @@ export const component: ClosureComponent = (): Component => {
return [ return [
m("h1", "Listing config"), m("h1", "Listing config"),
m( m(
"loading", ".all-parameters",
{ queries: [confs] }, search,
m( m(
".all-parameters", ".parameter-list",
search, { style: "height: 400px" },
m( renderTable(confs, vnode.state["searchString"])
".parameter-list", ),
{ style: "height: 400px" }, m(".actions-bar", [newConfig].concat(subs))
renderTable(confs, vnode.state["searchString"])
),
m(".actions-bar", [newConfig].concat(subs))
)
), ),
]; ];
}, },
+5 -45
View File
@@ -46,7 +46,6 @@ button,
select, select,
option, option,
textarea { textarea {
outline-color: var(--color4);
font-family: sans-serif; font-family: sans-serif;
color: var(--color5); color: var(--color5);
} }
@@ -107,7 +106,7 @@ body {
& > .drawer-wrapper { & > .drawer-wrapper {
position: fixed; position: fixed;
z-index: 10; z-index: 101;
width: 720px; width: 720px;
margin-left: -360px; margin-left: -360px;
top: 0; top: 0;
@@ -286,21 +285,9 @@ body {
} }
& > .logo { & > .logo {
display: inline-block; margin: 10px;
position: relative; display: inline;
& > img { height: 56px;
margin: 10px;
height: 56px;
}
& > .version {
position: absolute;
bottom: 5px;
right: 10px;
font-family: monospace;
font-size: 10px;
color: #666;
}
} }
& > .user-menu { & > .user-menu {
@@ -347,6 +334,7 @@ body {
table.table { table.table {
border-spacing: 0; border-spacing: 0;
text-align: left; text-align: left;
display: block;
overflow-x: auto; overflow-x: auto;
& table.table { & table.table {
@@ -752,31 +740,3 @@ span.parameter-value {
opacity: 0.3; opacity: 0.3;
} }
} }
.loading {
opacity: 0.6;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loading-overlay {
display: flex;
align-items: center;
justify-content: center;
&::after {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTEwIDUwYTQwIDQwIDAgMDA4MCAwIDQwIDQyIDAgMDEtODAgMCIgZmlsbD0iI2IxMmQ1YyIgLz48L3N2Zz4=);
content: "";
width: 48px;
height: 48px;
animation: spin 1s linear infinite;
background-repeat: no-repeat;
background-size: 100% 100%;
}
}
+4 -16
View File
@@ -42,25 +42,13 @@ export const component: ClosureComponent = (): Component => {
view: (vnode) => { view: (vnode) => {
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`; document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]); const dev = store.fetch("devices", vnode.attrs["deviceFilter"]).value;
if (!dev.value.length) { if (!dev.length) return "Loading";
if (!dev.fulfilling)
return m("p.error", `No such device ${vnode.attrs["deviceId"]}`);
return m(
"loading",
{ queries: [dev] },
m("div", { style: "height: 100px;" })
);
}
const conf = config.ui.device; const conf = config.ui.device;
const cmps = []; const cmps = [];
for (const c of Object.values(conf)) { for (const c of Object.values(conf))
cmps.push( cmps.push(m.context({ device: dev[0] }, c["type"], c));
m.context({ device: dev.value[0], deviceQuery: dev }, c["type"], c)
);
}
return [m("h1", vnode.attrs["deviceId"]), cmps]; return [m("h1", vnode.attrs["deviceId"]), cmps];
}, },
+10 -10
View File
@@ -154,14 +154,14 @@ function renderActions(selected: Set<string>): Children {
.deleteResource("devices", id) .deleteResource("devices", id)
.then(() => { .then(() => {
notifications.push("success", `${id}: Deleted`); notifications.push("success", `${id}: Deleted`);
if (--counter === 0) store.setTimestamp(Date.now()); if (--counter === 0) store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", `${id}: ${err.message}`); notifications.push("error", `${id}: ${err.message}`);
if (--counter === 0) store.setTimestamp(Date.now()); if (--counter === 0) store.fulfill(0, Date.now());
}); });
} }
if (--counter === 0) store.setTimestamp(Date.now()); if (--counter === 0) store.fulfill(0, Date.now());
}, },
}, },
"Delete" "Delete"
@@ -186,14 +186,14 @@ function renderActions(selected: Set<string>): Children {
.updateTags(id, { [tag]: true }) .updateTags(id, { [tag]: true })
.then(() => { .then(() => {
notifications.push("success", `${id}: Tags updated`); notifications.push("success", `${id}: Tags updated`);
if (--counter === 0) store.setTimestamp(Date.now()); if (--counter === 0) store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", `${id}: ${err.message}`); notifications.push("error", `${id}: ${err.message}`);
if (--counter === 0) store.setTimestamp(Date.now()); if (--counter === 0) store.fulfill(0, Date.now());
}); });
} }
if (--counter === 0) store.setTimestamp(Date.now()); if (--counter === 0) store.fulfill(0, Date.now());
}, },
}, },
"Tag" "Tag"
@@ -220,14 +220,14 @@ function renderActions(selected: Set<string>): Children {
.updateTags(id, { [tag]: false }) .updateTags(id, { [tag]: false })
.then(() => { .then(() => {
notifications.push("success", `${id}: Tags updated`); notifications.push("success", `${id}: Tags updated`);
if (--counter === 0) store.setTimestamp(Date.now()); if (--counter === 0) store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", `${id}: ${err.message}`); notifications.push("error", `${id}: ${err.message}`);
if (--counter === 0) store.setTimestamp(Date.now()); if (--counter === 0) store.fulfill(0, Date.now());
}); });
} }
if (--counter === 0) store.setTimestamp(Date.now()); if (--counter === 0) store.fulfill(0, Date.now());
}, },
}, },
"Untag" "Untag"
@@ -336,7 +336,7 @@ export const component: ClosureComponent = (): Component => {
return [ return [
m("h1", "Listing devices"), m("h1", "Listing devices"),
m(filterComponent, filterAttrs), m(filterComponent, filterAttrs),
m("loading", { queries: [devs, count] }, m(indexTableComponent, attrs)), m(indexTableComponent, attrs),
]; ];
}, },
}; };
+133 -8
View File
@@ -76,13 +76,13 @@ function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
] ]
); );
} else { } else {
const type = task.parameterValues[0][2];
let value = task.parameterValues[0][1];
if (type === "xsd:dateTime" && typeof value === "number")
value = new Date(value).toJSON() || value;
input = m("input", { input = m("input", {
type: ["xsd:int", "xsd:unsignedInt"].includes(type) ? "number" : "text", type: ["xsd:dateTime", "xsd:int", "xsd:unsignedInt"].includes(
value: value, task.parameterValues[0][2]
)
? "number"
: "text",
value: task.parameterValues[0][1],
oninput: (e) => { oninput: (e) => {
e.redraw = false; e.redraw = false;
task.parameterValues[0][1] = input.dom.value; task.parameterValues[0][1] = input.dom.value;
@@ -184,6 +184,122 @@ function renderStagingDownload(task: StageTask): Children {
]; ];
} }
const uploadTypesForDevice: WeakMap<
Record<string, unknown>,
Record<string, string>
> = new WeakMap();
function getUploadTypesForDevice(
device: Record<string, unknown>
): Record<string, string> {
if (uploadTypesForDevice.has(device)) return uploadTypesForDevice.get(device);
const instances: Set<string> = new Set();
const filenames: Record<string, string> = {};
const versions: Record<string, string> = {};
if (device) {
for (const [k, v] of Object.entries(device)) {
if (/DeviceInfo\.VendorConfigFile\.[0-9]+(\.Name|\.Version)?$/.test(k)) {
const p = k.split(".");
if (p[p.length - 1] === "Name") {
const n = `3 Vendor Configuration File ${p[p.length - 2]}`;
instances.add(n);
if (v["value"]) filenames[n] = v["value"][0];
} else if (p[p.length - 1] === "Version") {
const n = `3 Vendor Configuration File ${p[p.length - 2]}`;
instances.add(n);
if (v["value"]) versions[n] = v["value"][0];
} else {
const n = `3 Vendor Configuration File ${p[p.length - 1]}`;
instances.add(n);
}
} else if (/DeviceInfo\.VendorLogFile\.[0-9]+(\.Name)?$/.test(k)) {
const p = k.split(".");
if (p[p.length - 1] === "Name") {
const n = `4 Vendor Log File ${p[p.length - 2]}`;
instances.add(n);
if (v["value"]) filenames[n] = v["value"][0];
} else {
const n = `4 Vendor Log File ${p[p.length - 1]}`;
instances.add(n);
}
}
}
}
if (!instances.size) {
instances.add("1 Vendor Configuration File");
instances.add("2 Vendor Log File");
}
const res: Record<string, string> = {};
for (const i of instances) {
let n = filenames[i];
const v = versions[i];
if (!n) n = i.replace(/\s+/g, "_") + ".txt";
if (v) n = `${n}-${v}`;
res[i] = n;
}
uploadTypesForDevice.set(device, res);
return res;
}
function renderStagingUpload(task: StageTask): Children {
if (!task.fileName || !task.fileType) invalid.add(task);
else invalid.delete(task);
const commonTypes: Record<string, number> = {};
const filenames: Record<string, string> = {};
for (const d of task.devices) {
const dev = store.fetch("devices", ["=", ["PARAM", "DeviceID.ID"], d])
.value[0];
if (dev) {
const u = getUploadTypesForDevice(dev);
Object.assign(filenames, u);
for (const k of Object.keys(u))
commonTypes[k] = (commonTypes[k] || 0) + 1;
}
}
const typesList = [""];
typesList.push(
...Object.entries(commonTypes)
.filter(([, c]) => c === task.devices.length)
.map(([k]) => k)
.sort()
);
return [
"Fetch ",
m(
"select",
{
onchange: (e) => {
task.fileType = e.target.value;
task.fileName = filenames[task.fileType];
},
},
typesList.map((t) =>
m(
"option",
{ disabled: !t, value: t, selected: (task.fileType || "") === t },
t
)
)
),
" as ",
m("input", {
onchange: (e) => {
task.fileName = e.target.value;
},
value: task.fileName || "",
}),
];
}
function renderStaging(staging: Set<StageTask>): Child[] { function renderStaging(staging: Set<StageTask>): Child[] {
const elements: Child[] = []; const elements: Child[] = [];
@@ -204,6 +320,7 @@ function renderStaging(staging: Set<StageTask>): Child[] {
if (s.name === "setParameterValues") if (s.name === "setParameterValues")
elms = renderStagingSpv(s, queueFunc, cancelFunc); elms = renderStagingSpv(s, queueFunc, cancelFunc);
else if (s.name === "download") elms = renderStagingDownload(s); else if (s.name === "download") elms = renderStagingDownload(s);
else if (s.name === "upload") elms = renderStagingUpload(s);
const queue = m( const queue = m(
"button.primary", "button.primary",
@@ -323,6 +440,14 @@ function renderQueue(queue: Set<QueueTask>): Child[] {
m(".actions", actions) m(".actions", actions)
) )
); );
} else if (t.name === "upload") {
details.push(
m(
`div.${t.status}`,
`Fetch file ${t.fileName} (${t.fileType})`,
m(".actions", actions)
)
);
} else { } else {
details.push(m(`div.${t.status}`, t.name, m(".actions", actions))); details.push(m(`div.${t.status}`, t.name, m(".actions", actions)));
} }
@@ -355,7 +480,7 @@ function renderNotifications(notifs): Child[] {
}, },
onbeforeremove: (vnode) => { onbeforeremove: (vnode) => {
(vnode.dom as HTMLDivElement).style.opacity = "0"; (vnode.dom as HTMLDivElement).style.opacity = "0";
return new Promise<void>((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve(); resolve();
}, 500); }, 500);
@@ -465,7 +590,7 @@ const component: ClosureComponent = (): Component => {
} }
) )
.then(() => { .then(() => {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
+8 -6
View File
@@ -19,8 +19,8 @@
import * as notifications from "./notifications"; import * as notifications from "./notifications";
export let codeMirror: typeof import("codemirror"); export let codeMirror;
export let yaml: typeof import("yaml"); export let yaml;
let note; let note;
@@ -43,9 +43,11 @@ export function loadCodeMirror(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const promises = [ const promises = [
import("codemirror"), import(/* webpackChunkName: "codemirror" */ "codemirror"),
import("codemirror/mode/javascript/javascript"), import(
import("codemirror/mode/yaml/yaml"), /* webpackChunkName: "codemirror" */ "codemirror/mode/javascript/javascript"
),
import(/* webpackChunkName: "codemirror" */ "codemirror/mode/yaml/yaml"),
]; ];
Promise.all(promises) Promise.all(promises)
.then((modules) => { .then((modules) => {
@@ -63,7 +65,7 @@ export function loadYaml(): Promise<void> {
if (yaml) return Promise.resolve(); if (yaml) return Promise.resolve();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
import("yaml") import(/* webpackChunkName: "yaml" */ "yaml")
.then((module) => { .then((module) => {
yaml = module; yaml = module;
resolve(); resolve();
+12 -13
View File
@@ -27,7 +27,7 @@ import * as notifications from "./notifications";
import memoize from "../lib/common/memoize"; import memoize from "../lib/common/memoize";
import * as smartQuery from "./smart-query"; import * as smartQuery from "./smart-query";
import { map, parse, stringify } from "../lib/common/expression-parser"; import { map, parse, stringify } from "../lib/common/expression-parser";
import { stringify as yamlStringify } from "../lib/common/yaml"; import { loadYaml, yaml } from "./dynamic-loader";
const PAGE_SIZE = config.ui.pageSize || 10; const PAGE_SIZE = config.ui.pageSize || 10;
@@ -76,7 +76,13 @@ export function init(
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : ""; const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : ""; const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
return Promise.resolve({ filter, sort }); return new Promise((resolve, reject) => {
loadYaml()
.then(() => {
resolve({ filter, sort });
})
.catch(reject);
});
} }
export const component: ClosureComponent = (): Component => { export const component: ClosureComponent = (): Component => {
@@ -141,11 +147,8 @@ export const component: ClosureComponent = (): Component => {
return m("a", { href: deviceHref }, fault["device"]); return m("a", { href: deviceHref }, fault["device"]);
} }
if (attr.id === "message")
return m("long-text", { text: fault["message"] });
if (attr.id === "detail") if (attr.id === "detail")
return m("long-text", { text: yamlStringify(fault["detail"]) }); return m("long-text", { text: yaml.stringify(fault["detail"]) });
if (attr.id === "timestamp") if (attr.id === "timestamp")
return new Date(fault["timestamp"]).toLocaleString(); return new Date(fault["timestamp"]).toLocaleString();
@@ -187,11 +190,11 @@ export const component: ClosureComponent = (): Component => {
"success", "success",
`${res.length} faults deleted` `${res.length} faults deleted`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}); });
}, },
}, },
@@ -208,11 +211,7 @@ export const component: ClosureComponent = (): Component => {
return [ return [
m("h1", "Listing faults"), m("h1", "Listing faults"),
m(filterComponent, filterAttrs), m(filterComponent, filterAttrs),
m( m(indexTableComponent, attrs),
"loading",
{ queries: [faults, count] },
m(indexTableComponent, attrs)
),
]; ];
}, },
}; };
+7 -14
View File
@@ -93,7 +93,7 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
.resourceExists("files", id) .resourceExists("files", id)
.then((exists) => { .then((exists) => {
if (exists) { if (exists) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ file: "File already exists" }); return void resolve({ file: "File already exists" });
} }
const headers = Object.assign( const headers = Object.assign(
@@ -117,8 +117,8 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
"success", "success",
`File ${exists ? "updated" : "created"}` `File ${exists ? "updated" : "created"}`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch(reject); .catch(reject);
}) })
@@ -212,9 +212,6 @@ export const component: ClosureComponent = (): Component => {
attrs["sortAttributes"] = sortAttributes; attrs["sortAttributes"] = sortAttributes;
attrs["onSortChange"] = onSortChange; attrs["onSortChange"] = onSortChange;
attrs["downloadUrl"] = downloadUrl; attrs["downloadUrl"] = downloadUrl;
attrs["recordActionsCallback"] = (file) => {
return [m("a", { href: "api/blob/files/" + file["_id"] }, "Download")];
};
if (window.authorizer.hasAccess("files", 3)) { if (window.authorizer.hasAccess("files", 3)) {
attrs["actionsCallback"] = (selected: Set<string>): Children => { attrs["actionsCallback"] = (selected: Set<string>): Children => {
@@ -230,7 +227,7 @@ export const component: ClosureComponent = (): Component => {
Object.assign( Object.assign(
{ {
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object) putActionHandler(action, object)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -288,11 +285,11 @@ export const component: ClosureComponent = (): Component => {
"success", "success",
`${res.length} files deleted` `${res.length} files deleted`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}); });
}, },
}, },
@@ -310,11 +307,7 @@ export const component: ClosureComponent = (): Component => {
return [ return [
m("h1", "Listing files"), m("h1", "Listing files"),
m(filterComponent, filterAttrs), m(filterComponent, filterAttrs),
m( m(indexTableComponent, attrs),
"loading",
{ queries: [files, count] },
m(indexTableComponent, attrs)
),
]; ];
}, },
}; };
+1 -6
View File
@@ -23,7 +23,6 @@ import drawerComponent from "./drawer-component";
import userMenu from "./user-menu"; import userMenu from "./user-menu";
import adminMenu from "./admin-menu"; import adminMenu from "./admin-menu";
import * as overlay from "./overlay"; import * as overlay from "./overlay";
import { version as VERSION } from "../package.json";
const adminPages = [ const adminPages = [
"presets", "presets",
@@ -52,11 +51,7 @@ const component: ClosureComponent = (): Component => {
return [ return [
m("#header", [ m("#header", [
m( m("img.logo", { src: "logo.svg" }),
"div.logo",
m("img", { src: "logo.svg" }),
m("span.version", "v" + VERSION)
),
m(userMenu), m(userMenu),
m(menu, attrs), m(menu, attrs),
m(drawerComponent), m(drawerComponent),
+1 -1
View File
@@ -60,7 +60,7 @@ export function render(): Children {
}, },
onbeforeremove: (vnode) => { onbeforeremove: (vnode) => {
(vnode.dom as HTMLDivElement).style.opacity = "0"; (vnode.dom as HTMLDivElement).style.opacity = "0";
return new Promise<void>((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve(); resolve();
}, 500); }, 500);
+11 -14
View File
@@ -47,6 +47,7 @@ const attributes = [
"devices", "devices",
"faults", "faults",
"files", "files",
"uploads",
"permissions", "permissions",
"users", "users",
"presets", "presets",
@@ -117,7 +118,7 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
.resourceExists("permissions", id) .resourceExists("permissions", id)
.then((exists) => { .then((exists) => {
if (exists) { if (exists) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "Permission already exists" }); return void resolve({ _id: "Permission already exists" });
} }
@@ -125,8 +126,8 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
.putResource("permissions", id, object) .putResource("permissions", id, object)
.then(() => { .then(() => {
notifications.push("success", "Permission created"); notifications.push("success", "Permission created");
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch(reject); .catch(reject);
}) })
@@ -136,11 +137,11 @@ function putActionHandler(action, _object): Promise<ValidationErrors> {
.deleteResource("permissions", object["_id"]) .deleteResource("permissions", object["_id"])
.then(() => { .then(() => {
notifications.push("success", "Permission deleted"); notifications.push("success", "Permission deleted");
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch((err) => { .catch((err) => {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
reject(err); reject(err);
}); });
} else { } else {
@@ -291,7 +292,7 @@ export const component: ClosureComponent = (): Component => {
Object.assign( Object.assign(
{ {
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object) putActionHandler(action, object)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -351,11 +352,11 @@ export const component: ClosureComponent = (): Component => {
"success", "success",
`${res.length} permissions deleted` `${res.length} permissions deleted`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}); });
}, },
}, },
@@ -373,11 +374,7 @@ export const component: ClosureComponent = (): Component => {
return [ return [
m("h1", "Listing permissions"), m("h1", "Listing permissions"),
m(filterComponent, filterAttrs), m(filterComponent, filterAttrs),
m( m(indexTableComponent, attrs),
"loading",
{ queries: [permissions, count] },
m(indexTableComponent, attrs)
),
]; ];
}, },
}; };
+15 -21
View File
@@ -116,27 +116,21 @@ function drawChart(chartData): Children {
legend.push(m("span.legend-total", `Total: ${total}`)); legend.push(m("span.legend-total", `Total: ${total}`));
return m( return m("div", { class: "pie-chart" }, [
"loading", m(
{ "svg",
queries: Object.values(chartData.slices).map((s) => s["count"]), {
}, // Adding 2 as padding; strokes must not be more than 2
m("div", { class: "pie-chart" }, [ viewBox: "-102 -102 204 204",
m( width: "204px",
"svg", height: "204px",
{ xmlns: "http://www.w3.org/2000/svg",
// Adding 2 as padding; strokes must not be more than 2 "xmlns:xlink": "http://www.w3.org/1999/xlink",
viewBox: "-102 -102 204 204", },
width: "204px", paths.concat(links)
height: "204px", ),
xmlns: "http://www.w3.org/2000/svg", m(".legend", legend),
"xmlns:xlink": "http://www.w3.org/1999/xlink", ]);
},
paths.concat(links)
),
m(".legend", legend),
])
);
} }
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
+12 -16
View File
@@ -86,12 +86,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.resourceExists("presets", id) .resourceExists("presets", id)
.then((exists) => { .then((exists) => {
if (exists && isNew) { if (exists && isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "Preset already exists" }); return void resolve({ _id: "Preset already exists" });
} }
if (!exists && !isNew) { if (!exists && !isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "Preset does not exist" }); return void resolve({ _id: "Preset does not exist" });
} }
@@ -102,8 +102,8 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
"success", "success",
`Preset ${exists ? "updated" : "created"}` `Preset ${exists ? "updated" : "created"}`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch(reject); .catch(reject);
}) })
@@ -113,12 +113,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.deleteResource("presets", object["_id"]) .deleteResource("presets", object["_id"])
.then(() => { .then(() => {
notifications.push("success", "Preset deleted"); notifications.push("success", "Preset deleted");
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch((err) => { .catch((err) => {
reject(err); reject(err);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}); });
} else { } else {
reject(new Error("Undefined action")); reject(new Error("Undefined action"));
@@ -294,7 +294,7 @@ export const component: ClosureComponent = (): Component => {
{ {
base: preset, base: preset,
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object, false) putActionHandler(action, object, false)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -355,7 +355,7 @@ export const component: ClosureComponent = (): Component => {
Object.assign( Object.assign(
{ {
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object, true) putActionHandler(action, object, true)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -413,11 +413,11 @@ export const component: ClosureComponent = (): Component => {
"success", "success",
`${res.length} presets deleted` `${res.length} presets deleted`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}); });
}, },
}, },
@@ -435,11 +435,7 @@ export const component: ClosureComponent = (): Component => {
return [ return [
m("h1", "Listing presets"), m("h1", "Listing presets"),
m(filterComponent, filterAttrs), m(filterComponent, filterAttrs),
m( m(indexTableComponent, attrs),
"loading",
{ queries: [presets, count] },
m(indexTableComponent, attrs)
),
]; ];
}, },
}; };
+13 -23
View File
@@ -66,12 +66,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.resourceExists("provisions", id) .resourceExists("provisions", id)
.then((exists) => { .then((exists) => {
if (exists && isNew) { if (exists && isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "Provision already exists" }); return void resolve({ _id: "Provision already exists" });
} }
if (!exists && !isNew) { if (!exists && !isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "Provision does not exist" }); return void resolve({ _id: "Provision does not exist" });
} }
@@ -82,16 +82,10 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
"success", "success",
`Provision ${exists ? "updated" : "created"}` `Provision ${exists ? "updated" : "created"}`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch((err) => { .catch(reject);
if (err["code"] === 400 && err["response"]) {
reject(new Error(err["response"]));
return;
}
reject(err);
});
}) })
.catch(reject); .catch(reject);
} else if (action === "delete") { } else if (action === "delete") {
@@ -99,11 +93,11 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.deleteResource("provisions", object["_id"]) .deleteResource("provisions", object["_id"])
.then(() => { .then(() => {
notifications.push("success", "Provision deleted"); notifications.push("success", "Provision deleted");
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch((err) => { .catch((err) => {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
reject(err); reject(err);
}); });
} else { } else {
@@ -220,7 +214,7 @@ export const component: ClosureComponent = (): Component => {
{ {
base: provision, base: provision,
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object, false) putActionHandler(action, object, false)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -272,7 +266,7 @@ export const component: ClosureComponent = (): Component => {
Object.assign( Object.assign(
{ {
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object, true) putActionHandler(action, object, true)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -332,11 +326,11 @@ export const component: ClosureComponent = (): Component => {
"success", "success",
`${res.length} provisions deleted` `${res.length} provisions deleted`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}); });
}, },
}, },
@@ -354,11 +348,7 @@ export const component: ClosureComponent = (): Component => {
return [ return [
m("h1", "Listing provisions"), m("h1", "Listing provisions"),
m(filterComponent, filterAttrs), m(filterComponent, filterAttrs),
m( m(indexTableComponent, attrs),
"loading",
{ queries: [provisions, count] },
m(indexTableComponent, attrs)
),
]; ];
}, },
}; };
+172 -142
View File
@@ -58,11 +58,11 @@ const resources: Resources = {};
for (const r of [ for (const r of [
"devices", "devices",
"faults", "faults",
"files",
"presets", "presets",
"provisions", "provisions",
"virtualParameters", "virtualParameters",
"files", "files",
"uploads",
"config", "config",
"users", "users",
"permissions", "permissions",
@@ -76,14 +76,14 @@ for (const r of [
} }
class QueryResponse { class QueryResponse {
public get fulfilled(): number { public get fulfilled(): boolean {
queries.accessed.set(this, Date.now()); queries.accessed.set(this, Date.now());
return queries.fulfilled.get(this) || 0; return !!queries.fulfilled.get(this);
} }
public get fulfilling(): boolean { public get fulfilling(): boolean {
queries.accessed.set(this, Date.now()); queries.accessed.set(this, Date.now());
return !(queries.fulfilled.get(this) >= fulfillTimestamp); return queries.fulfilling.has(this);
} }
public get value(): any { public get value(): any {
@@ -348,7 +348,18 @@ export function fetch(
return queryResponse; return queryResponse;
} }
export function fulfill(accessTimestamp: number): void { function _fulfill(
accessTimestamp: number,
_fulfillTimestamp: number
): Promise<boolean> {
let updated = false;
if (_fulfillTimestamp > fulfillTimestamp) {
for (const resource of Object.values(resources))
resource.combinedFilter = null;
fulfillTimestamp = _fulfillTimestamp;
}
const allPromises = []; const allPromises = [];
for (const [resourceType, resource] of Object.entries(resources)) { for (const [resourceType, resource] of Object.entries(resources)) {
@@ -362,32 +373,38 @@ export function fulfill(accessTimestamp: number): void {
if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) { if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) {
queries.fulfilling.add(queryResponse); queries.fulfilling.add(queryResponse);
let filter = queries.filter.get(queryResponse);
filter = unpackExpression(filter);
allPromises.push( allPromises.push(
xhrRequest({ new Promise((resolve, reject) => {
method: "HEAD", updated = true;
url: let filter = queries.filter.get(queryResponse);
`api/${resourceType}/?` + filter = unpackExpression(filter);
m.buildQueryString({ xhrRequest({
filter: memoizedStringify(filter), method: "HEAD",
}), url:
extract: (xhr) => { `api/${resourceType}/?` +
if (xhr.status === 403) throw new Error("Not authorized"); m.buildQueryString({
if (!xhr.status) { filter: memoizedStringify(filter),
throw new Error("Server is unreachable"); }),
} else if (xhr.status !== 200) { extract: (xhr) => {
throw new Error( if (xhr.status === 403) throw new Error("Not authorized");
`Unexpected response status code ${xhr.status}` if (!xhr.status) {
); throw new Error("Server is unreachable");
} } else if (xhr.status !== 200) {
return +xhr.getResponseHeader("x-total-count"); throw new Error(
}, `Unexpected response status code ${xhr.status}`
background: false, );
}).then((c) => { }
queries.value.set(queryResponse, c); return +xhr.getResponseHeader("x-total-count");
queries.fulfilled.set(queryResponse, fulfillTimestamp); },
queries.fulfilling.delete(queryResponse); background: true,
})
.then((c) => {
queries.value.set(queryResponse, c);
queries.fulfilled.set(queryResponse, fulfillTimestamp);
queries.fulfilling.delete(queryResponse);
resolve();
})
.catch((err) => reject(err));
}) })
); );
} }
@@ -412,39 +429,45 @@ export function fulfill(accessTimestamp: number): void {
const limit = queries.limit.get(queryResponse); const limit = queries.limit.get(queryResponse);
const sort = queries.sort.get(queryResponse); const sort = queries.sort.get(queryResponse);
if (limit) { if (limit) {
let filter = queries.filter.get(queryResponse);
filter = unpackExpression(filter);
allPromises.push( allPromises.push(
xhrRequest({ new Promise((resolve, reject) => {
method: "GET", updated = true;
url: let filter = queries.filter.get(queryResponse);
`api/${resourceType}/?` + filter = unpackExpression(filter);
m.buildQueryString({ xhrRequest({
filter: memoizedStringify(filter), method: "GET",
limit: 1, url:
skip: limit - 1, `api/${resourceType}/?` +
sort: JSON.stringify(sort), m.buildQueryString({
projection: Object.keys(sort).join(","), filter: memoizedStringify(filter),
}), limit: 1,
background: true, skip: limit - 1,
}).then((res) => { sort: JSON.stringify(sort),
if ((res as any[]).length) { projection: Object.keys(sort).join(","),
// Generate bookmark object }),
const bm = Object.keys(sort).reduce((b, k) => { })
if (res[0][k] != null) { .then((res) => {
if (typeof res[0][k] === "object") { if ((res as any[]).length) {
if (res[0][k].value != null) b[k] = res[0][k].value[0]; // Generate bookmark object
} else { const bm = Object.keys(sort).reduce((b, k) => {
b[k] = res[0][k]; if (res[0][k] != null) {
} if (typeof res[0][k] === "object") {
} if (res[0][k].value != null)
b[k] = res[0][k].value[0];
} else {
b[k] = res[0][k];
}
}
return b; return b;
}, {}); }, {});
queries.bookmark.set(queryResponse, bm); queries.bookmark.set(queryResponse, bm);
} else { } else {
queries.bookmark.delete(queryResponse); queries.bookmark.delete(queryResponse);
} }
resolve();
})
.catch(reject);
}) })
); );
} }
@@ -452,82 +475,31 @@ export function fulfill(accessTimestamp: number): void {
} }
} }
Promise.all(allPromises) return new Promise((resolve, reject) => {
.then(() => { Promise.all(allPromises)
let updated = false; .then(() => {
const allPromises2 = []; const allPromises2 = [];
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) { for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
let combinedFilter = null; let combinedFilter = null;
for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(filter, null, fulfillTimestamp);
const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse);
if (bookmark) filter = limitFilter(filter, sort, bookmark);
combinedFilter = or(combinedFilter, filter);
}
const [union, diff] = unionDiff(
resources[resourceType].combinedFilter,
combinedFilter
);
if (!diff) {
for (const queryResponse of toFetch) { for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse); let filter = queries.filter.get(queryResponse);
filter = memoizedEvaluate(filter, null, fulfillTimestamp); filter = memoizedEvaluate(filter, null, fulfillTimestamp);
const limit = queries.limit.get(queryResponse);
const bookmark = queries.bookmark.get(queryResponse); const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse); const sort = queries.sort.get(queryResponse);
if (bookmark) filter = limitFilter(filter, sort, bookmark); if (bookmark) filter = limitFilter(filter, sort, bookmark);
combinedFilter = or(combinedFilter, filter);
queries.value.set(
queryResponse,
findMatches(resourceType, filter, sort, limit)
);
queries.fulfilled.set(queryResponse, fulfillTimestamp);
queries.fulfilling.delete(queryResponse);
updated = true;
} }
continue;
}
let deleted = new Set<string>(); const [union, diff] = unionDiff(
if (!resources[resourceType].combinedFilter) resources[resourceType].combinedFilter,
deleted = new Set(resources[resourceType].objects.keys()); combinedFilter
);
const combinedFilterDiff = diff;
resources[resourceType].combinedFilter = union;
allPromises2.push(
xhrRequest({
method: "GET",
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(combinedFilterDiff),
}),
background: false,
}).then((res) => {
for (const r of res as any[]) {
const id =
resourceType === "devices"
? r["DeviceID.ID"].value[0]
: r["_id"];
resources[resourceType].objects.set(id, r);
deleted.delete(id);
}
for (const d of deleted) {
const obj = resources[resourceType].objects.get(d);
if (evaluate(combinedFilterDiff, obj, fulfillTimestamp))
resources[resourceType].objects.delete(d);
}
if (!diff) {
for (const queryResponse of toFetch) { for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse); let filter = queries.filter.get(queryResponse);
filter = unpackExpression(filter); filter = memoizedEvaluate(filter, null, fulfillTimestamp);
const limit = queries.limit.get(queryResponse); const limit = queries.limit.get(queryResponse);
const bookmark = queries.bookmark.get(queryResponse); const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse); const sort = queries.sort.get(queryResponse);
@@ -540,29 +512,87 @@ export function fulfill(accessTimestamp: number): void {
queries.fulfilled.set(queryResponse, fulfillTimestamp); queries.fulfilled.set(queryResponse, fulfillTimestamp);
queries.fulfilling.delete(queryResponse); queries.fulfilling.delete(queryResponse);
} }
}) continue;
); }
}
if (updated) m.redraw(); updated = true;
return Promise.all(allPromises2); let deleted = new Set<string>();
}) if (!resources[resourceType].combinedFilter)
.catch((err) => { deleted = new Set(resources[resourceType].objects.keys());
notifications.push("error", err.message);
}); const combinedFilterDiff = diff;
resources[resourceType].combinedFilter = union;
allPromises2.push(
new Promise((resolve2, reject2) => {
xhrRequest({
method: "GET",
url:
`api/${resourceType}/?` +
m.buildQueryString({
filter: memoizedStringify(combinedFilterDiff),
}),
})
.then((res) => {
for (const r of res as any[]) {
const id =
resourceType === "devices"
? r["DeviceID.ID"].value[0]
: r["_id"];
resources[resourceType].objects.set(id, r);
deleted.delete(id);
}
for (const d of deleted) {
const obj = resources[resourceType].objects.get(d);
if (evaluate(combinedFilterDiff, obj, fulfillTimestamp))
resources[resourceType].objects.delete(d);
}
for (const queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse);
filter = unpackExpression(filter);
const limit = queries.limit.get(queryResponse);
const bookmark = queries.bookmark.get(queryResponse);
const sort = queries.sort.get(queryResponse);
if (bookmark) filter = limitFilter(filter, sort, bookmark);
queries.value.set(
queryResponse,
findMatches(resourceType, filter, sort, limit)
);
queries.fulfilled.set(queryResponse, fulfillTimestamp);
queries.fulfilling.delete(queryResponse);
}
resolve2();
})
.catch(reject2);
})
);
}
Promise.all(allPromises2)
.then(() => resolve(updated))
.catch(reject);
})
.catch(reject);
});
}
export function fulfill(
accessTimestamp: number,
_fulfillTimestamp: number,
callback?: (updated: boolean) => void
): void {
_fulfill(accessTimestamp, _fulfillTimestamp).then(callback, (err) => {
notifications.push("error", err.message);
if (callback) callback(false);
});
} }
export function getTimestamp(): number { export function getTimestamp(): number {
return fulfillTimestamp; return fulfillTimestamp;
} }
export function setTimestamp(t: number): void {
if (t > fulfillTimestamp) {
fulfillTimestamp = t;
for (const resource of Object.values(resources))
resource.combinedFilter = null;
}
}
export function postTasks( export function postTasks(
deviceId: string, deviceId: string,
tasks: QueueTask[] tasks: QueueTask[]
+8
View File
@@ -89,6 +89,14 @@ export function stageDownload(task: StageTask): void {
staging.add(task); staging.add(task);
} }
export function stageUpload(task: StageTask): void {
if (queue.size + task.devices.length > MAX_QUEUE) {
notifications.push("error", "Too many tasks in queue");
return;
}
staging.add(task);
}
export function commit( export function commit(
tasks: QueueTask[], tasks: QueueTask[],
callback: ( callback: (
+2 -3
View File
@@ -55,8 +55,7 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
for (const f of res) current[f._id] = f.value; for (const f of res) current[f._id] = f.value;
const diff = configFunctions.diffConfig(current, updated); const diff = configFunctions.diffConfig(current, updated);
if (!diff.add.length && !diff.remove.length) if (!diff.add.length && !diff.remove.length) return void resolve();
return void resolve(null);
const promises = []; const promises = [];
@@ -75,7 +74,7 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
Promise.all(promises) Promise.all(promises)
.then(() => { .then(() => {
resolve(null); resolve();
}) })
.catch(reject); .catch(reject);
}) })
+14 -18
View File
@@ -85,12 +85,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.resourceExists("users", id) .resourceExists("users", id)
.then((exists) => { .then((exists) => {
if (exists && isNew) { if (exists && isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "User already exists" }); return void resolve({ _id: "User already exists" });
} }
if (!exists && !isNew) { if (!exists && !isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "User does not exist" }); return void resolve({ _id: "User does not exist" });
} }
@@ -102,14 +102,14 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.changePassword(id, password) .changePassword(id, password)
.then(() => { .then(() => {
notifications.push("success", "User created"); notifications.push("success", "User created");
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch(reject); .catch(reject);
} else { } else {
notifications.push("success", "User updated"); notifications.push("success", "User updated");
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
} }
}) })
.catch(reject); .catch(reject);
@@ -120,11 +120,11 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.deleteResource("users", object["_id"]) .deleteResource("users", object["_id"])
.then(() => { .then(() => {
notifications.push("success", "User deleted"); notifications.push("success", "User deleted");
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch((err) => { .catch((err) => {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
reject(err); reject(err);
}); });
} else { } else {
@@ -247,7 +247,7 @@ export const component: ClosureComponent = (): Component => {
roles: user.roles.split(","), roles: user.roles.split(","),
}, },
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object, false) putActionHandler(action, object, false)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -329,7 +329,7 @@ export const component: ClosureComponent = (): Component => {
Object.assign( Object.assign(
{ {
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object, true) putActionHandler(action, object, true)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -387,11 +387,11 @@ export const component: ClosureComponent = (): Component => {
"success", "success",
`${res.length} users deleted` `${res.length} users deleted`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}); });
}, },
}, },
@@ -409,11 +409,7 @@ export const component: ClosureComponent = (): Component => {
return [ return [
m("h1", "Listing users"), m("h1", "Listing users"),
m(filterComponent, filterAttrs), m(filterComponent, filterAttrs),
m( m(indexTableComponent, attrs),
"loading",
{ queries: [users, count] },
m(indexTableComponent, attrs)
),
]; ];
}, },
}; };
+13 -23
View File
@@ -66,12 +66,12 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.resourceExists("virtualParameters", id) .resourceExists("virtualParameters", id)
.then((exists) => { .then((exists) => {
if (exists && isNew) { if (exists && isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "Virtual parameter already exists" }); return void resolve({ _id: "Virtual parameter already exists" });
} }
if (!exists && !isNew) { if (!exists && !isNew) {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
return void resolve({ _id: "Virtual parameter does not exist" }); return void resolve({ _id: "Virtual parameter does not exist" });
} }
@@ -82,16 +82,10 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
"success", "success",
`Virtual parameter ${exists ? "updated" : "created"}` `Virtual parameter ${exists ? "updated" : "created"}`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch((err) => { .catch(reject);
if (err["code"] === 400 && err["response"]) {
reject(new Error(err["response"]));
return;
}
reject(err);
});
}) })
.catch(reject); .catch(reject);
} else if (action === "delete") { } else if (action === "delete") {
@@ -99,11 +93,11 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
.deleteResource("virtualParameters", object["_id"]) .deleteResource("virtualParameters", object["_id"])
.then(() => { .then(() => {
notifications.push("success", "Virtual parameter deleted"); notifications.push("success", "Virtual parameter deleted");
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
resolve(null); resolve();
}) })
.catch((err) => { .catch((err) => {
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
reject(err); reject(err);
}); });
} else { } else {
@@ -220,7 +214,7 @@ export const component: ClosureComponent = (): Component => {
{ {
base: virtualParameter, base: virtualParameter,
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object, false) putActionHandler(action, object, false)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -272,7 +266,7 @@ export const component: ClosureComponent = (): Component => {
Object.assign( Object.assign(
{ {
actionHandler: (action, object) => { actionHandler: (action, object) => {
return new Promise<void>((resolve) => { return new Promise((resolve) => {
putActionHandler(action, object, true) putActionHandler(action, object, true)
.then((errors) => { .then((errors) => {
const errorList = errors const errorList = errors
@@ -332,11 +326,11 @@ export const component: ClosureComponent = (): Component => {
"success", "success",
`${res.length} virtual parameters deleted` `${res.length} virtual parameters deleted`
); );
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}) })
.catch((err) => { .catch((err) => {
notifications.push("error", err.message); notifications.push("error", err.message);
store.setTimestamp(Date.now()); store.fulfill(0, Date.now());
}); });
}, },
}, },
@@ -354,11 +348,7 @@ export const component: ClosureComponent = (): Component => {
return [ return [
m("h1", "Listing virtual parameters"), m("h1", "Listing virtual parameters"),
m(filterComponent, filterAttrs), m(filterComponent, filterAttrs),
m( m(indexTableComponent, attrs),
"loading",
{ queries: [virtualParameters, count] },
m(indexTableComponent, attrs)
),
]; ];
}, },
}; };