Compare commits

..
5 Commits
138 changed files with 14505 additions and 22373 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
!/bin/.*.ts
!/tools/.*.ts
+5 -16
View File
@@ -8,30 +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",
"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
+52 -203
View File
@@ -1,197 +1,48 @@
# Change Log # Change Log
## 1.2.8 (2021-10-27)
- Fix a remote code execution security vulnerability in genieacs-ui.
- All UI components can now be configured using fully dynamic expressions.
Previously some component types only accept fixed string values as properties.
- The `container` UI component can now be configured with an `element` property
that's either a string or a nested pair of `tag` and `attributes` properties.
The various attributes under the `attributes` property can now make use of a
new function `ENCODEURICOMPONENT()` to facilitate creating custom hyperlinks
in the UI.
- Improve sorting buttons' behavior in the device listing page and other pages.
Sorting by multiple columns should now feel more intuitive.
- Support the modulo (%) operator in expressions.
- Fix a regression in the previous release where the config option 'ui.pageSize'
no longer works.
- Fix process crash when a CPE sends an unsupported ACS method.
## 1.2.7 (2021-09-18)
- Fix regression causing frequent invalid session errors.
- Fix regression breaking digest authentication in connection requests.
## 1.2.6 (2021-09-16)
- New config option `cwmp.downloadSuccessOnTimeout` to enable a workaround for
CPEs that neglect to send a TransferComplete message after a successful
download.
- Display a progress bar when uploading new files.
- Default to dual stack interface binding (i.e. `::` instead of `0.0.0.0`).
Unless the binding interface is explicitly set, this will cause IPv4 addresses
in the logs to be displayed as IPv4-mapped IPv6 addresses (e.g.
`::ffff:127.0.0.1` instead of `127.0.0.1`).
- Detect and correct for client side (browser) clock skew that would otherwise
alter the numbers in the pie charts.
- Various improvements relating to dealing with buggy TR-069 client
implementations.
- Fix a bug causing "lock expired" exceptions when a CWMP session remains open
for a very long time due to slow clients.
- Fix metadata of uploaded files going missing due to nginx stripping away what
it considers to be invalid headers. The nginx directive
`ignore_invalid_headers` is no longer required.
- Fix crash when a CPE is assigned a tag containing a dot.
- Fix bug preventing the user from closing the preset pop-up after being
presented with unsupported preset message.
- Fix exceptions raised from ext scripts manifesting as timeout faults.
- Fix download getting triggered repeatedly when the value passed to `Download`
parameter is greater than the current timestamp.
- Fix crash when passing invalid attributes to `declare`.
- Fix crash in NBI when pushing tasks to non-existing devices.
- Fix crash in NBI when passing invalid JSON to various API endpoints.
- Fix crash when the output from `ping` command cannot be parsed in some rare
edge cases.
- A number of other fixes and stability improvements.
## 1.2.5 (2021-03-12)
- Support specifying custom types when uploading files.
- Fix JS compatibility issue with Safari browser.
## 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)
- New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly
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
a SetParamteerValues, AddObject, or DeleteObject request anyway.
- Tags no longer restrict what characters are allowed. Any character other than
alphanumeric characters, hyphen, or underscore is now encoded in the data
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.
- Better XML validation to avoid crashes caused by invalid CPE requests.
- Fix confusing 404 error message when the user attempts to modify a resource
when they don't have the necessary permissions.
- Fix a rare issue where genieacs-cwmp stops accepting new connections after
running for a few weeks.
- Fix exception when IS NULL operator is used in certain situations.
## 1.2.2 (2020-10-03)
- Added button to push files to selected devices from device listing page.
- A few minor UI improvements.
- Fix exception that can happen and persist after a Download request.
- Fix validation bug preventing running refreshObject task on data model root.
- Fix invalid arguments fault in refresh preset configuration when upgrading
from v1.1.
## 1.2.1 (2020-09-08)
- Fix bug causing faults to not be displayed 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
MongoDB queries. There should now be fewer edge cases where the two are not
equisatisfiable.
## 1.2.0 (2020-09-01)
- Support GetParameterAttributes and SetParameterAttributes TR-069 methods.
- Support CASE statement and COALESCE function in expressions.
- Provision arguments can now be a list of expressions that are dynamically
evaluated.
- 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.
- Config expressions can now access all available device parameters, not only
serial number, product class, and OUI.
- Use relative URLs throughout the UI to allow serving from a subdirectory using
a reverse proxy.
- Make Date.parse() and Date.UTC() available to provision scripts.
- 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
to determine the TCP connection timeout.
- The all-parameters component now limits the number of parameters displayed for
better performance.
- The process genieacs-cwmp is now much less likely to throw exceptions as a
result of invalid requests from CPE.
- 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.
@@ -201,68 +52,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.
@@ -271,19 +120,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
+17 -17
View File
@@ -1,21 +1,22 @@
# Have a question? # Have a question?
Please ask in the [forum] instead of opening a Github Issue. The Github issue Please ask in the [Users mailing list][mailing-lists] instead of opening a
tracker is strictly for bug reports. Github Issue. The Github issue 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 device, more often than not it's
device related issue. Please consult the [forum] before submitting an Issue. a device related issue. Please consult the [Users mailing list][mailing-lists]
before submitting an Issue.
# Pull Requests # Pull Requests
Pull requests are welcome. If you have a fix for a bug, go ahead and create a Pull requests are welcome. If you have a fix for a bug, go ahead and create a
pull request. For other kinds of enhancements, please discuss in the [forum] pull request. For other kinds of enhancements, please discuss in the [Users
first. mailing list][mailing-lists] first.
Please follow the guidelines [here][commit-guidelines] when committing your Please follow the guidelines [here][commit-guidelines] when committing your
changes. changes.
@@ -26,14 +27,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 [mailing-lists]: http://lists.genieacs.com
[forum]: https://forum.genieacs.com
+34 -24
View File
@@ -8,30 +8,35 @@ millions of concurrent devices.
## Quick Start ## Quick Start
*Important: This is a pre-release branch. Use
[v1.1](https://github.com/genieacs/genieacs/tree/v1.1) for production
deployments.*
Install [Node.js](http://nodejs.org/) and [MongoDB](http://www.mongodb.org/). Install [Node.js](http://nodejs.org/) and [MongoDB](http://www.mongodb.org/).
Refer to their corresponding documentation for installation instructions. The Refer to their corresponding documentation for installation instructions. The
supported versions are: supported versions are:
- Node.js: 12.3+ - Node.js: 10.x through 12.x
- MongoDB: 3.6+ - MongoDB: 2.6 through 4.1
Install GenieACS from NPM: Clone this repo or download the source archive then *cd* into the source
directory and install the required dependencies:
sudo npm install -g genieacs
To build from source instead, clone this repo or download the source archive
then _cd_ into the source directory then run:
npm install npm install
Now build:
npm run build npm run build
Finally, run the following services (found under `./dist/bin/` if building from Finally, run the following services from the build that's generated under
source): 'dist' directory:
### 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.
./dist/bin/genieacs-cwmp
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.
@@ -42,35 +47,40 @@ This is the northbound interface module. It exposes a REST API on port 7557 by
default. This one is only required if you have an external system integrating default. This one is only required if you have an external system integrating
with GenieACS using this API. with GenieACS using this API.
./dist/bin/genieacs-nbi
### genieacs-fs ### genieacs-fs
This is the file server from which the CPEs will download firmware images and This is the file server from which the CPEs will download firmware images and
such. It listens on port 7567 by default. such.
./dist/bin/genieacs-fs
### 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
signing browser cookies:
genieacs-ui --ui-jwt-secret secret ./dist/bin/genieacs-ui --ui-jwt-secret secret
The argument *--ui-jwt-secret* supplies the key used for signing browser
cookies.
The UI has plenty of configuration options. When you open GenieACS's UI in a The UI has plenty of configuration options. When you open GenieACS's UI in a
browser you'll be greeted with a database initialization wizard to help you browser you'll be greeted with a database initialization wizard to help you
populate some initial configuration. populate some initial configuration.
Visit [docs.genieacs.com](https://docs.genieacs.com) for more documentation and
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! In addition,
the [wiki](https://github.com/genieacs/genieacs/wiki) provides useful
documentation and tips from GenieACS users.
For commercial support options, please visit For commercial support options and professional services, please visit
[genieacs.com](https://genieacs.com/support/). [genieacs.com](https://genieacs.com/support/).
## License ## License
Copyright 2013-2021 GenieACS Inc. GenieACS is released under the Copyright 2013-2019 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).
+4 -2
View File
@@ -1,4 +1,6 @@
export default { export default {
extensions: ["ts"], "compileEnhancements": false,
require: ["esm", "ts-node/register/transpile-only"], "extensions": ["ts"],
"require": ["esm", "ts-node/register/transpile-only"]
}; };
+39 -27
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
/** /**
* Copyright 2013-2019 GenieACS Inc. * Copyright 2013-2019 GenieACS Inc.
* *
@@ -24,35 +26,37 @@ import * as server from "../lib/server";
import * as cwmp from "../lib/cwmp"; import * as cwmp from "../lib/cwmp";
import * as db from "../lib/db"; import * as db from "../lib/db";
import * as extensions from "../lib/extensions"; import * as extensions from "../lib/extensions";
import * as cache from "../lib/cache";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
logger.init("cwmp", VERSION); logger.init("cwmp", VERSION);
const SERVICE_ADDRESS = config.get("CWMP_INTERFACE") as string; const SERVICE_ADDRESS = config.get("CWMP_INTERFACE");
const SERVICE_PORT = config.get("CWMP_PORT") as number; const SERVICE_PORT = config.get("CWMP_PORT");
function exitWorkerGracefully(): void { function exitWorkerGracefully(): void {
setTimeout(exitWorkerUngracefully, 5000).unref(); setTimeout(exitWorkerUngracefully, 5000).unref();
Promise.all([ Promise.all([
db.disconnect(), db.disconnect(),
cache.disconnect(),
extensions.killAll(), extensions.killAll(),
cluster.worker.disconnect(), cluster.worker.disconnect()
]).catch(exitWorkerUngracefully); ]).catch(exitWorkerUngracefully);
} }
function exitWorkerUngracefully(): void { function exitWorkerUngracefully(): void {
extensions.killAll().finally(() => { extensions.killAll().then(() => {
process.exit(1); process.exit(1);
}); });
} }
if (!cluster.worker) { if (!cluster.worker) {
const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES") as number; const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES");
logger.info({ logger.info({
message: `genieacs-cwmp starting`, message: `genieacs-cwmp starting`,
pid: process.pid, pid: process.pid,
version: VERSION, version: VERSION
}); });
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
@@ -60,7 +64,7 @@ if (!cluster.worker) {
process.on("SIGINT", () => { process.on("SIGINT", () => {
logger.info({ logger.info({
message: "Received signal SIGINT, exiting", message: "Received signal SIGINT, exiting",
pid: process.pid, pid: process.pid
}); });
cluster.stop(); cluster.stop();
@@ -69,40 +73,42 @@ if (!cluster.worker) {
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
logger.info({ logger.info({
message: "Received signal SIGTERM, exiting", message: "Received signal SIGTERM, exiting",
pid: process.pid, pid: process.pid
}); });
cluster.stop(); cluster.stop();
}); });
} else { } else {
const key = config.get("CWMP_SSL_KEY") as string; const ssl = {
const cert = config.get("CWMP_SSL_CERT") as string; key: config.get("CWMP_SSL_KEY"),
cert: config.get("CWMP_SSL_CERT")
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
onConnection: cwmp.onConnection,
timeout: 30000,
keepAliveTimeout: 0,
}; };
process.on("uncaughtException", (err) => { process.on("uncaughtException", err => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return; if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({ logger.error({
message: "Uncaught exception", message: "Uncaught exception",
exception: err, exception: err,
pid: process.pid, pid: process.pid
}); });
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
const initPromise = db const initPromise = Promise.all([db.connect(), cache.connect()])
.connect()
.then(() => { .then(() => {
server.start(options, cwmp.listener); server.start(
SERVICE_PORT,
SERVICE_ADDRESS,
ssl,
cwmp.listener,
cwmp.onConnection,
0
);
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throw err; throw err;
}); });
@@ -110,13 +116,19 @@ if (!cluster.worker) {
process.on("SIGINT", () => { process.on("SIGINT", () => {
initPromise.finally(() => { initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
}); });
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
initPromise.finally(() => { initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
}); });
} }
+9 -9
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
/** /**
* Copyright 2013-2019 GenieACS Inc. * Copyright 2013-2019 GenieACS Inc.
* *
@@ -33,8 +35,8 @@ function errorToFault(err: Error): Fault {
message: err.message, message: err.message,
detail: { detail: {
name: err.name, name: err.name,
message: err.message, message: err.message
}, }
}; };
if (err.stack) { if (err.stack) {
@@ -54,16 +56,16 @@ function errorToFault(err: Error): Fault {
return fault; return fault;
} }
process.on("uncaughtException", (err) => { process.on("uncaughtException", err => {
const fault = errorToFault(err); const fault = errorToFault(err);
jobs.forEach((jobId) => { jobs.forEach(jobId => {
process.send([jobId, fault, null]); process.send([jobId, fault, null]);
}); });
jobs.clear(); jobs.clear();
process.disconnect(); process.disconnect();
}); });
process.on("message", (message) => { process.on("message", message => {
jobs.add(message[0]); jobs.add(message[0]);
if (!script) { if (!script) {
@@ -77,7 +79,7 @@ process.on("message", (message) => {
if (!script[funcName]) { if (!script[funcName]) {
const fault = { const fault = {
code: "ext", code: "ext",
message: `No such function '${funcName}' in extension '${fileName}'`, message: `No such function '${funcName}' in extension '${fileName}'`
}; };
process.send([message[0], fault, null]); process.send([message[0], fault, null]);
return; return;
@@ -91,6 +93,4 @@ process.on("message", (message) => {
}); });
// Ignore SIGINT // Ignore SIGINT
process.on("SIGINT", () => { process.on("SIGINT", () => {});
// Ignore
});
+34 -35
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
/** /**
* Copyright 2013-2019 GenieACS Inc. * Copyright 2013-2019 GenieACS Inc.
* *
@@ -23,18 +25,21 @@ import * as cluster from "../lib/cluster";
import * as server from "../lib/server"; import * as server from "../lib/server";
import { listener } from "../lib/fs"; import { listener } from "../lib/fs";
import * as db from "../lib/db"; import * as db from "../lib/db";
import * as cache from "../lib/cache";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
logger.init("fs", VERSION); logger.init("fs", VERSION);
const SERVICE_ADDRESS = config.get("FS_INTERFACE") as string; const SERVICE_ADDRESS = config.get("FS_INTERFACE");
const SERVICE_PORT = config.get("FS_PORT") as number; const SERVICE_PORT = config.get("FS_PORT");
function exitWorkerGracefully(): void { function exitWorkerGracefully(): void {
setTimeout(exitWorkerUngracefully, 5000).unref(); setTimeout(exitWorkerUngracefully, 5000).unref();
Promise.all([db.disconnect(), cluster.worker.disconnect()]).catch( Promise.all([
exitWorkerUngracefully db.disconnect(),
); cache.disconnect(),
cluster.worker.disconnect()
]).catch(exitWorkerUngracefully);
} }
function exitWorkerUngracefully(): void { function exitWorkerUngracefully(): void {
@@ -42,12 +47,12 @@ function exitWorkerUngracefully(): void {
} }
if (!cluster.worker) { if (!cluster.worker) {
const WORKER_COUNT = config.get("FS_WORKER_PROCESSES") as number; const WORKER_COUNT = config.get("FS_WORKER_PROCESSES");
logger.info({ logger.info({
message: `genieacs-fs starting`, message: `genieacs-fs starting`,
pid: process.pid, pid: process.pid,
version: VERSION, version: VERSION
}); });
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
@@ -55,7 +60,7 @@ if (!cluster.worker) {
process.on("SIGINT", () => { process.on("SIGINT", () => {
logger.info({ logger.info({
message: "Received signal SIGINT, exiting", message: "Received signal SIGINT, exiting",
pid: process.pid, pid: process.pid
}); });
cluster.stop(); cluster.stop();
@@ -64,61 +69,55 @@ if (!cluster.worker) {
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
logger.info({ logger.info({
message: "Received signal SIGTERM, exiting", message: "Received signal SIGTERM, exiting",
pid: process.pid, pid: process.pid
}); });
cluster.stop(); cluster.stop();
}); });
} else { } else {
const key = config.get("FS_SSL_KEY") as string; const ssl = {
const cert = config.get("FS_SSL_CERT") as string; key: config.get("FS_SSL_KEY"),
const options = { cert: config.get("FS_SSL_CERT")
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
}; };
let stopping = false; process.on("uncaughtException", err => {
process.on("uncaughtException", (err) => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return; if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({ logger.error({
message: "Uncaught exception", message: "Uncaught exception",
exception: err, exception: err,
pid: process.pid, pid: process.pid
}); });
stopping = true; server
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); .stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
const _listener = (req, res): void => { const initPromise = Promise.all([db.connect(), cache.connect()])
if (stopping) res.setHeader("Connection", "close");
void listener(req, res);
};
const initPromise = db
.connect()
.then(() => { .then(() => {
server.start(options, _listener); server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throw err; throw err;
}); });
}); });
process.on("SIGINT", () => { process.on("SIGINT", () => {
stopping = true;
initPromise.finally(() => { initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
}); });
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
stopping = true;
initPromise.finally(() => { initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
}); });
} }
+32 -34
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
/** /**
* Copyright 2013-2019 GenieACS Inc. * Copyright 2013-2019 GenieACS Inc.
* *
@@ -24,35 +26,37 @@ import * as server from "../lib/server";
import { listener } from "../lib/nbi"; import { listener } from "../lib/nbi";
import * as db from "../lib/db"; import * as db from "../lib/db";
import * as extensions from "../lib/extensions"; import * as extensions from "../lib/extensions";
import * as cache from "../lib/cache";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
logger.init("nbi", VERSION); logger.init("nbi", VERSION);
const SERVICE_ADDRESS = config.get("NBI_INTERFACE") as string; const SERVICE_ADDRESS = config.get("NBI_INTERFACE");
const SERVICE_PORT = config.get("NBI_PORT") as number; const SERVICE_PORT = config.get("NBI_PORT");
function exitWorkerGracefully(): void { function exitWorkerGracefully(): void {
setTimeout(exitWorkerUngracefully, 5000).unref(); setTimeout(exitWorkerUngracefully, 5000).unref();
Promise.all([ Promise.all([
db.disconnect(), db.disconnect(),
cache.disconnect(),
extensions.killAll(), extensions.killAll(),
cluster.worker.disconnect(), cluster.worker.disconnect()
]).catch(exitWorkerUngracefully); ]).catch(exitWorkerUngracefully);
} }
function exitWorkerUngracefully(): void { function exitWorkerUngracefully(): void {
extensions.killAll().finally(() => { extensions.killAll().then(() => {
process.exit(1); process.exit(1);
}); });
} }
if (!cluster.worker) { if (!cluster.worker) {
const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES") as number; const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES");
logger.info({ logger.info({
message: `genieacs-nbi starting`, message: `genieacs-nbi starting`,
pid: process.pid, pid: process.pid,
version: VERSION, version: VERSION
}); });
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
@@ -60,7 +64,7 @@ if (!cluster.worker) {
process.on("SIGINT", () => { process.on("SIGINT", () => {
logger.info({ logger.info({
message: "Received signal SIGINT, exiting", message: "Received signal SIGINT, exiting",
pid: process.pid, pid: process.pid
}); });
cluster.stop(); cluster.stop();
@@ -69,61 +73,55 @@ if (!cluster.worker) {
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
logger.info({ logger.info({
message: "Received signal SIGTERM, exiting", message: "Received signal SIGTERM, exiting",
pid: process.pid, pid: process.pid
}); });
cluster.stop(); cluster.stop();
}); });
} else { } else {
const key = config.get("NBI_SSL_KEY") as string; const ssl = {
const cert = config.get("NBI_SSL_CERT") as string; key: config.get("NBI_SSL_KEY"),
const options = { cert: config.get("NBI_SSL_CERT")
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
}; };
let stopping = false; process.on("uncaughtException", err => {
process.on("uncaughtException", (err) => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return; if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({ logger.error({
message: "Uncaught exception", message: "Uncaught exception",
exception: err, exception: err,
pid: process.pid, pid: process.pid
}); });
stopping = true; server
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); .stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
const _listener = (req, res): void => { const initPromise = Promise.all([db.connect(), cache.connect()])
if (stopping) res.setHeader("Connection", "close");
listener(req, res);
};
const initPromise = db
.connect()
.then(() => { .then(() => {
server.start(options, _listener); server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throw err; throw err;
}); });
}); });
process.on("SIGINT", () => { process.on("SIGINT", () => {
stopping = true;
initPromise.finally(() => { initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
}); });
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
stopping = true;
initPromise.finally(() => { initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
}); });
} }
+35 -25
View File
@@ -1,3 +1,5 @@
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
/** /**
* Copyright 2013-2019 GenieACS Inc. * Copyright 2013-2019 GenieACS Inc.
* *
@@ -23,36 +25,40 @@ import * as cluster from "../lib/cluster";
import * as server from "../lib/server"; import * as server from "../lib/server";
import { listener } from "../lib/ui"; import { listener } from "../lib/ui";
import * as extensions from "../lib/extensions"; import * as extensions from "../lib/extensions";
import * as db from "../lib/db"; import * as db from "../lib/ui/db";
import * as db2 from "../lib/db";
import * as cache from "../lib/cache";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
logger.init("ui", VERSION); logger.init("ui", VERSION);
const SERVICE_ADDRESS = config.get("UI_INTERFACE") as string; const SERVICE_ADDRESS = config.get("UI_INTERFACE");
const SERVICE_PORT = config.get("UI_PORT") as number; const SERVICE_PORT = config.get("UI_PORT");
function exitWorkerGracefully(): void { function exitWorkerGracefully(): void {
setTimeout(exitWorkerUngracefully, 5000).unref(); setTimeout(exitWorkerUngracefully, 5000).unref();
Promise.all([ Promise.all([
db.disconnect(), db.disconnect(),
db2.disconnect(),
cache.disconnect(),
extensions.killAll(), extensions.killAll(),
cluster.worker.disconnect(), cluster.worker.disconnect()
]).catch(exitWorkerUngracefully); ]).catch(exitWorkerUngracefully);
} }
function exitWorkerUngracefully(): void { function exitWorkerUngracefully(): void {
extensions.killAll().finally(() => { extensions.killAll().then(() => {
process.exit(1); process.exit(1);
}); });
} }
if (!cluster.worker) { if (!cluster.worker) {
const WORKER_COUNT = config.get("UI_WORKER_PROCESSES") as number; const WORKER_COUNT = config.get("UI_WORKER_PROCESSES");
logger.info({ logger.info({
message: `genieacs-ui starting`, message: `genieacs-ui starting`,
pid: process.pid, pid: process.pid,
version: VERSION, version: VERSION
}); });
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS); cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
@@ -60,7 +66,7 @@ if (!cluster.worker) {
process.on("SIGINT", () => { process.on("SIGINT", () => {
logger.info({ logger.info({
message: "Received signal SIGINT, exiting", message: "Received signal SIGINT, exiting",
pid: process.pid, pid: process.pid
}); });
cluster.stop(); cluster.stop();
@@ -69,32 +75,31 @@ if (!cluster.worker) {
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
logger.info({ logger.info({
message: "Received signal SIGTERM, exiting", message: "Received signal SIGTERM, exiting",
pid: process.pid, pid: process.pid
}); });
cluster.stop(); cluster.stop();
}); });
} else { } else {
const key = config.get("UI_SSL_KEY") as string; const ssl = {
const cert = config.get("UI_SSL_CERT") as string; key: config.get("UI_SSL_KEY"),
const options = { cert: config.get("UI_SSL_CERT")
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
}; };
let stopping = false; let stopping = false;
process.on("uncaughtException", (err) => { process.on("uncaughtException", err => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return; if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({ logger.error({
message: "Uncaught exception", message: "Uncaught exception",
exception: err, exception: err,
pid: process.pid, pid: process.pid
}); });
stopping = true; stopping = true;
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
const _listener = (req, res): void => { const _listener = (req, res): void => {
@@ -102,12 +107,11 @@ if (!cluster.worker) {
listener(req, res); listener(req, res);
}; };
const initPromise = db const initPromise = Promise.all([db2.connect(), cache.connect()])
.connect()
.then(() => { .then(() => {
server.start(options, _listener); server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throw err; throw err;
}); });
@@ -116,14 +120,20 @@ if (!cluster.worker) {
process.on("SIGINT", () => { process.on("SIGINT", () => {
stopping = true; stopping = true;
initPromise.finally(() => { initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
}); });
process.on("SIGTERM", () => { process.on("SIGTERM", () => {
stopping = true; stopping = true;
initPromise.finally(() => { initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully); server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
}); });
}); });
} }
+153 -112
View File
@@ -19,17 +19,18 @@
import * as path from "path"; import * as path from "path";
import * as fs from "fs"; import * as fs from "fs";
import { promisify } from "util";
import { rollup, WarningHandler } from "rollup"; import { rollup, WarningHandler } from "rollup";
import rollupJson from "@rollup/plugin-json"; import rollupReplace from "rollup-plugin-replace";
import typescript from "@rollup/plugin-typescript"; import rollupJson from "rollup-plugin-json";
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 webpack from "webpack";
import commonjs from "@rollup/plugin-commonjs";
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 } 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,31 @@ const builtins = [
"zlib", "zlib",
"crypto", "crypto",
"util", "util",
"mongodb",
"libxmljs",
"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"
]; ];
function rmDirSync(dirPath): void { function rmDirSync(dirPath): void {
@@ -74,7 +93,6 @@ function rmDirSync(dirPath): void {
fs.rmdirSync(dirPath); fs.rmdirSync(dirPath);
} }
// For lockfileVersion = 1
function stripDevDeps(deps): void { function stripDevDeps(deps): void {
if (!deps["dependencies"]) return; if (!deps["dependencies"]) return;
for (const [k, v] of Object.entries(deps["dependencies"])) { for (const [k, v] of Object.entries(deps["dependencies"])) {
@@ -84,15 +102,6 @@ function stripDevDeps(deps): void {
if (!Object.keys(deps["dependencies"]).length) delete deps["dependencies"]; if (!Object.keys(deps["dependencies"]).length) delete deps["dependencies"];
} }
// For lockfileVersion = 2
function stripDevDeps2(deps): void {
if (!deps["packages"]) return;
for (const [k, v] of Object.entries(deps["packages"])) {
delete v["devDependencies"];
if (v["dev"]) delete deps["packages"][k];
}
}
function xmlTostring(xml): string { function xmlTostring(xml): string {
const children = []; const children = [];
for (const c of xml.children || []) children.push(xmlTostring(c)); for (const c of xml.children || []) children.push(xmlTostring(c));
@@ -114,12 +123,14 @@ function generateSymbol(id: string, svgStr: string): string {
} }
} }
const symbolBody = xml.children[0].children const symbolBody = xml.children[0].children
.map((c) => xmlTostring(c)) .map(c => {
return xmlTostring(c);
})
.join(""); .join("");
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);
@@ -127,16 +138,16 @@ 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
}; };
packageJson["version"] = `${packageJson["version"]}+${BUILD_METADATA}`; packageJson["version"] = `${packageJson["version"]}+${BUILD_METADATA}`;
fs.writeFileSync( fs.writeFileSync(
@@ -150,12 +161,10 @@ async function init(): Promise<string[]> {
); );
npmShrinkwrapJson["version"] = packageJson["version"]; npmShrinkwrapJson["version"] = packageJson["version"];
stripDevDeps(npmShrinkwrapJson); stripDevDeps(npmShrinkwrapJson);
stripDevDeps2(npmShrinkwrapJson);
fs.writeFileSync( fs.writeFileSync(
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> {
@@ -164,7 +173,7 @@ async function copyStatic(): Promise<void> {
"README.md", "README.md",
"CHANGELOG.md", "CHANGELOG.md",
"public/logo.svg", "public/logo.svg",
"public/favicon.png", "public/favicon.png"
]; ];
for (const file of files) { for (const file of files) {
@@ -185,53 +194,35 @@ async function generateCss(): Promise<void> {
stage: 3, stage: 3,
features: { features: {
"nesting-rules": true, "nesting-rules": true,
"color-mod-function": true, "color-mod-function": true
}, }
}), }),
cssnano, cssnano
]).process(cssIn, { from: cssInPath, to: cssOutPath }); ]).process(cssIn, { from: cssInPath, to: cssOutPath });
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 [ for (const bin of ["dump-data-model"]) {
"genieacs-cwmp", const inputFile = path.resolve(INPUT_DIR, `tools/${bin}`);
"genieacs-ext", const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`);
"genieacs-nbi",
"genieacs-fs",
"genieacs-ui",
]) {
const inputFile = path.resolve(INPUT_DIR, `bin/${bin}.ts`);
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
},
treeshake: {
propertyReadSideEffects: false,
moduleSideEffects: false,
}, },
plugins: [ plugins: [
rollupJson({ preferConst: true }), rollupReplace({
{ delimiters: ["", ""],
name: "", "#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": ""
resolveId: (importee, importer) => { }),
if (importee.endsWith("/package.json")) {
const p = path.resolve(path.dirname(importer), importee);
if (p === path.resolve(INPUT_DIR, "package.json"))
return path.resolve(OUTPUT_DIR, "package.json");
}
return null;
},
},
typescript({ typescript({
tsconfig: "./tsconfig.json", tsconfig: "./tsconfig.json",
include: [`bin/${bin}.ts`, "lib/**/*.ts"], include: [`tools/${bin}`, "lib/**/*.ts"]
}), }),
MODE === "production" ? terser() : null, MODE === "production" ? terser() : null
], ]
}); });
await bundle.write({ await bundle.write({
@@ -240,7 +231,7 @@ async function generateBackendJs(externals: string[]): Promise<void> {
sourcemap: "inline", sourcemap: "inline",
sourcemapExcludeSources: true, sourcemapExcludeSources: true,
banner: "#!/usr/bin/env node", banner: "#!/usr/bin/env node",
file: outputFile, file: outputFile
}); });
// Mark as executable // Mark as executable
@@ -249,78 +240,129 @@ async function generateBackendJs(externals: string[]): Promise<void> {
} }
} }
async function generateFrontendJs(externals: string[]): Promise<void> { async function generateBackendJs(): Promise<void> {
for (const bin of [
"genieacs-cwmp",
"genieacs-ext",
"genieacs-nbi",
"genieacs-fs",
"genieacs-ui"
]) {
const inputFile = path.resolve(INPUT_DIR, `bin/${bin}`);
const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`);
const bundle = await rollup({
input: inputFile,
external: externals,
acorn: {
allowHashBang: true
},
treeshake: {
propertyReadSideEffects: false,
pureExternalModules: true
},
plugins: [
rollupReplace({
delimiters: ["", ""],
"#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": ""
}),
rollupJson({ preferConst: true }),
{
resolveId: (importee, importer) => {
if (importee.endsWith("/package.json")) {
const p = path.resolve(path.dirname(importer), importee);
if (p === path.resolve(INPUT_DIR, "package.json"))
return path.resolve(OUTPUT_DIR, "package.json");
}
return null;
}
},
typescript({
tsconfig: "./tsconfig.json",
include: [`bin/${bin}`, "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 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 bundle = await rollup({ const bundle = await rollup({
input: inputFile, input: inputFile,
external: [ external: externals,
...builtins,
...externals.filter((e) => !inlineDeps.includes(e)),
],
plugins: [ plugins: [
rollupJson({ preferConst: true }), rollupJson({ preferConst: true }),
{ typescript({ tsconfig: "./tsconfig.json" })
name: "",
resolveId: function (importee, importer) {
if (importee.endsWith("/package.json")) {
const p = path.resolve(path.dirname(importer), importee);
if (p === path.resolve(INPUT_DIR, "package.json"))
return path.resolve(OUTPUT_DIR, "package.json");
}
return null;
},
},
typescript({ tsconfig: "./tsconfig.json" }),
nodeResolve(),
commonjs(),
MODE === "production" ? terser() : null,
], ],
preserveEntrySignatures: false, inlineDynamicImports: true,
treeshake: { treeshake: {
propertyReadSideEffects: false, propertyReadSideEffects: false,
moduleSideEffects: false, pureExternalModules: true
}, },
onwarn: ((warning, warn) => { onwarn: ((warning, warn) => {
// Ignore circular dependency warnings // Ignore circular dependency warnings
if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning); if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning);
}) as WarningHandler, }) as WarningHandler
}); });
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: "esm",
sourcemap: true, sourcemap: "inline",
sourcemapExcludeSources: true, sourcemapExcludeSources: true,
dir: outputDir, file: outputFile
}); });
const webpackConf = {
mode: MODE,
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"
}
};
const stats = await promisify(webpack)(webpackConf);
process.stdout.write(stats.toString({ colors: true }) + "\n");
} }
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: [
{
name: "preset-default",
params: {
overrides: {
removeViewBox: false,
},
},
},
],
});
symbols.push(generateSymbol(id, data)); symbols.push(generateSymbol(id, data));
} }
fs.writeFileSync( fs.writeFileSync(
@@ -330,21 +372,20 @@ 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 .catch(err => {
})
.catch((err) => {
process.stderr.write(err.stack + "\n"); process.stderr.write(err.stack + "\n");
}); });
}) })
.catch((err) => { .catch(err => {
process.stderr.write(err.stack + "\n"); process.stderr.write(err.stack + "\n");
}); });
-172
View File
@@ -1,172 +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
downloadSuccessOnTimeout
TransferComplete
nginx
ENCODEURICOMPONENT
pageSize
-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
+11 -12
View File
@@ -64,7 +64,7 @@ CWMP_PORT
CWMP_INTERFACE CWMP_INTERFACE
The network interface that genieacs-cwmp binds to. The network interface that genieacs-cwmp binds to.
Default: ``::`` Default: ``0.0.0.0``
CWMP_SSL_CERT CWMP_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.
@@ -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
@@ -102,12 +102,12 @@ NBI_PORT
NBI_INTERFACE NBI_INTERFACE
The network interface that genieacs-nbi binds to. The network interface that genieacs-nbi binds to.
Default: ``::`` Default: ``0.0.0.0``
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.
@@ -140,7 +140,7 @@ FS_PORT
FS_INTERFACE FS_INTERFACE
The network interface that genieacs-fs binds to. The network interface that genieacs-fs binds to.
Default: ``::`` Default: ``0.0.0.0``
FS_SSL_CERT FS_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.
@@ -164,13 +164,12 @@ FS_ACCESS_LOG_FILE
Default: unset Default: unset
FS_URL_PREFIX FS_HOSTNAME
The URL prefix (e.g. 'https://example.com:7657/') to use when generating the The hostname to use when generating the file URL for TR-069 Download
file URL for TR-069 Download requests. Set this if genieacs-fs and requests. Set this if the hostname where genieacs-fs is listening differs
genieacs-cwmp are behind a proxy or running on different servers. from the hostname in the ACS URL.
Default: auto generated based on the hostname from the ACS URL, FS_PORT Default: the hostname from the ACS URL
config, and whether or not SSL is enabled for genieacs-fs.
UI_WORKER_PROCESSES UI_WORKER_PROCESSES
The number of worker processes to spawn for genieacs-ui. A value of 0 means The number of worker processes to spawn for genieacs-ui. A value of 0 means
@@ -186,7 +185,7 @@ UI_PORT
UI_INTERFACE UI_INTERFACE
The network interface that genieacs-ui binds to. The network interface that genieacs-ui binds to.
Default: ``::`` Default: ``0.0.0.0``
UI_SSL_CERT UI_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.
+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;
+3 -4
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.
@@ -19,7 +19,7 @@ Prerequisites
.. topic:: Node.js .. topic:: Node.js
GenieACS requires Node.js 12.13 and up. Refer to https://nodejs.org/ for GenieACS requires Node.js 10.x and up. Refer to https://nodejs.org/ for
instructions. instructions.
.. topic:: MongoDB .. topic:: MongoDB
@@ -34,7 +34,7 @@ Install GenieACS
.. parsed-literal:: .. parsed-literal::
sudo npm install -g genieacs@\ |release| sudo npm install -g --unsafe-perm genieacs@\ |release|
.. topic:: Installing from source .. topic:: Installing from source
@@ -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
+7 -7
View File
@@ -18,8 +18,8 @@ differences between the scripts of provisions and virtual parameters are:
.. code:: javascript .. code:: javascript
// [<declared attr timestamps, declared attr values>, <current attr timestamps>, <current attr values>] // [<current attr timestamps>, <current attr values>, <declared attr timestamps, declared attr values>]
[{path: 1559849387191, value: 1559849387191}, {value: ["new val", "xsd:string"]}, {path: 1559840000000, value: 1559840000000}, {value: ["cur val", "xsd:string"]}] [{path: 1559840000000, value: 1559840000000}, {value: ["cur val", "xsd:string"]}, {path: 1559849387191, value: 1559849387191}, {value: ["new val", "xsd:string"]}]
- Virtual parameter scripts must return an object containing the attributes of - Virtual parameter scripts must return an object containing the attributes of
this parameter. this parameter.
@@ -69,9 +69,9 @@ Expose an external value as a virtual parameter
// Example: Expose an external value as a virtual parameter // Example: Expose an external value as a virtual parameter
let serial = declare("DeviceID.SerialNumber", {value: 1}); let serial = declare("DeviceID.SerialNumber", {value: 1});
if (args[1].value) { if (values.value) {
ext("example-ext", "set", serial.value[0], args[1].value[0]); ext("example-ext", "set", serial.value[0], values.value[0]);
return {writable: true, value: [args[1].value[0], "xsd:string"]}; return {writable: true, value: [values.value[0], "xsd:string"]};
} }
else { else {
let v = ext("example-ext", "get", serial.value[0]); let v = ext("example-ext", "get", serial.value[0]);
@@ -85,8 +85,8 @@ Create an editable virtual parameter for WPA passphrase
// Example: Create an editable virtual parameter for WPA passphrase // Example: Create an editable virtual parameter for WPA passphrase
let m = ""; let m = "";
if (args[1].value) { if (VALUES && VALUES.value) {
m = args[1].value[0]; m = VALUES.value[0];
declare("Device.WiFi.AccessPoint.1.Security.KeyPassphrase", null, {value: m}); declare("Device.WiFi.AccessPoint.1.Security.KeyPassphrase", null, {value: m});
declare("InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase", null, {value: m}); declare("InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase", null, {value: m});
} }
+113 -170
View File
@@ -17,90 +17,71 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { parse } from "url";
import * as db from "./db"; import * as db from "./db";
import * as common from "./common"; import * as common from "./common";
import * as cache from "./cache"; import * as cache from "./cache";
import { import {
getCurrentSnapshot, getCurrentSnapshot,
getConfig, getConfig,
getConfigExpression, getConfigExpression
} from "./local-cache"; } from "./local-cache";
import { import {
httpConnectionRequest, httpConnectionRequest,
udpConnectionRequest, udpConnectionRequest
} from "./connection-request"; } from "./connection-request";
import { Expression, Task } from "./types"; import { Expression, Task } from "./types";
import { flattenDevice } from "./mongodb-functions";
import { evaluate } from "./common/expression";
export async function connectionRequest( export async function connectionRequest(deviceId): Promise<void> {
deviceId: string, const options = {
device?: Record<string, { value?: [boolean | number | string, string] }> projection: {
): Promise<string> { _deviceId: 1,
if (!device) { "Device.ManagementServer.ConnectionRequestURL._value": 1,
const res = await db.devicesCollection.findOne({ _id: deviceId }); "Device.ManagementServer.UDPConnectionRequestAddress._value": 1,
if (!res) throw new Error("No such device"); "Device.ManagementServer.ConnectionRequestUsername._value": 1,
device = flattenDevice(res); "Device.ManagementServer.ConnectionRequestPassword._value": 1,
} "InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value": 1,
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value": 1,
let connectionRequestUrl, udpConnectionRequestAddress, username, password; "InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value": 1,
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
connectionRequestUrl = (device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
device[
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
] || {}
).value || [""])[0];
username = ((
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername"
] || {}
).value || [""])[0];
password = ((
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword"
] || {}
).value || [""])[0];
} else {
connectionRequestUrl = (device[
"Device.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
device["Device.ManagementServer.UDPConnectionRequestAddress"] || {}
).value || [""])[0];
username = ((
device["Device.ManagementServer.ConnectionRequestUsername"] || {}
).value || [""])[0];
password = ((
device["Device.ManagementServer.ConnectionRequestPassword"] || {}
).value || [""])[0];
}
const remoteAddress = new URL(connectionRequestUrl).hostname;
const evalCallback = (exp): Expression => {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM" && typeof exp[1] === "string") {
let name = exp[1];
if (name === "id") name = "DeviceID.ID";
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
else if (name === "productClass") name = "DeviceID.ProductClass";
else if (name === "oui") name = "DeviceID.OUI";
else if (name === "remoteAddress") return remoteAddress;
else if (name === "username") return username;
else if (name === "password") return password;
const p = device[name];
if (p?.value) return p.value[0];
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress;
else if (exp[1] === "USERNAME") return username;
else if (exp[1] === "PASSWORD") return password;
} }
return exp; };
const device = await db.devicesCollection.findOne({ _id: deviceId }, options);
if (!device) throw new Error("No such device");
let managementServer,
connectionRequestUrl,
udpConnectionRequestAddress,
username,
password;
if (device.Device)
// TR-181 data model
managementServer = device.Device.ManagementServer;
// TR-098 data model
else managementServer = device.InternetGatewayDevice.ManagementServer;
if (managementServer.ConnectionRequestURL)
connectionRequestUrl = managementServer.ConnectionRequestURL._value;
if (managementServer.UDPConnectionRequestAddress) {
udpConnectionRequestAddress =
managementServer.UDPConnectionRequestAddress._value;
}
if (managementServer.ConnectionRequestUsername)
username = managementServer.ConnectionRequestUsername._value;
if (managementServer.ConnectionRequestPassword)
password = managementServer.ConnectionRequestPassword._value;
const context = {
id: device["_id"],
serialNumber: device["_deviceId"]["SerialNumber"],
productClass: device["_deviceId"]["ProductClass"],
oui: device["_deviceId"]["OUI"],
remoteAddress: connectionRequestUrl
? parse(connectionRequestUrl).host
: null,
username: username || "",
password: password || ""
}; };
const snapshot = await getCurrentSnapshot(); const snapshot = await getCurrentSnapshot();
@@ -108,23 +89,20 @@ export async function connectionRequest(
const UDP_CONNECTION_REQUEST_PORT = +getConfig( const UDP_CONNECTION_REQUEST_PORT = +getConfig(
snapshot, snapshot,
"cwmp.udpConnectionRequestPort", "cwmp.udpConnectionRequestPort",
{}, context,
now, now
evalCallback
); );
const CONNECTION_REQUEST_TIMEOUT = +getConfig( const CONNECTION_REQUEST_TIMEOUT = +getConfig(
snapshot, snapshot,
"cwmp.connectionRequestTimeout", "cwmp.connectionRequestTimeout",
{}, context,
now, now
evalCallback
); );
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig( const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig(
snapshot, snapshot,
"cwmp.connectionRequestAllowBasicAuth", "cwmp.connectionRequestAllowBasicAuth",
{}, context,
now, now
evalCallback
); );
let authExp: Expression = getConfigExpression( let authExp: Expression = getConfigExpression(
snapshot, snapshot,
@@ -136,85 +114,64 @@ export async function connectionRequest(
"FUNC", "FUNC",
"AUTH", "AUTH",
["PARAM", "username"], ["PARAM", "username"],
["PARAM", "password"], ["PARAM", "password"]
] as Expression; ] as Expression;
} }
authExp = evaluate(authExp, {}, now, evalCallback); const debug = !!getConfig(snapshot, "cwmp.debug", context, now);
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback); let udpProm;
let udpProm = Promise.resolve(false);
if (udpConnectionRequestAddress) { if (udpConnectionRequestAddress) {
try { udpProm = udpConnectionRequest(
const u = new URL("udp://" + udpConnectionRequestAddress); udpConnectionRequestAddress,
udpProm = udpConnectionRequest( authExp,
u.hostname, context,
parseInt(u.port || "80"), UDP_CONNECTION_REQUEST_PORT,
authExp, debug,
UDP_CONNECTION_REQUEST_PORT, deviceId
debug, );
deviceId
).then(
() => true,
() => false
);
} catch (err) {
// Ignore invalid address
}
} }
const status = await httpConnectionRequest( try {
connectionRequestUrl, await httpConnectionRequest(
authExp, connectionRequestUrl,
CONNECTION_REQUEST_ALLOW_BASIC_AUTH, authExp,
CONNECTION_REQUEST_TIMEOUT, context,
debug, CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
deviceId CONNECTION_REQUEST_TIMEOUT,
); debug,
deviceId
if (await udpProm) return ""; );
} catch (err) {
return status; if (!udpProm) throw err;
await udpProm;
}
} }
export async function awaitSessionStart( export async function watchTask(deviceId, taskId, timeout): Promise<string> {
deviceId: string, await new Promise(resolve => setTimeout(resolve, 500));
lastInform: number,
timeout: number
): Promise<boolean> {
const now = Date.now();
const device = await db.devicesCollection.findOne(
{ _id: deviceId },
{ projection: { _lastInform: 1 } }
);
const li = (device["_lastInform"] as Date).getTime();
if (li > lastInform) return true;
const token = await cache.get(`cwmp_session_${deviceId}`);
if (token) return true;
if (timeout < 500) return false;
await new Promise((resolve) => setTimeout(resolve, 500));
timeout -= Date.now() - now;
return awaitSessionStart(deviceId, lastInform, timeout);
}
export async function awaitSessionEnd( const task = await db.tasksCollection.findOne(
deviceId: string, { _id: taskId },
timeout: number { projection: { _id: 1 } }
): Promise<boolean> { );
const now = Date.now(); if (!task) return "completed";
const token = await cache.get(`cwmp_session_${deviceId}`);
if (!token) return true; const q = { _id: `${deviceId}:task_${taskId}` };
if (timeout < 500) return false; const fault = await db.faultsCollection.findOne(q, {
await new Promise((resolve) => setTimeout(resolve, 500)); projection: { _id: 1 }
timeout -= Date.now() - now; });
return awaitSessionEnd(deviceId, timeout); if (fault) return "fault";
if ((timeout -= 500) <= 0) return "timeout";
return watchTask(deviceId, taskId, timeout);
} }
function sanitizeTask(task): void { function sanitizeTask(task): void {
task.timestamp = new Date(task.timestamp || Date.now()); task.timestamp = new Date(task.timestamp || Date.now());
if (task.expiry) { if (task.expiry) {
if (task.expiry instanceof Date || isNaN(task.expiry)) if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry))
task.expiry = new Date(task.expiry); task.expiry = new Date(task.expiry);
else task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000); else task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000);
} }
@@ -252,10 +209,6 @@ function sanitizeTask(task): void {
break; break;
case "refreshObject": case "refreshObject":
if (typeof task.objectName !== "string")
throw new Error("Missing 'objectName' property");
break;
case "deleteObject": case "deleteObject":
if (typeof task.objectName !== "string" || !task.objectName.length) if (typeof task.objectName !== "string" || !task.objectName.length)
throw new Error("Missing 'objectName' property"); throw new Error("Missing 'objectName' property");
@@ -292,39 +245,29 @@ function sanitizeTask(task): void {
case "provisions": case "provisions":
if ( if (
!Array.isArray(task.provisions) || !Array.isArray(task.provisions) ||
!task.provisions.every((arr) => !task.provisions.every(arr =>
arr.every( arr.every(
(s) => s => s == null || ["boolean", "number", "string"].includes(typeof s)
s == null || ["boolean", "number", "string"].includes(typeof s)
) )
) )
) )
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;
} }
export async function insertTasks(tasks: any[]): Promise<Task[]> { export async function insertTasks(tasks): Promise<Task[]> {
if (tasks && !Array.isArray(tasks)) tasks = [tasks]; if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE) tasks = [tasks];
else if (!tasks?.length) return tasks || []; else if (!tasks || tasks.length === 0) return tasks || [];
for (const task of tasks) { for (const task of tasks) {
sanitizeTask(task); sanitizeTask(task);
if (task.uniqueKey) { if (task.uniqueKey) {
await db.tasksCollection.deleteOne({ await db.tasksCollection.deleteOne({
device: task.device, device: task.device,
uniqueKey: task.uniqueKey, uniqueKey: task.uniqueKey
}); });
} }
} }
@@ -332,20 +275,20 @@ export async function insertTasks(tasks: any[]): Promise<Task[]> {
return tasks; return tasks;
} }
export async function deleteDevice(deviceId: string): Promise<void> { export async function deleteDevice(deviceId): Promise<void> {
await Promise.all([ await Promise.all([
db.tasksCollection.deleteMany({ device: deviceId }), db.tasksCollection.deleteMany({ device: deviceId }),
db.devicesCollection.deleteOne({ _id: deviceId }), db.devicesCollection.deleteOne({ _id: deviceId }),
db.faultsCollection.deleteMany({ db.faultsCollection.deleteMany({
_id: { _id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`, $regex: `^${common.escapeRegExp(deviceId)}\\:`
}, }
}), }),
db.operationsCollection.deleteMany({ db.operationsCollection.deleteMany({
_id: { _id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`, $regex: `^${common.escapeRegExp(deviceId)}\\:`
}, }
}), }),
cache.del(`${deviceId}_tasks_faults_operations`), cache.del(`${deviceId}_tasks_faults_operations`)
]); ]);
} }
+23 -18
View File
@@ -19,17 +19,15 @@
import { createHash, randomBytes, pbkdf2 } from "crypto"; import { createHash, randomBytes, pbkdf2 } from "crypto";
function parseHeaderFeilds(str: string): Record<string, string> { function parseHeaderFeilds(str: string): {} {
const res = {}; const res = {};
const parts = str.split(","); const parts = str.split(",");
let part: string; let part;
while ((part = parts.shift()) != null) { while ((part = parts.shift()) != null) {
const name = part.split("=", 1)[0]; const name = part.split("=", 1)[0];
if (name.length === part.length) { if (name.length === part.length)
if (!part.trim()) continue;
throw new Error("Unable to parse auth header"); throw new Error("Unable to parse auth header");
}
let value = part.slice(name.length + 1); let value = part.slice(name.length + 1);
if (!/^\s*"/.test(value)) { if (!/^\s*"/.test(value)) {
@@ -52,9 +50,7 @@ function parseHeaderFeilds(str: string): Record<string, string> {
return res; return res;
} }
export function parseAuthorizationHeader(authHeader: string): { export function parseAuthorizationHeader(authHeader): { method: string } {
method: string;
} {
authHeader = authHeader.trim(); authHeader = authHeader.trim();
const method = authHeader.split(" ", 1)[0]; const method = authHeader.split(" ", 1)[0];
const res = { method: method }; const res = { method: method };
@@ -76,9 +72,7 @@ export function parseAuthorizationHeader(authHeader: string): {
return res; return res;
} }
export function parseWwwAuthenticateHeader( export function parseWwwAuthenticateHeader(authHeader): {} {
authHeader: string
): Record<string, string> {
authHeader = authHeader.trim(); authHeader = authHeader.trim();
const method = authHeader.split(" ", 1)[0]; const method = authHeader.split(" ", 1)[0];
const res = { method: method }; const res = { method: method };
@@ -86,7 +80,7 @@ export function parseWwwAuthenticateHeader(
return res; return res;
} }
export function basic(username: string, password: string): string { export function basic(username, password): string {
return "Basic " + Buffer.from(`${username}:${password}`).toString("base64"); return "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
} }
@@ -103,12 +97,20 @@ export function digest(
nc?: string | Buffer nc?: string | Buffer
): string { ): string {
const ha1 = createHash("md5"); const ha1 = createHash("md5");
ha1.update(username).update(":").update(realm).update(":").update(password); ha1
.update(username)
.update(":")
.update(realm)
.update(":")
.update(password);
// TODO support "MD5-sess" algorithm directive // TODO support "MD5-sess" algorithm directive
const ha1d = ha1.digest("hex"); const ha1d = ha1.digest("hex");
const ha2 = createHash("md5"); const ha2 = createHash("md5");
ha2.update(httpMethod).update(":").update(uri); ha2
.update(httpMethod)
.update(":")
.update(uri);
if (qop === "auth-int") { if (qop === "auth-int") {
const bodyHash = createHash("md5") const bodyHash = createHash("md5")
@@ -120,7 +122,10 @@ export function digest(
const ha2d = ha2.digest("hex"); const ha2d = ha2.digest("hex");
const hash = createHash("md5"); const hash = createHash("md5");
hash.update(ha1d).update(":").update(nonce); hash
.update(ha1d)
.update(":")
.update(nonce);
if (qop) { if (qop) {
hash hash
.update(":") .update(":")
@@ -141,7 +146,7 @@ export function solveDigest(
uri: string | Buffer, uri: string | Buffer,
httpMethod: string | Buffer, httpMethod: string | Buffer,
body: string | Buffer, body: string | Buffer,
authHeader: Record<string, string> authHeader
): string { ): string {
const cnonce = randomBytes(8).toString("hex"); const cnonce = randomBytes(8).toString("hex");
const nc = "00000001"; const nc = "00000001";
@@ -178,7 +183,7 @@ export function solveDigest(
return authString; return authString;
} }
export function generateSalt(length: number): Promise<string> { export function generateSalt(length): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
randomBytes(length, (err, rand) => { randomBytes(length, (err, rand) => {
if (err) return void reject(err); if (err) return void reject(err);
@@ -187,7 +192,7 @@ export function generateSalt(length: number): Promise<string> {
}); });
} }
export function hashPassword(pass: string, salt: string): Promise<string> { export function hashPassword(pass, salt): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
pbkdf2(pass, salt, 10000, 128, "sha512", (err, hash) => { pbkdf2(pass, salt, 10000, 128, "sha512", (err, hash) => {
if (err) return void reject(err); if (err) return void reject(err);
+95 -73
View File
@@ -17,102 +17,124 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Collection } from "mongodb"; import { MongoClient, Collection } from "mongodb";
import { onConnect } from "./db";
import * as config from "./config"; import * as config from "./config";
const CLOCK_SKEW_TOLERANCE = 30000;
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL"); const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
let cacheCollection: Collection; let clientPromise: Promise<MongoClient>;
let mongoCollection: Collection;
let mongoTimeOffset = 0;
onConnect(async (db) => { export async function connect(): Promise<void> {
cacheCollection = db.collection("cache"); const MONGODB_CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
await cacheCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }); clientPromise = MongoClient.connect(MONGODB_CONNECTION_URL, {
}); useNewUrlParser: true
});
export async function get(key: string): Promise<string> { const db = (await clientPromise).db();
const res = await cacheCollection.findOne({ _id: key }); mongoCollection = db.collection("cache");
if (res) return res["value"]; await mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
return null; const now = Date.now();
const res = await db.command({ hostInfo: 1 });
mongoTimeOffset = res.system.currentTime.getTime() - now;
} }
export async function del(key: string): Promise<void> { export async function disconnect(): Promise<void> {
await cacheCollection.deleteOne({ _id: key }); if (clientPromise) await (await clientPromise).close();
}
export async function get(key): Promise<any> {
const expire = new Date(Date.now() - mongoTimeOffset);
if (Array.isArray(key)) {
const res = await mongoCollection.find({ _id: { $in: key } }).toArray();
const indices = {};
key.forEach((v, i) => {
indices[v] = i;
});
const values = [];
res.forEach(r => {
if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"];
});
return values;
} else {
const res = await mongoCollection.findOne({ _id: { $in: [key] } });
if (res && res["expire"] > expire) return res["value"];
return null;
}
}
export async function del(key): Promise<void> {
if (Array.isArray(key))
await mongoCollection.deleteMany({ _id: { $in: key } });
else await mongoCollection.deleteOne({ _id: key });
} }
export async function set( export async function set(
key: string, key: string,
value: string, value: string | number,
ttl: number = MAX_CACHE_TTL ttl: number = MAX_CACHE_TTL
): Promise<void> { ): Promise<void> {
const timestamp = new Date(); const expire = new Date(Date.now() - mongoTimeOffset + ttl * 1000);
const expire = new Date( await mongoCollection.replaceOne(
timestamp.getTime() + CLOCK_SKEW_TOLERANCE + ttl * 1000
);
await cacheCollection.replaceOne(
{ _id: key }, { _id: key },
{ _id: key, value, expire, timestamp }, { _id: key, value: value, expire: expire },
{ upsert: true } { upsert: true }
); );
} }
export async function pop(key: string): Promise<any> { export async function pop(key): Promise<any> {
const res = await cacheCollection.findOneAndDelete({ _id: key }); const res = await mongoCollection.findOneAndDelete({ _id: key });
if (res?.["value"]) return res["value"]["value"];
if (
res &&
res["value"] &&
+res["value"]["expire"] - (Date.now() - mongoTimeOffset)
)
return res["value"]["value"];
return null; return null;
} }
export async function acquireLock( export async function lock(lockName, ttl): Promise<Function> {
lockName: string, const token = Math.random()
ttl: number, .toString(36)
timeout = 0, .slice(2);
token = Math.random().toString(36).slice(2)
): Promise<string> { async function unlockOrExtend(extendTtl): Promise<void> {
try { if (!extendTtl) {
const now = Date.now(); const res = await mongoCollection.deleteOne({
const r = await cacheCollection.findOneAndUpdate( _id: lockName,
{ _id: lockName, token }, value: token
{ });
$set: { if (res["result"]["n"] !== 1) throw new Error("Lock expired");
value: token, } else {
expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE), const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000);
}, const res = await mongoCollection.updateOne(
$currentDate: { timestamp: true as unknown as Date }, { _id: lockName, value: token },
}, { expire: expire }
{ upsert: true, returnDocument: "after" } );
); if (res["result"]["n"] !== 1) throw new Error("Lock expired");
const v = r.value;
if (Math.abs(v["timestamp"].getTime() - now) > CLOCK_SKEW_TOLERANCE)
throw new Error("Database clock skew too great");
} catch (err) {
if (err.code === 11000) {
if (timeout > 0) {
return new Promise((resolve, reject) => {
const w = 50 + Math.random() * 50;
setTimeout(() => {
acquireLock(lockName, ttl, timeout - w, token).then(
resolve,
reject
);
}, w);
});
}
throw new Error("Failed to acquire lock");
} }
throw err;
} }
return token; const expireTest = new Date(Date.now() - mongoTimeOffset);
} const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000);
export async function releaseLock( try {
lockName: string, await mongoCollection.updateOne(
token: string { _id: lockName, expire: { $lte: expireTest } },
): Promise<void> { { $set: { value: token, expire: expireSet } },
const res = await cacheCollection.deleteOne({ { upsert: true }
_id: lockName, );
value: token, } catch (err) {
}); if (err && err.code === 11000) {
if (res.deletedCount !== 1) throw new Error("Lock expired"); await new Promise(resolve => setTimeout(resolve, 200));
return lock(lockName, ttl);
}
}
return unlockOrExtend;
} }
+7 -11
View File
@@ -17,14 +17,14 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import cluster, { Worker } from "cluster"; import cluster from "cluster";
import { cpus } from "os"; import { cpus } from "os";
import * as logger from "./logger"; import * as logger from "./logger";
let respawnTimestamp = 0; let respawnTimestamp = 0;
let crashes: number[] = []; let crashes: number[] = [];
function fork(): Worker { function fork(): cluster.Worker {
const w = cluster.fork(); const w = cluster.fork();
w.on("error", (err: NodeJS.ErrnoException) => { w.on("error", (err: NodeJS.ErrnoException) => {
// Avoid exception when attempting to kill the process just as it's exiting // Avoid exception when attempting to kill the process just as it's exiting
@@ -41,7 +41,7 @@ function restartWorker(worker, code, signal): void {
message: "Worker died", message: "Worker died",
pid: worker.process.pid, pid: worker.process.pid,
exitCode: null, exitCode: null,
signal: null, signal: null
}; };
if (code != null) msg.exitCode = code; if (code != null) msg.exitCode = code;
@@ -57,7 +57,7 @@ function restartWorker(worker, code, signal): void {
min2 = 0, min2 = 0,
min3 = 0; min3 = 0;
crashes = crashes.filter((n) => { crashes = crashes.filter(n => {
if (n > now - 60000) ++min1; if (n > now - 60000) ++min1;
else if (n > now - 120000) ++min2; else if (n > now - 120000) ++min2;
else if (n > now - 180000) ++min3; else if (n > now - 180000) ++min3;
@@ -72,7 +72,7 @@ function restartWorker(worker, code, signal): void {
logger.error({ logger.error({
message: "Too many crashes, exiting", message: "Too many crashes, exiting",
pid: process.pid, pid: process.pid
}); });
return; return;
} }
@@ -89,11 +89,7 @@ function restartWorker(worker, code, signal): void {
}, respawnTimestamp - now); }, respawnTimestamp - now);
} }
export function start( export function start(workerCount, servicePort, serviceAddress): void {
workerCount: number,
servicePort: number,
serviceAddress: string
): void {
cluster.on("listening", (worker, address) => { cluster.on("listening", (worker, address) => {
if ( if (
(address.addressType === 4 || address.addressType === 6) && (address.addressType === 4 || address.addressType === 6) &&
@@ -104,7 +100,7 @@ export function start(
message: "Worker listening", message: "Worker listening",
pid: worker.process.pid, pid: worker.process.pid,
address: address.address, address: address.address,
port: address.port, port: address.port
}); });
} }
}); });
+15 -19
View File
@@ -17,12 +17,22 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
export function generateDeviceId( export const UNDEFINED_TYPE = "[object Undefined]";
deviceIdStruct: Record<string, string> export const NULL_TYPE = "[object Null]";
): string { export const BOOLEAN_TYPE = "[object Boolean]";
export const NUMBER_TYPE = "[object Number]";
export const STRING_TYPE = "[object String]";
export const ARRAY_TYPE = "[object Array]";
export const OBJECT_TYPE = "[object Object]";
export const REGEXP_TYPE = "[object RegExp]";
export const DATE_TYPE = "[object Date]";
export const typeOf = (obj): string => Object.prototype.toString.call(obj);
export function generateDeviceId(deviceIdStruct): string {
// Escapes everything except alphanumerics and underscore // Escapes everything except alphanumerics and underscore
function esc(str): string { function esc(str): string {
return str.replace(/[^A-Za-z0-9_]/g, (chr) => { return str.replace(/[^A-Za-z0-9_]/g, chr => {
const buf = Buffer.from(chr, "utf8"); const buf = Buffer.from(chr, "utf8");
let rep = ""; let rep = "";
for (const b of buf) rep += "%" + b.toString(16).toUpperCase(); for (const b of buf) rep += "%" + b.toString(16).toUpperCase();
@@ -44,20 +54,6 @@ export function generateDeviceId(
} }
// Source: http://stackoverflow.com/a/6969486 // Source: http://stackoverflow.com/a/6969486
export function escapeRegExp(str: string): string { export function escapeRegExp(str): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
} }
export function encodeTag(tag: string): string {
return encodeURIComponent(tag)
.replace(
/[!~*'().]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
)
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
.replace(/%/g, "0x");
}
export function decodeTag(tag: string): string {
return decodeURIComponent(tag.replace(/0x(?=[0-9A-Z]{2})/g, "%"));
}
+10 -11
View File
@@ -21,15 +21,15 @@ import { PermissionSet, Expression } from "../types";
import { evaluate, or } from "./expression"; import { evaluate, or } from "./expression";
export default class Authorizer { export default class Authorizer {
private declare permissionSets: PermissionSet[]; private permissionSets: PermissionSet[];
private declare validatorCache: WeakMap< private validatorCache: WeakMap<
any, object,
(mutationType, mutation, any) => boolean (mutationType, mutation, any) => boolean
>; >;
private declare hasAccessCache: Map<string, boolean>; private hasAccessCache: Map<string, boolean>;
private declare getFilterCache: Map<string, Expression>; private getFilterCache: Map<string, Expression>;
public constructor(permissionSets: PermissionSet[]) { public constructor(permissionSets) {
this.permissionSets = permissionSets; this.permissionSets = permissionSets;
this.validatorCache = new WeakMap(); this.validatorCache = new WeakMap();
this.hasAccessCache = new Map(); this.hasAccessCache = new Map();
@@ -77,8 +77,8 @@ export default class Authorizer {
} }
public getValidator( public getValidator(
resourceType: string, resourceType,
resource: unknown resource
): (mutationType: string, mutation?: any, args?: any) => boolean { ): (mutationType: string, mutation?: any, args?: any) => boolean {
if (this.validatorCache.has(resource)) if (this.validatorCache.has(resource))
return this.validatorCache.get(resource); return this.validatorCache.get(resource);
@@ -108,7 +108,7 @@ export default class Authorizer {
mutation, mutation,
resourceType, resourceType,
object: resource, object: resource,
options: any, options: any
}; };
const valueFunction = (paramName): any => { const valueFunction = (paramName): any => {
@@ -124,8 +124,7 @@ export default class Authorizer {
if (value == null) break; if (value == null) break;
} }
} else if (object[entry]) { } else if (object[entry]) {
if (paramName) value = object[entry][paramName]; value = object[entry][paramName];
else value = object[entry];
} }
return value; return value;
-106
View File
@@ -1,106 +0,0 @@
/**
* 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/>.
*/
export function and(a: bigint, b: bigint): bigint {
return a & b;
}
export function or(a: bigint, b: bigint): bigint {
return a | b;
}
export function xor(a: bigint, b: bigint): bigint {
return a ^ b;
}
export function not(a: bigint): bigint {
return ~a;
}
export function lshift(a: bigint, b: bigint): bigint {
return a << b;
}
export function rshift(a: bigint, b: bigint): bigint {
return a >> b;
}
export function add(a: bigint, b: bigint): bigint {
return a + b;
}
export function sub(a: bigint, b: bigint): bigint {
return a - b;
}
export function mul(a: bigint, b: bigint): bigint {
return a * b;
}
export function div(a: bigint, b: bigint): bigint {
return a / b;
}
export function exp(a: bigint, b: bigint): bigint {
return a ** b;
}
export function rem(a: bigint, b: bigint): bigint {
return a % b;
}
export function toNumber(a: bigint): number {
return Number(a);
}
export function eq(a: bigint, b: bigint): boolean {
return a === b;
}
export function ne(a: bigint, b: bigint): boolean {
return a !== b;
}
export function lt(a: bigint, b: bigint): boolean {
return a < b;
}
export function lte(a: bigint, b: bigint): boolean {
return a <= b;
}
export function gt(a: bigint, b: bigint): boolean {
return a > b;
}
export function gte(a: bigint, b: bigint): boolean {
return a >= b;
}
export function asUintN(a: number, b: bigint): bigint {
return BigInt.asUintN(a, b);
}
export function asIntN(a: number, b: bigint): bigint {
return BigInt.asIntN(a, b);
}
const _BigInt = BigInt;
type _bigint = bigint;
export { _BigInt as BigInt, _bigint as bigint };
File diff suppressed because it is too large. Load diff
+285
View File
@@ -0,0 +1,285 @@
/**
* 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 { map, parseLikePattern } from "./expression-parser";
import { Expression } from "../types";
type CNF = number[][];
function or(cnf1: CNF, cnf2: CNF): CNF {
if (cnf1.length === 0) return cnf2;
else if (cnf2.length === 0) return cnf1;
if (cnf1.length > cnf2.length) {
const c = cnf1;
cnf1 = cnf2;
cnf2 = c;
}
const res = [];
for (let i = 0; i < cnf1.length; ++i)
for (let j = i; j < cnf2.length; ++j) res.push(cnf1[i].concat(cnf2[j]));
return res;
}
function likePatternIncludes(pat1, pat2, idx1 = 0, idx2 = 0): boolean {
while (idx1 < pat1.length && idx2 < pat2.length) {
if (pat1[idx1] === "\\%") {
for (let i = idx2; i <= pat2.length; ++i)
if (likePatternIncludes(pat1, pat2, idx1 + 1, i)) return true;
return false;
} else if (pat2[idx2] === "\\%") {
if (pat1[idx1] !== "\\%") return false;
} else if (pat1[idx1] === "\\_") {
// Ignore
} else if (pat2[idx2] === "\\_") {
return false;
} else if (pat2[idx2] !== pat1[idx1]) {
return false;
}
++idx1;
++idx2;
}
if (idx1 === pat1.length && idx2 === pat2.length) return true;
return false;
}
export function booleanCnf(exp: Expression): { vars: number; clauses: CNF } {
const ranges = new Map();
const likePatterns = new Map();
map(exp, e => {
if (!Array.isArray(e)) return e;
if (e[0] === "LIKE" || e[0] === "NOT LIKE") {
if (
typeof e[2] === "string" &&
(e.length >= 4 || (e[3] != null && !Array.isArray(e[3])))
) {
const key = JSON.stringify(e[1]);
let s = likePatterns.get(key);
if (!s) likePatterns.set(key, (s = new Map()));
s.set(`${e[3] || ""}:${e[2]}`, parseLikePattern(e[2], e[3]));
}
} else if ([">", ">=", "<", "<=", "=", "<>"].includes(e[0])) {
if (!Array.isArray(e[2])) {
const key = `${JSON.stringify(e[1])}:${typeof e[2]}`;
let s = ranges.get(key);
if (!s) ranges.set(key, (s = new Set()));
s.add(e[2]);
} else if (!Array.isArray(e[1])) {
const key = `${JSON.stringify(e[2])}:${typeof e[1]}`;
let s = ranges.get(key);
if (!s) ranges.set(key, (s = new Set()));
s.add(e[2]);
}
}
return e;
});
for (const [k, v] of ranges) {
if (k.endsWith(":number")) {
ranges.set(
k,
Array.from(v as number[]).sort((a: number, b: number) => a - b)
);
} else {
ranges.set(k, Array.from(v).sort());
}
}
const expressions: Map<string, number> = new Map();
const mutuallyExclusive: Map<string, Set<number>> = new Map();
function getVariable(lhs, op, rhs = ""): number {
const key = `${lhs}:${op}:${rhs}`;
let f = expressions.get(key);
if (!f) {
expressions.set(key, (f = expressions.size + 1));
let m = mutuallyExclusive.get(lhs);
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
m.add(f);
if (rhs || op !== "null") m.add(getVariable(lhs, "null"));
}
return f;
}
function getComparisonVariables(
lhs,
from,
fromInclusive,
to,
toInclusive
): number[] {
const type = from != null ? typeof from : typeof to;
const range = ranges.get(`${lhs}:${type}`);
const vars = [];
if (from == null) vars.push(`${lhs}:--${range[0]}`);
for (let i = 0; i < range.length; ++i) {
if (
i > 0 &&
(from == null || range[i] > from) &&
(to == null || range[i] <= to)
)
vars.push(`${lhs}:${range[i - 1]}--${range[i]}`);
if (
(range[i] > from && range[i] < to) ||
(range[i] === from && fromInclusive) ||
(range[i] === to && toInclusive)
)
vars.push(`${lhs}:${range[i]}`);
}
if (to == null) vars.push(`${lhs}:${range[range.length - 1]}--`);
return vars.map(key => {
let f = expressions.get(key);
if (!f) {
expressions.set(key, (f = expressions.size + 1));
let m = mutuallyExclusive.get(lhs);
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
m.add(f);
}
return f;
});
}
function getLikeVariables(lhs, pat, esc): number[] {
const vars = [];
const pats = likePatterns.get(lhs);
pat = pats.get(`${esc || ""}:${pat}`);
for (const p of pats.values()) {
if (pat === p || likePatternIncludes(pat, p))
vars.push(`${lhs}:like:${JSON.stringify(p)}`);
}
return vars.map(key => {
let f = expressions.get(key);
if (!f) expressions.set(key, (f = expressions.size + 1));
return f;
});
}
interface ComparisonOperators {
[name: string]: (v: string) => [string, boolean, string, boolean];
}
const comparisonOperators: ComparisonOperators = {
"=": v => [v, true, v, true],
">": v => [v, false, null, null],
">=": v => [v, true, null, null],
"<": v => [null, null, v, false],
"<=": v => [null, null, v, true]
};
function recursive(clause, negate): CNF {
let op = clause[0];
if ((op === "AND" && !negate) || (op === "OR" && negate)) {
let res = [];
for (let i = 1; i < clause.length; ++i)
res = res.concat(recursive(clause[i], negate));
return res;
} else if ((op === "OR" && !negate) || (op === "AND" && negate)) {
let res = [];
for (let i = 1; i < clause.length; ++i)
res = or(res, recursive(clause[i], negate));
return res;
} else if (op === "NOT") {
return recursive(clause[1], !negate);
} else if (op === "<>") {
const vars = getComparisonVariables(
JSON.stringify(clause[1]),
...comparisonOperators["="](clause[2])
);
if (!negate) return vars.map(x => [0 - x]);
else return [vars];
} else if (op === "IS NULL") {
const v = getVariable(JSON.stringify(clause[1]), "null");
if (negate) return [[0 - v]];
else return [[v]];
} else if (op === "IS NOT NULL") {
const v = getVariable(JSON.stringify(clause[1]), "null");
if (!negate) return [[0 - v]];
else return [[v]];
} else if (op in comparisonOperators) {
if (!Array.isArray(clause[2])) {
const vars = getComparisonVariables(
JSON.stringify(clause[1]),
...comparisonOperators[op](clause[2])
);
if (negate) return vars.map(x => [0 - x]);
else return [vars];
} else if (!Array.isArray(clause[1])) {
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
const vars = getComparisonVariables(
JSON.stringify(clause[2]),
...comparisonOperators[op](clause[1])
);
if (negate) return vars.map(x => [0 - x]);
else return [vars];
} else {
let lhs = JSON.stringify(clause[1]);
let rhs = JSON.stringify(clause[2]);
// For consistency
if (rhs > lhs) {
const t = rhs;
rhs = lhs;
lhs = t;
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
}
const v = getVariable(lhs, op, rhs);
if (negate) return [[0 - v]];
else return [[v]];
}
} else if (
(op === "LIKE" || op === "NOT LIKE") &&
!Array.isArray(clause[2]) &&
clause[2] != null &&
(clause.length >= 4 || clause[3] != null)
) {
if (op === "NOT LIKE") negate = !negate;
const vars = getLikeVariables(
JSON.stringify(clause[1]),
clause[2],
clause[3]
);
if (negate) return vars.map(x => [0 - x]);
else return [vars];
} else {
const v = getVariable(JSON.stringify(clause), "");
if (negate) return [[0 - v]];
else return [[v]];
}
}
const cnf = recursive(exp, false);
for (const m of mutuallyExclusive.values()) {
const ar: number[] = Array.from(m);
for (let i = 0; i < ar.length; ++i)
for (let j = i + 1; j < ar.length; ++j) cnf.push([0 - ar[i], 0 - ar[j]]);
}
return { vars: expressions.size, clauses: cnf };
}
+52 -121
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 parsimmon from "parsimmon"; import * as parsimmon from "parsimmon";
import { Expression } from "../types"; import { Expression } from "../types";
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n"). // Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
@@ -27,9 +27,9 @@ function interpretEscapes(str): string {
f: "\f", f: "\f",
n: "\n", n: "\n",
r: "\r", r: "\r",
t: "\t", t: "\t"
}; };
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/g, (_, escape) => { return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => {
const type = escape.charAt(0); const type = escape.charAt(0);
const hex = escape.slice(1); const hex = escape.slice(1);
if (type === "u") return String.fromCharCode(parseInt(hex, 16)); if (type === "u") return String.fromCharCode(parseInt(hex, 16));
@@ -40,10 +40,7 @@ function interpretEscapes(str): string {
}); });
} }
export function map( export function map(exp, callback): Expression {
exp: Expression,
callback: (e: Expression) => Expression
): Expression {
if (!Array.isArray(exp)) return callback(exp); if (!Array.isArray(exp)) return callback(exp);
let clone; let clone;
@@ -58,10 +55,7 @@ export function map(
return callback(clone || exp); return callback(clone || exp);
} }
export async function mapAsync( export async function mapAsync(exp, callback): Promise<Expression> {
exp: Expression,
callback: (e: Expression) => Promise<Expression>
): Promise<Expression> {
if (!Array.isArray(exp)) return callback(exp); if (!Array.isArray(exp)) return callback(exp);
let clone; let clone;
@@ -76,10 +70,7 @@ export async function mapAsync(
return callback(clone || exp); return callback(clone || exp);
} }
function binaryLeft( function binaryLeft(operatorsParser, nextParser): parsimmon.Parser<{}> {
operatorsParser: parsimmon.Parser<string>,
nextParser: parsimmon.Parser<Expression>
): parsimmon.Parser<Expression> {
return parsimmon.seqMap( return parsimmon.seqMap(
nextParser, nextParser,
parsimmon.seq(operatorsParser, nextParser).many(), parsimmon.seq(operatorsParser, nextParser).many(),
@@ -87,13 +78,15 @@ function binaryLeft(
rest.reduce((acc, ch) => { rest.reduce((acc, ch) => {
const [op, another] = ch; const [op, another] = ch;
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]); if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]);
if (Array.isArray(another) && op === another[0])
return [op, acc].concat(another.slice(1));
return [op, acc, another]; return [op, acc, another];
}, first) }, first)
); );
} }
const lang = parsimmon.createLanguage({ const lang = parsimmon.createLanguage({
ComparisonOperator: function () { ComparisonOperator: function() {
return parsimmon return parsimmon
.alt( .alt(
parsimmon.string(">="), parsimmon.string(">="),
@@ -105,10 +98,13 @@ const lang = parsimmon.createLanguage({
) )
.skip(parsimmon.optWhitespace); .skip(parsimmon.optWhitespace);
}, },
LikeOperator: function () { LikeOperator: function() {
return parsimmon return parsimmon
.alt( .alt(
parsimmon.regexp(/like/i).result("LIKE").desc("LIKE"), parsimmon
.regexp(/like/i)
.result("LIKE")
.desc("LIKE"),
parsimmon parsimmon
.regexp(/not\s+like/i) .regexp(/not\s+like/i)
.result("NOT LIKE") .result("NOT LIKE")
@@ -117,7 +113,7 @@ const lang = parsimmon.createLanguage({
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace); .skip(parsimmon.optWhitespace);
}, },
IsNullOperator: function () { IsNullOperator: function() {
return parsimmon return parsimmon
.alt( .alt(
parsimmon parsimmon
@@ -132,7 +128,7 @@ const lang = parsimmon.createLanguage({
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace); .skip(parsimmon.optWhitespace);
}, },
NotOperator: function () { NotOperator: function() {
return parsimmon return parsimmon
.regexp(/not/i) .regexp(/not/i)
.result("NOT") .result("NOT")
@@ -140,7 +136,7 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace) .skip(parsimmon.optWhitespace)
.desc("NOT"); .desc("NOT");
}, },
AndOperator: function () { AndOperator: function() {
return parsimmon return parsimmon
.regexp(/and/i) .regexp(/and/i)
.result("AND") .result("AND")
@@ -148,7 +144,7 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace) .skip(parsimmon.optWhitespace)
.desc("AND"); .desc("AND");
}, },
OrOperator: function () { OrOperator: function() {
return parsimmon return parsimmon
.regexp(/or/i) .regexp(/or/i)
.result("OR") .result("OR")
@@ -156,7 +152,7 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace) .skip(parsimmon.optWhitespace)
.desc("OR"); .desc("OR");
}, },
Parameter: function (r) { Parameter: function(r) {
return parsimmon return parsimmon
.alt( .alt(
parsimmon.regexp(/[a-zA-Z0-9_.*-]+/), parsimmon.regexp(/[a-zA-Z0-9_.*-]+/),
@@ -166,43 +162,48 @@ const lang = parsimmon.createLanguage({
) )
) )
.atLeast(1) .atLeast(1)
.map((x) => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]]) .map(x => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]])
.skip(parsimmon.optWhitespace) .skip(parsimmon.optWhitespace)
.desc("parameter"); .desc("parameter");
}, },
StringValueSql: function () { StringValueSql: function() {
return parsimmon return parsimmon
.regexp(/'([^']*)'/, 1) .regexp(/'([^']*)'/, 1)
.atLeast(1) .atLeast(1)
.skip(parsimmon.optWhitespace) .skip(parsimmon.optWhitespace)
.map((s) => s.join("'")) .map(s => s.join("'"))
.desc("string"); .desc("string");
}, },
StringValueJs: function () { StringValueJs: function() {
return parsimmon return parsimmon
.regexp(/"((?:\\.|.)*?)"/, 1) .regexp(/"((?:\\.|.)*?)"/, 1)
.skip(parsimmon.optWhitespace) .skip(parsimmon.optWhitespace)
.map(interpretEscapes) .map(interpretEscapes)
.desc("string"); .desc("string");
}, },
NumberValue: function () { NumberValue: function() {
return parsimmon return parsimmon
.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/) .regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace) .skip(parsimmon.optWhitespace)
.map(Number) .map(Number)
.desc("number"); .desc("number");
}, },
BooleanValue: function () { BooleanValue: function() {
return parsimmon return parsimmon
.alt( .alt(
parsimmon.regexp(/true/i).result(true).desc("TRUE"), parsimmon
parsimmon.regexp(/false/i).result(false).desc("FALSE") .regexp(/true/i)
.result(true)
.desc("TRUE"),
parsimmon
.regexp(/false/i)
.result(false)
.desc("FALSE")
) )
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace); .skip(parsimmon.optWhitespace);
}, },
NullValue: function () { NullValue: function() {
return parsimmon return parsimmon
.regexp(/null/i) .regexp(/null/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/)) .notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
@@ -210,73 +211,32 @@ const lang = parsimmon.createLanguage({
.result(null) .result(null)
.desc("NULL"); .desc("NULL");
}, },
FuncValue: function (r) { FuncValue: function(r) {
return parsimmon.seqMap( return parsimmon.seqMap(
parsimmon parsimmon
.regexp(/([a-zA-Z0-9_]+)/, 1) .regexp(/([a-zA-Z0-9_]+)/, 1)
.skip(parsimmon.optWhitespace) .skip(parsimmon.optWhitespace)
.desc("function"), .desc("function"),
r.ExpressionList.wrap( r.ValueExpression.sepBy(
parsimmon.string(",").skip(parsimmon.optWhitespace)
).wrap(
parsimmon.string("(").skip(parsimmon.optWhitespace), parsimmon.string("(").skip(parsimmon.optWhitespace),
parsimmon.string(")").skip(parsimmon.optWhitespace) parsimmon.string(")").skip(parsimmon.optWhitespace)
), ),
(f, args) => ["FUNC", f.toUpperCase()].concat(args) (f, args) => ["FUNC", f.toUpperCase()].concat(args)
); );
}, },
WhenPair: function (r) { Value: function(r) {
return parsimmon.seq(
parsimmon
.regexp(/when/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("WHEN")
.then(r.Expression),
parsimmon
.regexp(/then/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("THEN")
.then(r.Expression)
);
},
CaseStatement: function (r) {
return parsimmon.seqMap(
parsimmon
.regexp(/case/i)
.result("CASE")
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("CASE"),
r.WhenPair.many(),
parsimmon
.regexp(/else/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace)
.desc("ELSE")
.then(r.Expression)
.map((e) => [[true, e]])
.fallback(null)
.skip(
parsimmon
.regex(/end/i)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
)
.skip(parsimmon.optWhitespace),
(...arr) => arr.flat(2)
);
},
Value: function (r) {
return parsimmon.alt( return parsimmon.alt(
r.NullValue, r.NullValue,
r.BooleanValue, r.BooleanValue,
r.NumberValue, r.NumberValue,
r.StringValueSql, r.StringValueSql,
r.StringValueJs, r.StringValueJs,
r.FuncValue, r.FuncValue
r.CaseStatement
); );
}, },
ValueExpression: function (r) { ValueExpression: function(r) {
return binaryLeft( return binaryLeft(
parsimmon.string("||").skip(parsimmon.optWhitespace), parsimmon.string("||").skip(parsimmon.optWhitespace),
binaryLeft( binaryLeft(
@@ -285,11 +245,7 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace), .skip(parsimmon.optWhitespace),
binaryLeft( binaryLeft(
parsimmon parsimmon
.alt( .alt(parsimmon.string("*"), parsimmon.string("/"))
parsimmon.string("*"),
parsimmon.string("/"),
parsimmon.string("%")
)
.skip(parsimmon.optWhitespace), .skip(parsimmon.optWhitespace),
parsimmon.alt( parsimmon.alt(
r.Value, r.Value,
@@ -303,7 +259,7 @@ const lang = parsimmon.createLanguage({
) )
); );
}, },
Comparison: function (r) { Comparison: function(r) {
return parsimmon.alt( return parsimmon.alt(
parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]), parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]),
parsimmon.seqMap( parsimmon.seqMap(
@@ -333,16 +289,8 @@ const lang = parsimmon.createLanguage({
) )
); );
}, },
ExpressionList: function (r) { Expression: function(r) {
return r.Expression.sepBy( function unary(operatorsParser, nextParser): parsimmon.Parser<{}> {
parsimmon.string(",").skip(parsimmon.optWhitespace)
);
},
Expression: function (r) {
function unary(
operatorsParser: parsimmon.Parser<string>,
nextParser: parsimmon.Parser<Expression>
): parsimmon.Parser<Expression> {
return parsimmon.seq(operatorsParser, nextParser).or(nextParser); return parsimmon.seq(operatorsParser, nextParser).or(nextParser);
} }
@@ -353,7 +301,7 @@ const lang = parsimmon.createLanguage({
unary(r.NotOperator, r.Comparison.or(r.ValueExpression)) unary(r.NotOperator, r.Comparison.or(r.ValueExpression))
) )
).trim(parsimmon.optWhitespace); ).trim(parsimmon.optWhitespace);
}, }
}); });
export function parse(str: string): Expression { export function parse(str: string): Expression {
@@ -361,12 +309,7 @@ export function parse(str: string): Expression {
return lang.Expression.tryParse(str); return lang.Expression.tryParse(str);
} }
export function parseList(str: string): Expression[] { export function stringify(exp, level = 0): string {
if (!str) return [];
return lang.ExpressionList.tryParse(str);
}
export function stringify(exp: Expression, level = 0): string {
if (!Array.isArray(exp)) return JSON.stringify(exp); if (!Array.isArray(exp)) return JSON.stringify(exp);
const opLevels = { const opLevels = {
@@ -387,8 +330,7 @@ export function stringify(exp: Expression, level = 0): string {
"+": 31, "+": 31,
"-": 31, "-": 31,
"*": 32, "*": 32,
"/": 32, "/": 32
"%": 32,
}; };
const op = exp[0].toUpperCase(); const op = exp[0].toUpperCase();
@@ -402,7 +344,7 @@ export function stringify(exp: Expression, level = 0): string {
return wrap( return wrap(
`${exp[1]}(${exp `${exp[1]}(${exp
.slice(2) .slice(2)
.map((e) => stringify(e)) .map(e => stringify(e))
.join(", ")})` .join(", ")})`
); );
} else if (op === "PARAM") { } else if (op === "PARAM") {
@@ -412,7 +354,7 @@ export function stringify(exp: Expression, level = 0): string {
return wrap( return wrap(
exp[1] exp[1]
.slice(1) .slice(1)
.map((p) => { .map(p => {
if (typeof p === "string") return p; if (typeof p === "string") return p;
else return `{${stringify(p)}}`; else return `{${stringify(p)}}`;
}) })
@@ -439,17 +381,6 @@ export function stringify(exp: Expression, level = 0): string {
)}` )}`
); );
} }
} else if (op === "CASE") {
const parts: string[] = ["CASE"];
for (let i = 1; i < exp.length - 1; i += 2) {
if (!Array.isArray(exp[i]) && exp[i]) {
if (exp[i + 1] != null) parts.push("ELSE", stringify(exp[i + 1]));
break;
}
parts.push("WHEN", stringify(exp[i]), "THEN", stringify(exp[i + 1]));
}
parts.push("END");
return parts.join(" ");
} else if (op in opLevels) { } else if (op in opLevels) {
const parts = exp.slice(1).map((e, i) => { const parts = exp.slice(1).map((e, i) => {
return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0)); return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0));
@@ -462,7 +393,7 @@ export function stringify(exp: Expression, level = 0): string {
} }
} }
export function parseLikePattern(pat: string, esc: string): string[] { export function parseLikePattern(pat, esc): string[] {
const chars = pat.split(""); const chars = pat.split("");
for (let i = 0; i < chars.length; ++i) { for (let i = 0; i < chars.length; ++i) {
@@ -477,5 +408,5 @@ export function parseLikePattern(pat: string, esc: string): string[] {
while (chars[i + 1] === "%") chars[++i] = ""; while (chars[i + 1] === "%") chars[++i] = "";
} }
} }
return chars.filter((c) => c); return chars.filter(c => c);
} }
+167 -250
View File
@@ -18,11 +18,13 @@
*/ */
import { map, mapAsync, parseLikePattern } from "./expression-parser"; import { map, mapAsync, parseLikePattern } from "./expression-parser";
import { booleanCnf } from "./expression-cnf";
import { naiveDpll } from "./sat-solver";
import { Expression } from "../types"; import { Expression } from "../types";
const isArray = Array.isArray; const isArray = Array.isArray;
const regExpCache = new WeakMap<any, RegExp>(); const regExpCache = new WeakMap<object, RegExp>();
const REDUCE_SKIP = {}; const REDUCE_SKIP = {};
function reduce(exp, callback): Expression { function reduce(exp, callback): Expression {
@@ -42,7 +44,7 @@ function reduce(exp, callback): Expression {
return exp; return exp;
} }
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp { export function likePatternToRegExp(pat, esc = "", flags = ""): RegExp {
const convChars = { const convChars = {
"-": "\\-", "-": "\\-",
"/": "\\/", "/": "\\/",
@@ -61,252 +63,159 @@ export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
"{": "\\{", "{": "\\{",
"}": "\\}", "}": "\\}",
"\\%": ".*", "\\%": ".*",
"\\_": ".", "\\_": "."
}; };
let chars = parseLikePattern(pat, esc); let chars = parseLikePattern(pat, esc);
if (!chars.length) return new RegExp("^$", flags); if (!chars.length) return new RegExp("^$", flags);
chars = chars.map((c) => convChars[c] || c); chars = chars.map(c => convChars[c] || c);
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0]; chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
const l = chars.length - 1; const l = chars.length - 1;
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$"; chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
return new RegExp(chars.join(""), flags); return new RegExp(chars.join(""), flags);
} }
function compare( function evalExp(e: Expression): Expression {
a: boolean | number | string, if (e[0] === "AND") {
b: boolean | number | string return reduce(e, (a, b) => {
): number { if (!isArray(a)) return a ? b : a;
if (typeof a === "boolean") a = +a; if (!isArray(b)) return b ? a : b;
if (typeof b === "boolean") b = +b; return REDUCE_SKIP;
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1; });
return a > b ? 1 : a < b ? -1 : 0; } else if (e[0] === "OR") {
} return reduce(e, (a, b) => {
if (!isArray(a)) return a ? a : b;
function toNumber(a: boolean | number | string): number { if (!isArray(b)) return b ? b : a;
switch (typeof a) { return REDUCE_SKIP;
case "number": });
return a; } else if (e[0] === "NOT") {
case "boolean": if (!isArray(e[1])) return !e[1];
return +a; else if (e[1][0] === "NOT") return e[1][1];
case "string": } else if (e[0] === "IS NULL") {
return parseFloat(a) || 0; if (isArray(e[1])) return e;
} else if (e[1] == null) return true;
} else return null;
} else if (e[0] === "IS NOT NULL") {
function toString(a: boolean | number | string): string { if (isArray(e[1])) return e;
switch (typeof a) { else if (e[1] != null) return true;
case "string": else return null;
return a; } else if (e[0] === "LIKE") {
case "number": if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
return a.toString();
case "boolean":
return (+a).toString();
}
}
export function evaluateCallback(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "CASE") {
for (let i = 1; i < exp.length; i += 2) {
if (Array.isArray(exp[i])) return exp;
if (exp[i]) return exp[i + 1];
}
return null;
} else if (exp[0] === "FUNC") {
if (exp[1] === "COALESCE") {
const args: Expression[] = [];
for (let i = 2; i < exp.length; ++i) {
const e = exp[i];
if (e == null) continue;
args.push(e);
if (!Array.isArray(e)) break;
}
if (!args.length) return null;
if (args.length === 1) return args[0];
return ["FUNC", "COALESCE", ...args];
} else if (exp[1] === "UPPER") {
if (exp[2] == null) return null;
if (!isArray(exp[2])) return toString(exp[2]).toUpperCase();
} else if (exp[1] === "LOWER") {
if (exp[2] == null) return null;
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") {
if (exp[1] == null) return null;
} else if (exp[0] === "AND") {
for (let i = 1; i < exp.length; ++i)
if (!Array.isArray(exp[i]) && exp[i] != null && !exp[i]) return false;
const args: Expression[] = [];
for (let i = 1; i < exp.length; ++i) {
const ee = exp[i];
if (ee == null) return null;
if (Array.isArray(ee)) {
if (ee[0] === "AND") args.push(...ee.slice(1));
else args.push(ee);
}
}
if (!args.length) return true;
if (args.length === 1) args.push(true);
return ["AND", ...args];
} else if (exp[0] === "OR") {
const args: Expression[] = [];
for (let i = 1; i < exp.length; ++i) {
const ee = exp[i];
if (Array.isArray(ee)) {
if (ee[0] === "OR") args.push(...ee.slice(1));
else args.push(ee);
} else if (ee) {
return true;
}
}
if (!args.length) return exp.some((ee) => ee == null) ? null : false;
if (args.length === 1) args.push(false);
return ["OR", ...args];
} else if (exp[0] === "NOT") {
if (exp[1] == null) return null;
if (!isArray(exp[1])) return !exp[1];
else if (exp[1][0] === "NOT") return exp[1][1];
} else if (exp[0] === "IS NULL") {
if (isArray(exp[1])) return exp;
else if (exp[1] == null) return true;
else return false;
} else if (exp[0] === "IS NOT NULL") {
if (isArray(exp[1])) return exp;
else if (exp[1] != null) return true;
else return false;
} else if (exp[0] === "LIKE") {
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
else if ( else if (
exp[1] == null || e[1] == null ||
exp[2] == null || e[2] == null ||
((exp as any[]).length >= 4 && exp[3] == null) ((e as any[]).length >= 4 && e[3] == null)
) )
return null; return null;
let r = regExpCache.get(exp as any[]); let r = regExpCache.get(e as any[]);
if (!r) { if (!r) {
r = likePatternToRegExp(exp[2], exp[3]); r = likePatternToRegExp(e[2], e[3]);
regExpCache.set(exp as any[], r); regExpCache.set(e as any[], r);
} }
return r.test(exp[1]); return r.test(e[1]);
} else if (exp[0] === "NOT LIKE") { } else if (e[0] === "NOT LIKE") {
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp; if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
else if ( else if (
exp[1] == null || e[1] == null ||
exp[2] == null || e[2] == null ||
((exp as any[]).length >= 4 && exp[3] == null) ((e as any[]).length >= 4 && e[3] == null)
) )
return null; return null;
let r = regExpCache.get(exp as any[]); let r = regExpCache.get(e as any[]);
if (!r) { if (!r) {
r = likePatternToRegExp(exp[2], exp[3]); r = likePatternToRegExp(e[2], e[3]);
regExpCache.set(exp as any[], r); regExpCache.set(e as any[], r);
} }
return !r.test(exp[1]); return !r.test(e[1]);
} else if (exp[0] === "=") { } else if (e[0] === "=") {
if (exp[1] == null || exp[2] == null) return null; if (isArray(e[1]) || isArray(e[2])) return e;
if (isArray(exp[1]) || isArray(exp[2])) return exp; if (e[1] == null || e[2] == null) return null;
return compare(exp[1], exp[2]) === 0; return e[1] === e[2];
} else if (exp[0] === "<>") { } else if (e[0] === "<>") {
if (exp[1] == null || exp[2] == null) return null; if (isArray(e[1]) || isArray(e[2])) return e;
if (isArray(exp[1]) || isArray(exp[2])) return exp; if (e[1] == null || e[2] == null) return null;
return compare(exp[1], exp[2]) !== 0; return e[1] !== e[2];
} else if (exp[0] === ">") { } else if (e[0] === ">") {
if (exp[1] == null || exp[2] == null) return null; if (isArray(e[1]) || isArray(e[2])) return e;
if (isArray(exp[1]) || isArray(exp[2])) return exp; if (e[1] == null || e[2] == null) return null;
return compare(exp[1], exp[2]) > 0; return e[1] > e[2];
} else if (exp[0] === ">=") { } else if (e[0] === ">=") {
if (exp[1] == null || exp[2] == null) return null; if (isArray(e[1]) || isArray(e[2])) return e;
if (isArray(exp[1]) || isArray(exp[2])) return exp; if (e[1] == null || e[2] == null) return null;
return compare(exp[1], exp[2]) >= 0; return e[1] >= e[2];
} else if (exp[0] === "<") { } else if (e[0] === "<") {
if (exp[1] == null || exp[2] == null) return null; if (isArray(e[1]) || isArray(e[2])) return e;
if (isArray(exp[1]) || isArray(exp[2])) return exp; if (e[1] == null || e[2] == null) return null;
return compare(exp[1], exp[2]) < 0; return e[1] < e[2];
} else if (exp[0] === "<=") { } else if (e[0] === "<=") {
if (exp[1] == null || exp[2] == null) return null; if (isArray(e[1]) || isArray(e[2])) return e;
if (isArray(exp[1]) || isArray(exp[2])) return exp; if (e[1] == null || e[2] == null) return null;
return compare(exp[1], exp[2]) <= 0; return e[1] <= e[2];
} else if (exp[0] === "*") { } else if (e[0] === "*") {
return reduce(exp, (a, b) => { return reduce(e, (a, b) => {
if (a == null || b == null) return null; if (!isArray(a) && !isArray(b)) {
if (!isArray(a) && !isArray(b)) return toNumber(a) * toNumber(b); if (a == null || b == null) return null;
return a * b;
}
return REDUCE_SKIP; return REDUCE_SKIP;
}); });
} else if (exp[0] === "/") { } else if (e[0] === "/") {
return reduce(exp, (a, b, i) => { return reduce(e, (a, b, i) => {
if (a == null || b == null) return null; if (!isArray(a) && !isArray(b)) {
if (isArray(a) || isArray(b)) return REDUCE_SKIP; if (a == null || b == null) return null;
const n1 = toNumber(a); return i === 0 ? a / b : a * b;
const n2 = toNumber(b); }
if (i !== 0) return n1 * n2;
if (n2 === 0) return null;
return n1 / n2;
});
} else if (exp[0] === "+") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toNumber(a) + toNumber(b);
return REDUCE_SKIP; return REDUCE_SKIP;
}); });
} else if (exp[0] === "-") { } else if (e[0] === "+") {
return reduce(exp, (a, b, i) => { return reduce(e, (a, b) => {
if (a == null || b == null) return null; if (!isArray(a) && !isArray(b)) {
if (!isArray(a) && !isArray(b)) if (a == null || b == null) return null;
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b); return a + b;
}
return REDUCE_SKIP; return REDUCE_SKIP;
}); });
} else if (exp[0] === "%") { } else if (e[0] === "-") {
return reduce(exp, (a, b, i) => { return reduce(e, (a, b, i) => {
if (a == null || b == null) return null; if (!isArray(a) && !isArray(b)) {
if (isArray(a) || isArray(b) || i !== 0) return REDUCE_SKIP; if (a == null || b == null) return null;
const n1 = toNumber(a); return i === 0 ? a - b : a + b;
const n2 = Math.trunc(toNumber(b)); }
if (n2 === 0) return null; return REDUCE_SKIP;
return n1 % n2;
}); });
} else if (exp[0] === "||") { } else if (e[0] === "||") {
return reduce(exp, (a, b) => { return reduce(e, (a, b) => {
if (a == null || b == null) return null; if (!isArray(a) && !isArray(b)) {
if (!isArray(a) && !isArray(b)) return toString(a) + toString(b); if (a == null || b == null) return null;
return `${a}${b}`;
}
return REDUCE_SKIP; return REDUCE_SKIP;
}); });
} }
return exp; return e;
} }
export function evaluate( export function evaluate(
exp: Expression, exp,
obj: Record<string, unknown> | ((e: string) => Expression), obj,
now: number, now: number,
cb?: (e: Expression) => Expression cb?: Function
): string | number | boolean | null; ): string | number | boolean | null;
export function evaluate( export function evaluate(exp, obj?, now?: number, cb?: Function): Expression;
exp: Expression, export function evaluate(exp, obj?, now?: number, cb?: Function): Expression {
obj?: Record<string, unknown> | ((e: string) => Expression), return map(exp, e => {
now?: number,
cb?: (e: Expression) => Expression
): Expression;
export function evaluate(
exp: Expression,
obj?: Record<string, unknown> | ((e: string) => Expression),
now?: number,
cb?: (e: Expression) => Expression
): Expression {
return map(exp, (e) => {
if (cb) e = cb(e); if (cb) e = cb(e);
if (!isArray(e)) return e; if (!isArray(e)) return e;
if (e[0] === "FUNC" && e[1] === "NOW") { if (e[0] === "FUNC") {
if (now) return now; if (e[1] === "NOW") {
if (now) return now;
} else if (e[1] === "UPPER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
} else if (e[1] === "LOWER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
}
} else if (e[0] === "PARAM") { } else if (e[0] === "PARAM") {
if (e[1] == null) return null; if (e[1] == null) return null;
if (obj && !isArray(e[1])) { if (obj && !isArray(e[1])) {
@@ -319,92 +228,100 @@ export function evaluate(
return v; return v;
} }
} }
return evaluateCallback(e); return evalExp(e);
}); });
} }
export async function evaluateAsync( export async function evaluateAsync(
exp: Expression, exp,
obj: Record<string, unknown>, obj,
now: number, now: number,
cb?: (e: Expression) => Promise<Expression> cb?: Function
): Promise<string | number | boolean | null>; ): Promise<string | number | boolean | null>;
export async function evaluateAsync( export async function evaluateAsync(
exp: Expression, exp,
obj?: Record<string, unknown>, obj?,
now?: number, now?: number,
cb?: (e: Expression) => Promise<Expression> cb?: Function
): Promise<Expression>; ): Promise<Expression>;
export async function evaluateAsync( export async function evaluateAsync(
exp: Expression, exp,
obj?: Record<string, unknown>, obj?,
now?: number, now?: number,
cb?: (e: Expression) => Promise<Expression> cb?: Function
): Promise<Expression> { ): Promise<Expression> {
return mapAsync(exp, async (e) => { return mapAsync(exp, async e => {
if (cb) e = await cb(e); if (cb) e = await cb(e);
if (!isArray(e)) return e; if (!isArray(e)) return e;
if (e[0] === "FUNC") { if (e[0] === "FUNC") {
if (e[1] === "NOW") if (now) return now; if (e[1] === "NOW") {
if (now) return now;
} else if (e[1] === "UPPER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
} else if (e[1] === "LOWER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
}
} else if (e[0] === "PARAM") { } else if (e[0] === "PARAM") {
if (e[1] == null) return null; if (e[1] == null) return null;
if (obj && !isArray(e[1])) { if (obj && !isArray(e[1])) {
let v = obj[e[1]]; let v = obj[e[1]];
if (v == null) return null; if (v == null) return null;
if (typeof v === "object") v = v["value"] ? v["value"][0] : null; if (typeof v === "object") v = v.value ? v.value[0] : null;
return v as Expression; return v;
} }
} }
return evaluateCallback(e); return evalExp(e);
}); });
} }
export function and(exp1: Expression, exp2: Expression): Expression { export function and(exp1: Expression, exp2: Expression): Expression {
if (exp1 != null && !exp1) return false; if (!isArray(exp1)) return exp1 ? exp2 : exp1;
if (exp2 != null && !exp2) return false; if (!isArray(exp2)) return exp2 ? exp1 : exp2;
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
if (exp1 == null || exp2 == null) return null;
return true;
}
if (!Array.isArray(exp2) && exp1[0] === "AND") return exp1;
if (!Array.isArray(exp1) && exp2[0] === "AND") return exp2;
const res: Expression = ["AND"]; let res: Expression = ["AND"];
if (Array.isArray(exp1) && exp1[0] === "AND") res.push(...exp1.slice(1)); if (exp1[0] === "AND") res = res.concat(exp1.slice(1));
else res.push(exp1); else res.push(exp1);
if (Array.isArray(exp2) && exp2[0] === "AND") res.push(...exp2.slice(1)); if (exp2[0] === "AND") res = res.concat(exp2.slice(1));
else res.push(exp2); else res.push(exp2);
return res; return res;
} }
export function or(exp1: Expression, exp2: Expression): Expression { export function or(exp1: Expression, exp2: Expression): Expression {
if (!Array.isArray(exp1) && exp1) return true; if (!isArray(exp1)) return exp1 ? exp1 : exp2;
if (!Array.isArray(exp2) && exp2) return true; if (!isArray(exp2)) return exp2 ? exp2 : exp1;
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
if (exp1 == null || exp2 == null) return null;
return false;
}
if (!Array.isArray(exp2) && exp1[0] === "OR") return exp1;
if (!Array.isArray(exp1) && exp2[0] === "OR") return exp2;
const res: Expression = ["OR"]; let res: Expression = ["OR"];
if (Array.isArray(exp1) && exp1[0] === "OR") res.push(...exp1.slice(1)); if (exp1[0] === "OR") res = res.concat(exp1.slice(1));
else res.push(exp1); else res.push(exp1);
if (Array.isArray(exp2) && exp2[0] === "OR") res.push(...exp2.slice(1)); if (exp2[0] === "OR") res = res.concat(exp2.slice(1));
else res.push(exp2); else res.push(exp2);
return res; return res;
} }
export function extractParams(exp: Expression): Expression[] { export function not(exp): Expression {
if (isArray(exp) && exp[0] === "NOT") return exp[1];
return ["NOT", exp];
}
export function subset(exp1, exp2): boolean {
const e = evaluate(["NOT", ["OR", ["NOT", exp1], exp2]]);
if (!isArray(e)) return !e;
const { vars, clauses } = booleanCnf(e);
return !naiveDpll(clauses, vars);
}
export function extractParams(exp): Expression[] {
const params = new Set<Expression>(); const params = new Set<Expression>();
map(exp, (e) => { map(exp, e => {
if (isArray(e) && e[0] === "PARAM") params.add(e[1]); if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
return e; return e;
}); });
+4 -13
View File
@@ -39,7 +39,7 @@ function getKey(obj): string {
return k; return k;
} }
export default function memoize<T extends (...args: any[]) => any>(func: T): T { export default function memoize<T extends Function>(func: T): T {
const funcKey = getKey(func); const funcKey = getKey(func);
return ((...args) => { return ((...args) => {
const key = JSON.stringify(args.map(getKey)) + funcKey; const key = JSON.stringify(args.map(getKey)) + funcKey;
@@ -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;
} }
+10 -10
View File
@@ -20,9 +20,9 @@
import Path from "./path"; import Path from "./path";
export default class PathSet { export default class PathSet {
private declare lengthIndex: Set<Path>[]; private lengthIndex: Set<Path>[];
private declare fragmentIndex: Map<string, Set<Path>>[]; private fragmentIndex: Map<string, Set<Path>>[];
private declare stringIndex: Map<string, Path>; private stringIndex: Map<string, Path>;
public constructor() { public constructor() {
this.lengthIndex = []; this.lengthIndex = [];
@@ -73,8 +73,8 @@ export default class PathSet {
public find( public find(
path: Path, path: Path,
superset = false, superset: boolean = false,
subset = false, subset: boolean = false,
depth: number = path.length depth: number = path.length
): Path[] { ): Path[] {
if (path.alias) throw new Error("PathSet does not support aliased paths"); if (path.alias) throw new Error("PathSet does not support aliased paths");
@@ -106,24 +106,24 @@ export default class PathSet {
if (!fragmentIndexSet1) { if (!fragmentIndexSet1) {
if (!fragmentIndexSet2) return []; if (!fragmentIndexSet2) return [];
if (!res) res = [...fragmentIndexSet2]; if (!res) res = [...fragmentIndexSet2];
else res = res.filter((r) => fragmentIndexSet2.has(r)); else res = res.filter(r => fragmentIndexSet2.has(r));
} else if (!fragmentIndexSet2) { } else if (!fragmentIndexSet2) {
if (!res) res = [...fragmentIndexSet1]; if (!res) res = [...fragmentIndexSet1];
else res = res.filter((r) => fragmentIndexSet1.has(r)); else res = res.filter(r => fragmentIndexSet1.has(r));
} else { } else {
if (!res) { if (!res) {
res = [...fragmentIndexSet1, ...fragmentIndexSet2]; res = [...fragmentIndexSet1, ...fragmentIndexSet2];
} else { } else {
res = res.filter( res = res.filter(
(r) => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r) r => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r)
); );
} }
} }
if (!res.length) return res; if (!res.length) return res;
} }
if (!res) res = [].concat(...lengthIndex.map((a) => [...a])); if (!res) res = [].concat(...lengthIndex.map(a => [...a]));
else res = res.filter((r) => lengthIndex.some((a) => a.has(r))); else res = res.filter(r => lengthIndex.some(a => a.has(r)));
return res; return res;
} }
+56 -112
View File
@@ -23,102 +23,44 @@ type Alias = [Path, string][];
let cache1 = new Map<string, Path>(); let cache1 = new Map<string, Path>();
let cache2 = new Map<string, Path>(); let cache2 = new Map<string, Path>();
const CHAR_SPACE = 32;
const CHAR_BACKSLASH = 92;
const CHAR_COLON = 58;
const CHAR_QUOTE = 34;
const CHAR_OPEN_BRACKET = 91;
const CHAR_CLOSE_BRACKET = 93;
const CHAR_ASTERISK = 42;
const CHAR_COMMA = 44;
const CHAR_DOT = 46;
function charCodeAt(str: string, index: number): number {
if (index >= str.length) return 0;
return str.charCodeAt(index);
}
function legalChar(c: number): boolean {
return (
(c >= 97 && c <= 122) ||
(c >= 65 && c <= 90) ||
(c >= 48 && c <= 57) ||
c === 95 ||
c === 45
);
}
export default class Path { export default class Path {
public declare readonly segments: Segments; public readonly segments: Segments;
public declare readonly wildcard: number; public readonly wildcard: number;
public declare readonly alias: number; public readonly alias: number;
protected declare _string: string; protected _string: string;
protected declare _stringIndex: number[]; protected _stringIndex: number[];
protected static parseAliasValue(
pattern: string,
index: number
): { index: number; value: string } {
let i = index;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
if (charCodeAt(pattern, i) === CHAR_QUOTE) {
for (let j = i + 1; j < pattern.length; ++j) {
if (
pattern.charCodeAt(j) === CHAR_QUOTE &&
pattern.charCodeAt(j - 1) !== CHAR_BACKSLASH
) {
try {
++j;
const v = JSON.parse(pattern.slice(i, j));
return { index: j, value: v };
} catch {
return { index, value: null };
}
}
}
return { index, value: null };
}
for (; i < pattern.length; ++i) {
const c = pattern.charCodeAt(i);
if (c === CHAR_CLOSE_BRACKET || c === CHAR_COMMA) break;
}
return { index: i, value: pattern.slice(index, i).trim() };
}
protected static parseAliasPath(
pattern: string,
index: number
): { index: number; path: Path } {
let i = index;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
const { index: idx, segments } = Path.parsePath(pattern, i);
if (idx === i) return { index, path: null };
i = idx;
while (charCodeAt(pattern, i) === CHAR_SPACE) ++i;
if (charCodeAt(pattern, i) !== CHAR_COLON) return { index, path: null };
return { index: i + 1, path: new Path(segments) };
}
protected static parseAlias( protected static parseAlias(
pattern: string, pattern: string,
index: number index: number
): { index: number; alias: Alias } { ): { index: number; alias: Alias } {
const alias: Alias = []; const alias: Alias = [];
let i = index; while (index < pattern.length && pattern[index] !== "]") {
for (;;) { const { index: idx, segments } = Path.parsePath(pattern, index);
let path: Path; let j = (index = idx + 1);
let value: string; while (pattern[j] !== "]" && pattern[j] !== ",") {
({ index: i, path } = Path.parseAliasPath(pattern, i)); if (pattern[j] === '"' && index === j) {
if (!path) break; ++j;
({ index: i, value } = Path.parseAliasValue(pattern, i)); while (pattern[j] !== '"' || pattern[j - 1] === "\\") {
if (value == null) break; if (++j >= pattern.length)
alias.push([path, value]); throw new Error("Invalid alias expression");
index = i; }
}
if (++j >= pattern.length) throw new Error("Invalid alias expression");
}
if (charCodeAt(pattern, index) !== CHAR_COMMA) break; let value = pattern.slice(index, j).trim();
++i; index = j;
if (value[0] === '"') {
try {
value = JSON.parse(value);
} catch (error) {
throw new Error("Invalid alias expression");
}
}
alias.push([new Path(segments), value]);
if (pattern[index] === ",") ++index;
} }
// Ensure identical expressions have idential string representation // Ensure identical expressions have idential string representation
@@ -139,26 +81,29 @@ export default class Path {
index: number index: number
): { index: number; segments: Segments } { ): { index: number; segments: Segments } {
const segments = []; const segments = [];
// Colon separator is needed for parseAlias
if (index < pattern.length && pattern[index] !== ":") {
for (;;) {
if (pattern[index] === "[") {
const { index: idx, alias } = Path.parseAlias(pattern, index + 1);
index = idx + 1;
segments.push(alias);
} else {
const j = index;
while (
index < pattern.length &&
pattern[index] !== ":" &&
pattern[index] !== "."
)
++index;
const s = pattern.slice(j, index).trim();
segments.push(s);
}
let segStart = index; if (index >= pattern.length || pattern[index] === ":") break;
let wildcard = -1; else if (pattern[index] !== ".")
for (let i = index; i <= pattern.length; ++i) { throw new Error("Invalid alias expression");
const c = charCodeAt(pattern, i); ++index;
if (c === CHAR_OPEN_BRACKET && i === segStart) {
const { index: idx, alias } = Path.parseAlias(pattern, i + 1);
if (charCodeAt(pattern, idx) !== CHAR_CLOSE_BRACKET) break;
segments.push(alias);
index = i = idx + 1;
segStart = i + 1;
} else if (c === CHAR_ASTERISK && i === segStart) {
wildcard = i + 1;
} else if (!legalChar(c) || i === wildcard) {
if (i === segStart) break;
const s = pattern.slice(segStart, i);
segments.push(s);
index = i;
if (c !== CHAR_DOT) break;
segStart = i + 1;
} }
} }
@@ -173,7 +118,7 @@ export default class Path {
if (Array.isArray(s)) { if (Array.isArray(s)) {
alias |= 1 << i; alias |= 1 << i;
const parts = s.map( const parts = s.map(
(al) => `${al[0].toString()}:${JSON.stringify(al[1])}` al => `${al[0].toString()}:${JSON.stringify(al[1])}`
); );
return `[${parts.join(",")}]`; return `[${parts.join(",")}]`;
} else if (s === "*") { } else if (s === "*") {
@@ -197,8 +142,7 @@ export default class Path {
if (!path) { if (!path) {
path = cache2.get(str); path = cache2.get(str);
if (!path) { if (!path) {
const { index, segments } = Path.parsePath(str, 0); const { segments } = Path.parsePath(str, 0);
if (index < str.length) throw new Error("Invalid parameter path");
path = new Path(segments); path = new Path(segments);
if (path.toString() !== str) cache1.set(path.toString(), path); if (path.toString() !== str) cache1.set(path.toString(), path);
} }
@@ -215,7 +159,7 @@ export default class Path {
return this._string; return this._string;
} }
public slice(start = 0, end: number = this.segments.length): Path { public slice(start: number = 0, end: number = this.segments.length): Path {
if (start < 0) start = Math.max(0, this.segments.length + start); if (start < 0) start = Math.max(0, this.segments.length + start);
if (end < 0) end = Math.max(0, this.segments.length + end); if (end < 0) end = Math.max(0, this.segments.length + end);
@@ -267,7 +211,7 @@ export default class Path {
public stripAlias(): Path { public stripAlias(): Path {
if (!this.alias) return this; if (!this.alias) return this;
const segments = this.segments.map((s) => (Array.isArray(s) ? "*" : s)); const segments = this.segments.map(s => (Array.isArray(s) ? "*" : s));
const str = segments.join("."); const str = segments.join(".");
let path = cache1.get(str); let path = cache1.get(str);
+175
View File
@@ -0,0 +1,175 @@
/**
* Naive DPLL solver for propositional logic adapted from
* https://github.com/tammet/logictools
*
* Origianl code by Tanel Tammet released under the MIT license.
*
* Check whether a clause set is satisfiable or contradictory, using a naive
* implementation of the dpll (Davis-Putnam-Logemann-Loveland) procedure
* combining truth table search with the resolution-like unit propagation.
*
* The standard pure-literal rule is not implemented in this naive version,
* since it is not really crucial for the algorithm.
*
* The code is very similar to the pure table solver, except we use a
* unit-propagating, derived-varlist-returing satisfiableAt.
*/
/* eslint-disable @typescript-eslint/no-use-before-define */
function satisfiableAt(clauses, varVals, varnr, val): boolean {
if (varnr !== 0) varVals[varnr] = val;
const propRes = unitPropagate(clauses, varVals);
if (propRes === 1) {
varVals[varnr] = 0;
return true;
}
// if result false (-1) unitPropagate restores the varVals state itself
if (propRes === -1) {
varVals[varnr] = 0;
return false;
}
// find next unassigned var to split
let nextVar = 0;
for (let i = 1; i < varVals.length; i++) {
if (varVals[i] === 0) {
nextVar = i;
break;
}
}
if (nextVar === 0) throw new Error("Error in satisfiableAt");
if (
satisfiableAt(clauses, varVals, nextVar, 1) ||
satisfiableAt(clauses, varVals, nextVar, -1)
)
// not necessary to restore split var: search is halted anyway
return true;
// restore split var
varVals[nextVar] = 0;
// restore derived vars
for (let i = 0; i < propRes.length; i++) varVals[propRes[i]] = 0;
return false;
}
/**
* Check whether clauses are satisfiable under the varVals assignment, without
* search.
*
* Iterate through all clauses and check whether
*
* - all literals in the clause are assigned false: undo derived units (set
* values in varVals to 0) and return false immediately
*
* - a clause contains a literal assigned true: skip further actions on this
* clause
*
* - a single literal in the clause is unassigned, all the rest are assigned
* false: derive this literal as a unit clause.
*
* In case at least one literal is derived during the process above, iterate
* the whole process until no more new literals are derived.
*
* Returns:
*
* 1: all clauses were assigned true, hence the clause set is satisfiable.
* The whole search will terminate after that.
*
* -1: clause set is contradictory. Derived units have been restored.
*
* a list of derived variables: the status of the clause set is
* undetermined. The values of variables on the list will be restored (set to
* 0) during backtracking.
*/
function unitPropagate(clauses, varVals): number[] | 1 | -1 {
// all vars with a value derived in the forthcoming main loop
const derivedVars = [];
// the main loop is run, deriving new unit clauses, until either:
// - the clause set is detected to be unsatisfiable
// - the clause set is detected to be satisfiable
// - there are no more derived unit clauses
for (;;) {
let allClausesFound = true; // set to false if we find an undetermined clause
const derivedQueue = []; // derived units (literals) during one iteration
for (let i = 0; i < clauses.length; i++) {
const clause = clauses[i];
let clauseVal = 0;
let unassignedCount = 0; // count unassigned vars in clause
let unassignedLit = 0; // 0 means none found for this clause
for (let j = 0; j < clause.length; j++) {
const lit = clause[j];
let nr, polarity;
if (lit < 0) {
nr = 0 - lit;
polarity = -1;
} else {
nr = lit;
polarity = 1;
}
if (varVals[nr] === polarity) {
clauseVal = 1;
break;
} else if (varVals[nr] === 0) {
unassignedCount++;
unassignedLit = lit;
}
}
if (clauseVal === 1) continue; // clause is subsumed
// clause is not subsumed by varVals
if (unassignedCount === 0) {
for (let j = 0; j < derivedVars.length; j++)
varVals[derivedVars[j]] = 0; // restore derived
return -1;
} else if (unassignedCount === 1) {
// unassignedLit is a derived literal
derivedQueue.push(unassignedLit);
}
if (unassignedCount !== 0) allClausesFound = false;
}
// if all clauses were subsumed or all the variables in a clause had a value
// no need to restore varVals: whole search finished
if (allClausesFound) return 1;
// in case there are any derived units present, store to varVals and
// iterate the main loop
if (derivedQueue.length === 0) break;
// some units were derived: iterate the main loop
for (let j = 0; j < derivedQueue.length; j++) {
const lit = derivedQueue[j];
let nr, polarity;
if (lit < 0) {
nr = 0 - lit;
polarity = -1;
} else {
nr = lit;
polarity = 1;
}
varVals[nr] = polarity;
derivedVars.push(nr);
}
}
return derivedVars;
}
export function naiveDpll(clauses, maxvarnr): boolean {
// variable values are 0 if not set, 1 if positive, -1 if negative
const varVals = new Int32Array(maxvarnr + 1);
return satisfiableAt(clauses, varVals, 0, 0);
}
-218
View File
@@ -1,218 +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 lines: string[] = [];
let idx = 0;
let cand = 0;
for (let i = 1; i < str.length - 1; ++i) {
if (str[i] !== " ") continue;
if (str[i + 1] === " ") {
i += 2;
while (str[i] === " ") ++i;
continue;
}
if (i <= idx + LINE_WIDTH) {
cand = i;
continue;
}
const c = cand > idx ? cand : i;
lines.push(str.slice(idx, c));
idx = c + 1;
cand = i;
}
if (cand > idx && str.length > idx + LINE_WIDTH) {
lines.push(str.slice(idx, cand));
idx = cand + 1;
}
lines.push(str.slice(idx));
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";
}
+56 -31
View File
@@ -32,18 +32,18 @@ while (!existsSync(`${ROOT_DIR}/package.json`)) {
} }
// For compatibility with v1.1 // For compatibility with v1.1
let configDir, cwmpSsl, nbiSsl, fsSsl, uiSsl, fsHostname; let configDir, cwmpSsl, nbiSsl, fsSsl, uiSsl;
const options = { const options = {
EXT_DIR: { type: "path", default: resolve(ROOT_DIR, "config/ext") }, EXT_DIR: { type: "path", default: resolve(ROOT_DIR, "config/ext") },
MONGODB_CONNECTION_URL: { MONGODB_CONNECTION_URL: {
type: "string", type: "string",
default: "mongodb://127.0.0.1/genieacs", default: "mongodb://127.0.0.1/genieacs"
}, },
CWMP_WORKER_PROCESSES: { type: "int", default: 0 }, CWMP_WORKER_PROCESSES: { type: "int", default: 0 },
CWMP_PORT: { type: "int", default: 7547 }, CWMP_PORT: { type: "int", default: 7547 },
CWMP_INTERFACE: { type: "string", default: "::" }, CWMP_INTERFACE: { type: "string", default: "0.0.0.0" },
CWMP_SSL_CERT: { type: "string", default: "" }, CWMP_SSL_CERT: { type: "string", default: "" },
CWMP_SSL_KEY: { type: "string", default: "" }, CWMP_SSL_KEY: { type: "string", default: "" },
CWMP_LOG_FILE: { type: "path", default: "" }, CWMP_LOG_FILE: { type: "path", default: "" },
@@ -51,7 +51,7 @@ const options = {
NBI_WORKER_PROCESSES: { type: "int", default: 0 }, NBI_WORKER_PROCESSES: { type: "int", default: 0 },
NBI_PORT: { type: "int", default: 7557 }, NBI_PORT: { type: "int", default: 7557 },
NBI_INTERFACE: { type: "string", default: "::" }, NBI_INTERFACE: { type: "string", default: "0.0.0.0" },
NBI_SSL_CERT: { type: "string", default: "" }, NBI_SSL_CERT: { type: "string", default: "" },
NBI_SSL_KEY: { type: "string", default: "" }, NBI_SSL_KEY: { type: "string", default: "" },
NBI_LOG_FILE: { type: "path", default: "" }, NBI_LOG_FILE: { type: "path", default: "" },
@@ -59,16 +59,16 @@ const options = {
FS_WORKER_PROCESSES: { type: "int", default: 0 }, FS_WORKER_PROCESSES: { type: "int", default: 0 },
FS_PORT: { type: "int", default: 7567 }, FS_PORT: { type: "int", default: 7567 },
FS_INTERFACE: { type: "string", default: "::" }, FS_INTERFACE: { type: "string", default: "0.0.0.0" },
FS_SSL_CERT: { type: "string", default: "" }, FS_SSL_CERT: { type: "string", default: "" },
FS_SSL_KEY: { type: "string", default: "" }, FS_SSL_KEY: { type: "string", default: "" },
FS_URL_PREFIX: { type: "string", default: "" }, FS_HOSTNAME: { type: "string", default: "" },
FS_LOG_FILE: { type: "path", default: "" }, FS_LOG_FILE: { type: "path", default: "" },
FS_ACCESS_LOG_FILE: { type: "path", default: "" }, FS_ACCESS_LOG_FILE: { type: "path", default: "" },
UI_WORKER_PROCESSES: { type: "int", default: 0 }, UI_WORKER_PROCESSES: { type: "int", default: 0 },
UI_PORT: { type: "int", default: 3000 }, UI_PORT: { type: "int", default: 3000 },
UI_INTERFACE: { type: "string", default: "::" }, UI_INTERFACE: { type: "string", default: "0.0.0.0" },
UI_SSL_CERT: { type: "string", default: "" }, UI_SSL_CERT: { type: "string", default: "" },
UI_SSL_KEY: { type: "string", default: "" }, UI_SSL_KEY: { type: "string", default: "" },
UI_LOG_FILE: { type: "path", default: "" }, UI_LOG_FILE: { type: "path", default: "" },
@@ -76,7 +76,6 @@ const options = {
UI_JWT_SECRET: { type: "string", default: "" }, UI_JWT_SECRET: { type: "string", default: "" },
UDP_CONNECTION_REQUEST_PORT: { type: "int", default: 0 }, UDP_CONNECTION_REQUEST_PORT: { type: "int", default: 0 },
FORWARDED_HEADER: { type: "string", default: "" },
DOWNLOAD_TIMEOUT: { type: "int", default: 3600 }, DOWNLOAD_TIMEOUT: { type: "int", default: 3600 },
EXT_TIMEOUT: { type: "int", default: 3000 }, EXT_TIMEOUT: { type: "int", default: 3000 },
@@ -99,8 +98,16 @@ const options = {
CONNECTION_REQUEST_ALLOW_BASIC_AUTH: { type: "bool", default: false }, CONNECTION_REQUEST_ALLOW_BASIC_AUTH: { type: "bool", default: false },
MAX_COMMIT_ITERATIONS: { type: "int", default: 32 }, MAX_COMMIT_ITERATIONS: { type: "int", default: 32 },
// XML configuration
XML_RECOVER: { type: "bool", default: false },
XML_IGNORE_ENC: { type: "bool", default: false },
XML_FORMAT: { type: "bool", default: false },
XML_NO_DECL: { type: "bool", default: false },
XML_NO_EMPTY: { type: "bool", default: false },
XML_LIBXMLJS: { type: "bool", default: false },
// Should probably never be changed // Should probably never be changed
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }, DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }
}; };
const allConfig: { [name: string]: string | number } = {}; const allConfig: { [name: string]: string | number } = {};
@@ -112,20 +119,37 @@ function setConfig(name, value, commandLineArgument = false): boolean {
if (name === "CONFIG_DIR" || name === "config-dir") if (name === "CONFIG_DIR" || name === "config-dir")
configDir = configDir || resolve(ROOT_DIR, value); configDir = configDir || resolve(ROOT_DIR, value);
if (name === "CWMP_SSL" || name === "cwmp-ssl") if (name === "CWMP_SSL" || name === "cwmp-ssl") {
cwmpSsl = cwmpSsl || String(value).toLowerCase().trim(); cwmpSsl =
cwmpSsl ||
String(value)
.toLowerCase()
.trim();
}
if (name === "NBI_SSL" || name === "nbi-ssl") if (name === "NBI_SSL" || name === "nbi-ssl") {
nbiSsl = nbiSsl || String(value).toLowerCase().trim(); nbiSsl =
nbiSsl ||
String(value)
.toLowerCase()
.trim();
}
if (name === "FS_SSL" || name === "fs-ssl") if (name === "FS_SSL" || name === "fs-ssl") {
fsSsl = fsSsl || String(value).toLowerCase().trim(); fsSsl =
fsSsl ||
String(value)
.toLowerCase()
.trim();
}
if (name === "UI_SSL" || name === "ui-ssl") if (name === "UI_SSL" || name === "ui-ssl") {
uiSsl = uiSsl || String(value).toLowerCase().trim(); uiSsl =
uiSsl ||
if (name === "FS_HOSTNAME" || name === "fs-hostname") String(value)
fsHostname = fsHostname || String(value).trim(); .toLowerCase()
.trim();
}
// For compatibility with v1.0 // For compatibility with v1.0
if (name === "PRESETS_CACHE_DURATION" || name === "presets-cache-duration") if (name === "PRESETS_CACHE_DURATION" || name === "presets-cache-duration")
@@ -143,6 +167,12 @@ function setConfig(name, value, commandLineArgument = false): boolean {
) )
setConfig("GPV_BATCH_SIZE", value); setConfig("GPV_BATCH_SIZE", value);
if (name === "XML_PARSE_IGNORE_ENC" || name === "xml-parse-ignore-enc")
setConfig("XML_IGNORE_ENC", value);
if (name === "XML_PARSE_RECOVER" || name === "xml-parse-recover")
setConfig("XML_RECOVER", value);
if (name === "FS_IP" || name === "fs-ip") setConfig("FS_HOSTNAME", value); if (name === "FS_IP" || name === "fs-ip") setConfig("FS_HOSTNAME", value);
function cast(val, type): string | number | boolean { function cast(val, type): string | number | boolean {
@@ -150,7 +180,11 @@ function setConfig(name, value, commandLineArgument = false): boolean {
case "int": case "int":
return Number(val); return Number(val);
case "bool": case "bool":
return ["true", "1"].includes(String(val).trim().toLowerCase()); return ["true", "1"].includes(
String(val)
.trim()
.toLowerCase()
);
case "string": case "string":
return String(val); return String(val);
case "path": case "path":
@@ -240,15 +274,6 @@ if (["true", "1"].includes(uiSsl)) {
setConfig("UI_SSL_KEY", `${d}/cwmp.key`); setConfig("UI_SSL_KEY", `${d}/cwmp.key`);
} }
if (fsHostname) {
const FS_PORT = allConfig["FS_PORT"] || 7567;
const FS_SSL = !!allConfig["FS_SSL_CERT"];
setConfig(
"FS_URL_PREFIX",
(FS_SSL ? "https" : "http") + `://${fsHostname}:${FS_PORT}/`
);
}
// Defaults // Defaults
for (const [k, v] of Object.entries(options)) for (const [k, v] of Object.entries(options))
if (v["default"] != null) setConfig(k, v["default"]); if (v["default"] != null) setConfig(k, v["default"]);
@@ -284,7 +309,7 @@ export function get(
return null; return null;
} }
export function getDefault(optionName: string): string | number | boolean { export function getDefault(optionName): string | number | boolean {
const option = options[optionName]; const option = options[optionName];
if (!option) return null; if (!option) return null;
+78 -73
View File
@@ -19,7 +19,7 @@
import * as crypto from "crypto"; import * as crypto from "crypto";
import * as dgram from "dgram"; import * as dgram from "dgram";
import { URL } from "url"; import { parse } from "url";
import * as http from "http"; import * as http from "http";
import { evaluateAsync } from "./common/expression"; import { evaluateAsync } from "./common/expression";
import { Expression } from "./types"; import { Expression } from "./types";
@@ -29,13 +29,15 @@ import * as debug from "./debug";
async function extractAuth( async function extractAuth(
exp: Expression, exp: Expression,
context: {},
now: number,
dflt: any dflt: any
): Promise<[string, string, Expression]> { ): Promise<[string, string, Expression]> {
let username, password; let username, password;
const _exp = await evaluateAsync( const _exp = await evaluateAsync(
exp, exp,
{}, context,
0, now,
async (e: Expression): Promise<Expression> => { async (e: Expression): Promise<Expression> => {
if (!username && Array.isArray(e) && e[0] === "FUNC") { if (!username && Array.isArray(e) && e[0] === "FUNC") {
if (e[1] === "EXT") { if (e[1] === "EXT") {
@@ -48,8 +50,8 @@ async function extractAuth(
return fault ? null : value; return fault ? null : value;
} else if (e[1] === "AUTH") { } else if (e[1] === "AUTH") {
if (!Array.isArray(e[2]) && !Array.isArray(e[3])) { if (!Array.isArray(e[2]) && !Array.isArray(e[3])) {
username = e[2] || ""; username = e[2];
password = e[3] || ""; password = e[3];
} }
return dflt; return dflt;
} }
@@ -61,29 +63,31 @@ async function extractAuth(
} }
function httpGet( function httpGet(
url: URL,
options: http.RequestOptions, options: http.RequestOptions,
timeout,
_debug: boolean, _debug: boolean,
deviceId: string deviceId: string
): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders }> { ): Promise<{ statusCode: number; headers: {} }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = http const req = http
.get(url, options, (res) => { .get(options, res => {
res.resume(); res.resume();
resolve({ statusCode: res.statusCode, headers: res.headers }); resolve({ statusCode: res.statusCode, headers: res.headers });
if (_debug) { if (_debug) {
debug.outgoingHttpRequest(req, deviceId, "GET", url, null); debug.outgoingHttpRequest(req, deviceId, options, null);
debug.incomingHttpResponse(res, deviceId, null); debug.incomingHttpResponse(res, deviceId, null);
} }
}) })
.on("error", (err) => { .on("error", err => {
req.destroy(); req.abort();
reject(err); reject(new Error("Device is offline"));
if (_debug) if (_debug) debug.outgoingHttpRequestError(req, deviceId, options, err);
debug.outgoingHttpRequestError(req, deviceId, "GET", url, err);
}) })
.on("timeout", () => { .on("socket", socket => {
req.destroy(); socket.setTimeout(timeout);
socket.on("timeout", () => {
req.abort();
});
}); });
}); });
} }
@@ -91,20 +95,23 @@ function httpGet(
export async function httpConnectionRequest( export async function httpConnectionRequest(
address: string, address: string,
authExp: Expression, authExp: Expression,
context: {},
allowBasicAuth: boolean, allowBasicAuth: boolean,
timeout: number, timeout: number,
_debug: boolean, _debug: boolean,
deviceId: string deviceId: string
): Promise<string> { ): Promise<void> {
const url = new URL(address); const now = Date.now();
if (url.protocol !== "http:") const options: http.RequestOptions = parse(address);
return "Invalid connection request URL or protocol"; if (options.protocol !== "http:")
throw new Error("Invalid connection request URL or protocol");
const options: http.RequestOptions = { options.agent = new http.Agent({
agent: new http.Agent({ maxSockets: 1, keepAlive: true, timeout: timeout }), maxSockets: 1,
}; keepAlive: true
});
let authHeader: Record<string, string>; let authHeader: {};
let username: string; let username: string;
let password: string; let password: string;
@@ -112,13 +119,14 @@ export async function httpConnectionRequest(
let opts = options; let opts = options;
if (authHeader) { if (authHeader) {
if (authHeader["method"] === "Basic") { if (authHeader["method"] === "Basic") {
if (!allowBasicAuth) return "Basic HTTP authentication not allowed"; if (!allowBasicAuth)
throw new Error("Basic HTTP authentication not allowed");
opts = Object.assign( opts = Object.assign(
{ {
headers: { headers: {
Authorization: auth.basic(username || "", password || ""), Authorization: auth.basic(username || "", password || "")
}, }
}, },
options options
); );
@@ -129,70 +137,57 @@ export async function httpConnectionRequest(
Authorization: auth.solveDigest( Authorization: auth.solveDigest(
username, username,
password, password,
url.pathname + url.search, options.path,
"GET", "GET",
null, null,
authHeader authHeader
), )
}, }
}, },
options options
); );
} else { } else {
return "Unrecognized auth method"; throw new Error("Unrecognized auth method");
} }
} }
let res: { statusCode: number; headers: http.IncomingHttpHeaders }; let res = await httpGet(opts, timeout, _debug, deviceId);
try {
res = await httpGet(url, opts, _debug, deviceId);
} catch (err) {
// Workaround for some devices unexpectedly closing the connection
if (authHeader) {
try {
res = await httpGet(url, opts, _debug, deviceId);
} catch (err) {
return `Connection request error: ${err.message}`;
}
}
if (err["code"] === "ECONNRESET" || err["code"] === "ECONNREFUSED") // Workaround for some devices unexpectedly closing the connection
return "Device is offline"; if (res.statusCode === 0 && authHeader)
res = await httpGet(opts, timeout, _debug, deviceId);
return `Connection request error: ${err.message}`; if (res.statusCode === 0) throw new Error("Device is offline");
} if (res.statusCode === 200 || res.statusCode === 204) return;
if (res.statusCode === 200 || res.statusCode === 204) return "";
// When a Connection Request is received for the Virtual CWMP Device and the Proxied
// Device is offline the CPE Proxier MUST respond with an HTTP 503 failure
if (res.statusCode === 503) return "Device is offline";
if (res.statusCode === 401 && res.headers["www-authenticate"]) { if (res.statusCode === 401 && res.headers["www-authenticate"]) {
try { authHeader = auth.parseWwwAuthenticateHeader(
authHeader = auth.parseWwwAuthenticateHeader( res.headers["www-authenticate"]
res.headers["www-authenticate"] );
); [username, password, authExp] = await extractAuth(
} catch (err) { authExp,
return "Connection request error: Error parsing www-authenticate header"; context,
} now,
[username, password, authExp] = await extractAuth(authExp, false); false
);
} else { } else {
return `Connection request error: Unexpected status code ${res.statusCode}`; throw new Error(
`Unexpected response code from device: ${res.statusCode}`
);
} }
} }
throw new Error("Incorrect connection request credentials");
return "Connection request error: Incorrect connection request credentials";
} }
export async function udpConnectionRequest( export async function udpConnectionRequest(
host: string, address: string,
port: number,
authExp: Expression, authExp: Expression,
sourcePort = 0, context: {},
sourcePort: number = 0,
_debug: boolean, _debug: boolean,
deviceId: string deviceId: string
): Promise<void> { ): Promise<void> {
const [host, portStr] = address.split(":", 2);
const port = portStr ? parseInt(portStr) : 80;
const now = Date.now(); const now = Date.now();
const client = dgram.createSocket({ type: "udp4", reuseAddr: true }); const client = dgram.createSocket({ type: "udp4", reuseAddr: true });
@@ -207,7 +202,12 @@ export async function udpConnectionRequest(
let username: string; let username: string;
let password: string; let password: string;
[username, password, authExp] = await extractAuth(authExp, null); [username, password, authExp] = await extractAuth(
authExp,
context,
now,
null
);
if (username == null) username = ""; if (username == null) username = "";
if (password == null) password = ""; if (password == null) password = "";
@@ -219,12 +219,12 @@ export async function udpConnectionRequest(
.createHmac("sha1", password) .createHmac("sha1", password)
.update(`${ts}${id}${username}${cn}`) .update(`${ts}${id}${username}${cn}`)
.digest("hex"); .digest("hex");
const uri = `http://${host}:${port}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`; const uri = `http://${address}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
const msg = `GET ${uri} HTTP/1.1\r\nHost: ${host}:${port}\r\n\r\n`; const msg = `GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n`;
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();
@@ -233,7 +233,12 @@ export async function udpConnectionRequest(
}); });
} }
[username, password, authExp] = await extractAuth(authExp, null); [username, password, authExp] = await extractAuth(
authExp,
context,
now,
null
);
} }
client.close(); client.close();
} }
+308 -534
View File
File diff suppressed because it is too large. Load diff
+100 -153
View File
@@ -17,9 +17,9 @@
* 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 } from "mongodb";
import { get } from "./config"; import { get } from "./config";
import { decodeTag, encodeTag, escapeRegExp } from "./common"; import { escapeRegExp } from "./common";
import { parse } from "./common/expression-parser"; import { parse } from "./common/expression-parser";
import { import {
DeviceData, DeviceData,
@@ -27,7 +27,7 @@ import {
SessionFault, SessionFault,
Task, Task,
Operation, Operation,
Expression, Expression
} from "./types"; } from "./types";
import Path from "./common/path"; import Path from "./common/path";
@@ -45,8 +45,7 @@ export let tasksCollection: Collection,
configCollection: Collection; configCollection: Collection;
let clientPromise: Promise<MongoClient>; let clientPromise: Promise<MongoClient>;
export let client: MongoClient;
const INVALID_PATH_SUFFIX = "__invalid";
function compareAccessLists(list1: string[], list2: string[]): boolean { function compareAccessLists(list1: string[], list2: string[]): boolean {
if (list1.length !== list2.length) return false; if (list1.length !== list2.length) return false;
@@ -54,21 +53,14 @@ function compareAccessLists(list1: string[], list2: string[]): boolean {
return true; return true;
} }
const onConnectCallbacks: ((db: Db) => Promise<void>)[] = [];
export function onConnect(callback: (db: Db) => Promise<void>): void {
onConnectCallbacks.push(callback);
}
export async function connect(): Promise<void> { export async function connect(): Promise<void> {
clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL")); clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), {
useNewUrlParser: true
});
const client = await clientPromise; client = await clientPromise;
const db = client.db(); const db = client.db();
await Promise.all(onConnectCallbacks.map((c) => c(db)));
}
onConnect(async (db) => {
tasksCollection = db.collection("tasks"); tasksCollection = db.collection("tasks");
await tasksCollection.createIndex({ device: 1, timestamp: 1 }); await tasksCollection.createIndex({ device: 1, timestamp: 1 });
@@ -83,7 +75,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");
}); }
export async function disconnect(): Promise<void> { export async function disconnect(): Promise<void> {
if (clientPromise) await (await clientPromise).close(); if (clientPromise) await (await clientPromise).close();
@@ -91,7 +83,7 @@ export async function disconnect(): Promise<void> {
// Optimize projection by removing overlaps // Optimize projection by removing overlaps
// This can modify the object // This can modify the object
function optimizeProjection(obj: { [path: string]: 1 }): { [path: string]: 1 } { function optimizeProjection(obj: {}): {} {
if (obj[""]) return { "": obj[""] }; if (obj[""]) return { "": obj[""] };
const keys = Object.keys(obj).sort(); const keys = Object.keys(obj).sort();
@@ -118,24 +110,19 @@ export async function fetchDevice(
[ [
Path.parse("Events"), Path.parse("Events"),
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }, { object: [timestamp, 1], writable: [timestamp, 0] }
], ],
[ [
Path.parse("DeviceID"), Path.parse("DeviceID"),
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }, { object: [timestamp, 1], writable: [timestamp, 0] }
], ]
]; ];
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"];
@@ -159,41 +146,26 @@ export async function fetchDevice(
if (obj["_notification"] != null) { if (obj["_notification"] != null) {
attrs.notification = [ attrs.notification = [
obj["_attributesTimestamp"] || 1, obj["_attributesTimestamp"] || 1,
obj["_notification"], obj["_notification"]
]; ];
} }
if (obj["_accessList"] != null) if (obj["_accessList"] != null)
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]]; attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
try { res.push([Path.parse(path.slice(0, -1)), t, attrs]);
res.push([Path.parse(path), t, attrs]);
} catch (err) {
// The path parser is now more strict so we might be in a situation where
// the database contains invalid paths from before this change So here we
// encode the invalid characters.
const splits = path.split(".");
splits[splits.length - 1] =
encodeTag(splits[splits.length - 1]) + INVALID_PATH_SUFFIX;
path = splits.join(".");
res.push([Path.parse(path), t, attrs]);
return;
}
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":
@@ -203,8 +175,8 @@ export async function fetchDevice(
{ {
object: [+v, 0], object: [+v, 0],
writable: [+v, 0], writable: [+v, 0],
value: [+v, [+v, "xsd:dateTime"]], value: [+v, [+v, "xsd:dateTime"]]
}, }
]); ]);
break; break;
case "_lastBoot": case "_lastBoot":
@@ -214,8 +186,8 @@ export async function fetchDevice(
{ {
object: [+v, 0], object: [+v, 0],
writable: [+v, 0], writable: [+v, 0],
value: [+v, [+v, "xsd:dateTime"]], value: [+v, [+v, "xsd:dateTime"]]
}, }
]); ]);
break; break;
case "_lastBootstrap": case "_lastBootstrap":
@@ -225,8 +197,8 @@ export async function fetchDevice(
{ {
object: [+v, 0], object: [+v, 0],
writable: [+v, 0], writable: [+v, 0],
value: [+v, [+v, "xsd:dateTime"]], value: [+v, [+v, "xsd:dateTime"]]
}, }
]); ]);
break; break;
case "_registered": case "_registered":
@@ -237,8 +209,8 @@ export async function fetchDevice(
{ {
object: [timestamp, 0], object: [timestamp, 0],
writable: [timestamp, 0], writable: [timestamp, 0],
value: [timestamp, [+v, "xsd:dateTime"]], value: [timestamp, [+v, "xsd:dateTime"]]
}, }
]); ]);
break; break;
case "_id": case "_id":
@@ -248,8 +220,8 @@ export async function fetchDevice(
{ {
object: [timestamp, 0], object: [timestamp, 0],
writable: [timestamp, 0], writable: [timestamp, 0],
value: [timestamp, [v as string, "xsd:string"]], value: [timestamp, [v as string, "xsd:string"]]
}, }
]); ]);
break; break;
case "_tags": case "_tags":
@@ -257,19 +229,20 @@ export async function fetchDevice(
res.push([ res.push([
Path.parse("Tags"), Path.parse("Tags"),
timestamp, timestamp,
{ object: [timestamp, 1], writable: [timestamp, 0] }, { object: [timestamp, 1], writable: [timestamp, 0] }
]); ]);
} }
for (const t of v as string[]) { for (let t of v as string[]) {
t = t.replace(/[^a-zA-Z0-9-]+/g, "_");
res.push([ res.push([
Path.parse("Tags." + encodeTag(t)), Path.parse("Tags." + t),
timestamp, timestamp,
{ {
object: [timestamp, 0], object: [timestamp, 0],
writable: [timestamp, 1], writable: [timestamp, 1],
value: [timestamp, [true, "xsd:boolean"]], value: [timestamp, [true, "xsd:boolean"]]
}, }
]); ]);
} }
break; break;
@@ -281,8 +254,8 @@ export async function fetchDevice(
{ {
object: [timestamp, 0], object: [timestamp, 0],
writable: [timestamp, 0], writable: [timestamp, 0],
value: [timestamp, [v["_Manufacturer"], "xsd:string"]], value: [timestamp, [v["_Manufacturer"], "xsd:string"]]
}, }
]); ]);
} }
@@ -293,8 +266,8 @@ export async function fetchDevice(
{ {
object: [timestamp, 0], object: [timestamp, 0],
writable: [timestamp, 0], writable: [timestamp, 0],
value: [timestamp, [v["_OUI"], "xsd:string"]], value: [timestamp, [v["_OUI"], "xsd:string"]]
}, }
]); ]);
} }
@@ -305,8 +278,8 @@ export async function fetchDevice(
{ {
object: [timestamp, 0], object: [timestamp, 0],
writable: [timestamp, 0], writable: [timestamp, 0],
value: [timestamp, [v["_ProductClass"], "xsd:string"]], value: [timestamp, [v["_ProductClass"], "xsd:string"]]
}, }
]); ]);
} }
@@ -317,15 +290,14 @@ export async function fetchDevice(
{ {
object: [timestamp, 0], object: [timestamp, 0],
writable: [timestamp, 0], writable: [timestamp, 0],
value: [timestamp, [v["_SerialNumber"], "xsd:string"]], value: [timestamp, [v["_SerialNumber"], "xsd:string"]]
}, }
]); ]);
} }
break;
default:
if (!k.startsWith("_")) storeParams(v, k, 1, ts);
} }
} }
storeParams(device, "", 0, 0);
return res; return res;
} }
@@ -450,7 +422,7 @@ export async function saveDevice(
} else { } else {
if (!update["$pull"]["_tags"]) { if (!update["$pull"]["_tags"]) {
update["$pull"]["_tags"] = { update["$pull"]["_tags"] = {
$in: [], $in: []
}; };
} }
update["$pull"]["_tags"]["$in"].push(path.segments[1]); update["$pull"]["_tags"]["$in"].push(path.segments[1]);
@@ -460,16 +432,7 @@ export async function saveDevice(
break; break;
default: default:
if (!diff[2]) { if (!diff[2]) {
let pathStr = path.toString(); update["$unset"][path.toString()] = 1;
// Paths with that suffix are encoded and need to be decoded
if (pathStr.endsWith(INVALID_PATH_SUFFIX)) {
const splits = pathStr.split(".");
splits[splits.length - 1] = decodeTag(
splits[splits.length - 1].slice(0, 0 - INVALID_PATH_SUFFIX.length)
);
pathStr = splits.join(".");
}
update["$unset"][pathStr] = 1;
continue; continue;
} }
@@ -505,7 +468,7 @@ export async function saveDevice(
break; break;
case "object": case "object":
if (!diff[1]?.object || object2 !== object1) { if (!diff[1] || !diff[1].object || object2 !== object1) {
update["$set"][ update["$set"][
path.length ? path.toString() + "._object" : "_object" path.length ? path.toString() + "._object" : "_object"
] = !!object2; ] = !!object2;
@@ -513,7 +476,7 @@ export async function saveDevice(
break; break;
case "writable": case "writable":
if (!diff[1]?.writable || writable2 !== writable1) { if (!diff[1] || !diff[1].writable || writable2 !== writable1) {
update["$set"][ update["$set"][
path.length ? path.toString() + "._writable" : "_writable" path.length ? path.toString() + "._writable" : "_writable"
] = !!writable2; ] = !!writable2;
@@ -534,8 +497,9 @@ export async function saveDevice(
} }
if (attributesTimestamp2 !== attributesTimestamp1) { if (attributesTimestamp2 !== attributesTimestamp1) {
update["$set"][path.toString() + "._attributesTimestamp"] = update["$set"][
new Date(attributesTimestamp2); path.toString() + "._attributesTimestamp"
] = new Date(attributesTimestamp2);
} }
break; break;
@@ -553,8 +517,9 @@ export async function saveDevice(
} }
if (attributesTimestamp2 !== attributesTimestamp1) { if (attributesTimestamp2 !== attributesTimestamp1) {
update["$set"][path.toString() + "._attributesTimestamp"] = update["$set"][
new Date(attributesTimestamp2); path.toString() + "._attributesTimestamp"
] = new Date(attributesTimestamp2);
} }
} }
} }
@@ -564,7 +529,7 @@ export async function saveDevice(
for (const attrName of Object.keys(diff[1])) { for (const attrName of Object.keys(diff[1])) {
if ( if (
diff[1][attrName][1] != null && diff[1][attrName][1] != null &&
diff[2]?.[attrName]?.[1] == null (!diff[2] || !diff[2][attrName] || diff[2][attrName][1] == null)
) { ) {
const p = path.length ? path.toString() + "." : ""; const p = path.length ? path.toString() + "." : "";
update["$unset"][`${p}_${attrName}`] = 1; update["$unset"][`${p}_${attrName}`] = 1;
@@ -615,10 +580,10 @@ export async function saveDevice(
} }
const result = await devicesCollection.updateOne({ _id: deviceId }, update, { const result = await devicesCollection.updateOne({ _id: deviceId }, update, {
upsert: isNew, upsert: isNew
}); });
if (!result.matchedCount && !result.upsertedCount) if (result.result.n !== 1)
throw new Error(`Device ${deviceId} not found in database`); throw new Error(`Device ${deviceId} not found in database`);
if (update2) { if (update2) {
@@ -628,7 +593,7 @@ export async function saveDevice(
} }
export async function getFaults( export async function getFaults(
deviceId: string deviceId
): Promise<{ [channel: string]: SessionFault }> { ): Promise<{ [channel: string]: SessionFault }> {
const res = await faultsCollection const res = await faultsCollection
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } }) .find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
@@ -642,39 +607,32 @@ export async function getFaults(
delete r.device; delete r.device;
r.timestamp = +r.timestamp; r.timestamp = +r.timestamp;
r.provisions = JSON.parse(r.provisions); r.provisions = JSON.parse(r.provisions);
faults[channel] = r as SessionFault; faults[channel] = r;
} }
return faults; return faults;
} }
export async function saveFault( export async function saveFault(deviceId, channel, fault): Promise<void> {
deviceId: string,
channel: string,
fault: SessionFault
): Promise<void> {
const id = `${deviceId}:${channel}`; const id = `${deviceId}:${channel}`;
const f = Object.assign({}, fault) as Record<string, any>; fault = Object.assign({}, fault);
f["_id"] = id; fault._id = id;
f["device"] = deviceId; fault.device = deviceId;
f["channel"] = channel; fault.channel = channel;
f["timestamp"] = new Date(fault.timestamp); fault.timestamp = new Date(fault.timestamp);
f["provisions"] = JSON.stringify(fault.provisions); fault.provisions = JSON.stringify(fault.provisions);
await faultsCollection.replaceOne({ _id: id }, f, { upsert: true }); await faultsCollection.replaceOne({ _id: id }, fault, { upsert: true });
} }
export async function deleteFault( export async function deleteFault(deviceId, channel): Promise<void> {
deviceId: string,
channel: string
): Promise<void> {
await faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` }); await faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` });
} }
export async function getDueTasks( export async function getDueTasks(
deviceId: string, deviceId,
timestamp: number timestamp
): Promise<[Task[], number]> { ): Promise<[Task[], number]> {
const cur = tasksCollection.find({ device: deviceId }).sort({ timestamp: 1 }); const cur = tasksCollection.find({ device: deviceId }).sort(["timestamp"]);
const tasks = [] as Task[]; const tasks = [] as Task[];
for await (const task of cur) { for await (const task of cur) {
@@ -683,13 +641,13 @@ export async function getDueTasks(
if (task.timestamp >= timestamp) return [tasks, +task.timestamp]; if (task.timestamp >= timestamp) return [tasks, +task.timestamp];
task._id = String(task._id); task._id = String(task._id);
tasks.push(task as Task); tasks.push(task);
// For API compatibility // For API compatibility
if (task.name === "download" && task.file) { if (task.name === "download" && task.file) {
let q; let q;
if (ObjectId.isValid(task.file)) if (ObjectID.isValid(task.file))
q = { _id: { $in: [task.file, new ObjectId(task.file)] } }; q = { _id: { $in: [task.file, new ObjectID(task.file)] } };
else q = { _id: task.file }; else q = { _id: task.file };
const res = await filesCollection.find(q).toArray(); const res = await filesCollection.find(q).toArray();
@@ -705,17 +663,14 @@ export async function getDueTasks(
return [tasks, null]; return [tasks, null];
} }
export async function clearTasks( export async function clearTasks(deviceId, taskIds): Promise<void> {
deviceId: string,
taskIds: string[]
): Promise<void> {
await tasksCollection.deleteMany({ await tasksCollection.deleteMany({
_id: { $in: taskIds.map((id) => new ObjectId(id)) }, _id: { $in: taskIds.map(id => new ObjectID(id)) }
}); });
} }
export async function getOperations( export async function getOperations(
deviceId: string deviceId
): Promise<{ [commandKey: string]: Operation }> { ): Promise<{ [commandKey: string]: Operation }> {
const res = await operationsCollection const res = await operationsCollection
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } }) .find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
@@ -725,61 +680,53 @@ export async function getOperations(
for (const r of res) { for (const r of res) {
const commandKey = r._id.slice(deviceId.length + 1); const commandKey = r._id.slice(deviceId.length + 1);
delete r._id; delete r._id;
// Workaround for a bug in v1.2.1 where operation object is saved without deserialization
if (typeof r.provisions !== "string") {
operations[commandKey] = r as Operation;
continue;
}
r.timestamp = +r.timestamp; r.timestamp = +r.timestamp;
if (r.args) r.args = JSON.parse(r.args); if (r.args) r.args = JSON.parse(r.args);
r.provisions = JSON.parse(r.provisions); r.provisions = JSON.parse(r.provisions);
r.retries = JSON.parse(r.retries); r.retries = JSON.parse(r.retries);
operations[commandKey] = r as Operation; operations[commandKey] = r;
} }
return operations; return operations;
} }
export async function saveOperation( export async function saveOperation(
deviceId: string, deviceId,
commandKey: string, commandKey,
operation: Operation operation
): Promise<void> { ): Promise<void> {
const id = `${deviceId}:${commandKey}`; const id = `${deviceId}:${commandKey}`;
const o = Object.assign({}, operation) as Record<string, any>; operation = Object.assign({}, operation);
o["_id"] = id; operation._id = id;
o["timestamp"] = new Date(operation.timestamp); operation.timestamp = new Date(operation.timestamp);
o["provisions"] = JSON.stringify(operation.provisions); operation.provisions = JSON.stringify(operation.provisions);
o["retries"] = JSON.stringify(operation.retries); operation.retries = JSON.stringify(operation.retries);
o["args"] = JSON.stringify(operation.args); operation.args = JSON.stringify(operation.args);
await operationsCollection.replaceOne({ _id: id }, o, { await operationsCollection.replaceOne({ _id: id }, operation, {
upsert: true, upsert: true
}); });
} }
export async function deleteOperation( export async function deleteOperation(deviceId, commandKey): Promise<void> {
deviceId: string,
commandKey: string
): Promise<void> {
await operationsCollection.deleteOne({ _id: `${deviceId}:${commandKey}` }); await operationsCollection.deleteOne({ _id: `${deviceId}:${commandKey}` });
} }
export async function getPresets(): Promise<Record<string, any>[]> { export async function getPresets(): Promise<{}[]> {
return presetsCollection.find().toArray(); return presetsCollection.find().toArray();
} }
export async function getObjects(): Promise<Record<string, any>[]> { export async function getObjects(): Promise<{}[]> {
return objectsCollection.find().toArray(); return objectsCollection.find().toArray();
} }
export async function getProvisions(): Promise<Record<string, any>[]> { export async function getProvisions(): Promise<{}[]> {
return provisionsCollection.find().toArray(); return provisionsCollection.find().toArray();
} }
export async function getVirtualParameters(): Promise<Record<string, any>[]> { export async function getVirtualParameters(): Promise<{}[]> {
return virtualParametersCollection.find().toArray(); return virtualParametersCollection.find().toArray();
} }
export function getFiles(): Promise<Record<string, any>[]> { export function getFiles(): Promise<{}[]> {
return filesCollection.find().toArray(); return filesCollection.find().toArray();
} }
@@ -787,9 +734,9 @@ export async function getConfig(): Promise<
{ id: string; value: Expression }[] { id: string; value: Expression }[]
> { > {
const res = await configCollection.find().toArray(); const res = await configCollection.find().toArray();
return res.map((c) => ({ return res.map(c => ({
id: c["_id"], id: c["_id"],
value: parse(c["value"]), value: parse(c["value"])
})); }));
} }
@@ -802,7 +749,7 @@ interface Permission {
} }
export async function getPermissions(): Promise<Permission[]> { export async function getPermissions(): Promise<Permission[]> {
return permissionsCollection.find().toArray() as unknown as Permission[]; return permissionsCollection.find().toArray();
} }
interface User { interface User {
@@ -813,5 +760,5 @@ interface User {
} }
export async function getUsers(): Promise<User[]> { export async function getUsers(): Promise<User[]> {
return usersCollection.find().toArray() as unknown as User[]; return usersCollection.find().toArray();
} }
+33 -30
View File
@@ -17,10 +17,15 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { IncomingMessage, ServerResponse, ClientRequest } from "http"; import {
IncomingMessage,
ServerResponse,
ClientRequest,
RequestOptions
} 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");
@@ -44,7 +49,7 @@ export function incomingHttpRequest(
): void { ): void {
if (!DEBUG_FILE) return; if (!DEBUG_FILE) return;
const now = new Date(); const now = new Date();
const con = httpRequest.socket; const con = httpRequest.connection;
const msg = { const msg = {
event: "incoming HTTP request", event: "incoming HTTP request",
timestamp: now, timestamp: now,
@@ -55,11 +60,11 @@ export function incomingHttpRequest(
method: httpRequest.method, method: httpRequest.method,
url: httpRequest.url, url: httpRequest.url,
headers: httpRequest.headers, headers: httpRequest.headers,
body: body, body: body
}; };
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`);
@@ -72,7 +77,7 @@ export function outgoingHttpResponse(
): void { ): void {
if (!DEBUG_FILE) return; if (!DEBUG_FILE) return;
const now = new Date(); const now = new Date();
const con = httpResponse.socket; const con = httpResponse.connection;
const msg = { const msg = {
event: "outgoing HTTP response", event: "outgoing HTTP response",
timestamp: now, timestamp: now,
@@ -81,11 +86,11 @@ export function outgoingHttpResponse(
connection: getConnectionTimestamp(con), connection: getConnectionTimestamp(con),
statusCode: httpResponse.statusCode, statusCode: httpResponse.statusCode,
headers: httpResponse.getHeaders(), headers: httpResponse.getHeaders(),
body: body, body: body
}; };
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`);
@@ -94,28 +99,27 @@ export function outgoingHttpResponse(
export function outgoingHttpRequest( export function outgoingHttpRequest(
httpRequest: ClientRequest, httpRequest: ClientRequest,
deviceId: string, deviceId: string,
method: "GET" | "PUT" | "POST" | "DELETE", options: RequestOptions,
url: URL,
body: string body: string
): void { ): void {
if (!DEBUG_FILE) return; if (!DEBUG_FILE) return;
const now = new Date(); const now = new Date();
const con = httpRequest.socket; const con = httpRequest.connection;
const msg = { const msg = {
event: "outgoing HTTP request", event: "outgoing HTTP request",
timestamp: now, timestamp: now,
remoteAddress: con.remoteAddress, remoteAddress: con.remoteAddress,
deviceId: deviceId, deviceId: deviceId,
connection: getConnectionTimestamp(con), connection: getConnectionTimestamp(con),
remotePort: url.port, remotePort: options.port,
method: method, method: options.method || "GET",
url: url.pathname + url.search, url: options.path,
headers: httpRequest.getHeaders(), headers: httpRequest.getHeaders(),
body: body, body: body
}; };
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`);
@@ -124,8 +128,7 @@ export function outgoingHttpRequest(
export function outgoingHttpRequestError( export function outgoingHttpRequestError(
httpRequest: ClientRequest, httpRequest: ClientRequest,
deviceId: string, deviceId: string,
method: "GET" | "PUT" | "POST" | "DELETE", options: RequestOptions,
url: URL,
err: Error err: Error
): void { ): void {
if (!DEBUG_FILE) return; if (!DEBUG_FILE) return;
@@ -133,18 +136,18 @@ export function outgoingHttpRequestError(
const msg = { const msg = {
event: "outgoing HTTP request", event: "outgoing HTTP request",
timestamp: now, timestamp: now,
remoteAddress: url.hostname, remoteAddress: options.hostname,
deviceId: deviceId, deviceId: deviceId,
connection: null, connection: null,
remotePort: url.port, remotePort: options.port,
method: method, method: options.method,
url: url.pathname + url.search, url: options.path,
headers: httpRequest.getHeaders(), headers: httpRequest.getHeaders(),
error: err.message, error: err.message
}; };
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`);
@@ -157,20 +160,20 @@ export function incomingHttpResponse(
): void { ): void {
if (!DEBUG_FILE) return; if (!DEBUG_FILE) return;
const now = new Date(); const now = new Date();
const con = httpResponse.socket; const con = httpResponse.connection;
const msg = { const msg = {
event: "incoming HTTP response", event: "incoming HTTP response",
timestamp: now, timestamp: now,
remoteAddress: con.remoteAddress, remoteAddress: con.remoteAddress,
deviceId: deviceId, deviceId: deviceId,
connection: getConnectionTimestamp(httpResponse.socket), connection: getConnectionTimestamp(httpResponse.connection),
statusCode: httpResponse.statusCode, statusCode: httpResponse.statusCode,
headers: httpResponse.headers, headers: httpResponse.headers,
body: body, body: body
}; };
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`);
@@ -190,11 +193,11 @@ export function outgoingUdpMessage(
remoteAddress: remoteAddress, remoteAddress: remoteAddress,
deviceId: deviceId, deviceId: deviceId,
remotePort: remotePort, remotePort: remotePort,
body: body, body: body
}; };
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`);
+26 -51
View File
@@ -21,15 +21,10 @@ import Path from "./common/path";
import * as config from "./config"; import * as config from "./config";
import * as device from "./device"; import * as device from "./device";
import * as scheduling from "./scheduling"; import * as scheduling from "./scheduling";
import { SessionContext, Declaration } from "./types";
const MAX_DEPTH = +config.get("MAX_DEPTH"); const MAX_DEPTH = +config.get("MAX_DEPTH");
export function refresh( export function refresh(sessionContext, provision, declarations): boolean {
sessionContext: SessionContext,
provision: (string | number | boolean)[],
declarations: Declaration[]
): boolean {
if ( if (
(provision.length !== 2 || typeof provision[1] !== "string") && (provision.length !== 2 || typeof provision[1] !== "string") &&
(provision.length !== 3 || (provision.length !== 3 ||
@@ -42,7 +37,7 @@ export function refresh(
) )
throw new Error("Invalid arguments"); throw new Error("Invalid arguments");
const every = 1000 * ((provision[2] as number) || 1); const every = 1000 * (provision[2] || 1);
const offset = scheduling.variance(sessionContext.deviceId, every); const offset = scheduling.variance(sessionContext.deviceId, every);
const t = scheduling.interval(sessionContext.timestamp, every, offset); const t = scheduling.interval(sessionContext.timestamp, every, offset);
@@ -54,7 +49,7 @@ export function refresh(
} else { } else {
attrGet = {}; attrGet = {};
refreshChildren = !!provision[3]; refreshChildren = !!provision[3];
for (const a of provision.slice(4)) attrGet[a as string] = t; for (const a of provision.slice(4)) attrGet[a] = t;
} }
let path = Path.parse(provision[1]); let path = Path.parse(provision[1]);
@@ -74,18 +69,14 @@ export function refresh(
pathSet: null, pathSet: null,
attrGet: attrGet, attrGet: attrGet,
attrSet: null, attrSet: null,
defer: true, defer: true
}); });
} }
return true; return true;
} }
export function value( export function value(sessionContext, provision, declarations): boolean {
sessionContext: SessionContext,
provision: (string | number | boolean)[],
declarations: Declaration[]
): boolean {
if ( if (
provision.length < 3 || provision.length < 3 ||
provision.length > 4 || provision.length > 4 ||
@@ -99,15 +90,15 @@ export function value(
attr = "value"; attr = "value";
val = provision[2]; val = provision[2];
} else { } else {
attr = (provision[2] as string) || ""; attr = provision[2] || "";
val = provision[3]; val = provision[3];
} }
if (attr === "accessList") { if (attr === "accessList") {
val = (val || "") val = (val || "")
.split(",") .split(",")
.map((s) => s.trim()) .map(s => s.trim())
.filter((s) => !!s); .filter(s => !!s);
} else if (attr === "value") { } else if (attr === "value") {
val = [val]; val = [val];
} }
@@ -118,17 +109,13 @@ export function value(
pathSet: null, pathSet: null,
attrGet: { [attr]: 1 }, attrGet: { [attr]: 1 },
attrSet: { [attr]: val }, attrSet: { [attr]: val },
defer: true, defer: true
}); });
return true; return true;
} }
export function tag( export function tag(sessionContext, provision, declarations): boolean {
sessionContext: SessionContext,
provision: (string | number | boolean)[],
declarations: Declaration[]
): boolean {
if ( if (
provision.length !== 3 || provision.length !== 3 ||
typeof provision[1] !== "string" || typeof provision[1] !== "string" ||
@@ -142,17 +129,13 @@ export function tag(
pathSet: null, pathSet: null,
attrGet: { value: 1 }, attrGet: { value: 1 },
attrSet: { value: [provision[2]] }, attrSet: { value: [provision[2]] },
defer: true, defer: true
}); });
return true; return true;
} }
export function reboot( export function reboot(sessionContext, provision, declarations): boolean {
sessionContext: SessionContext,
provision: (string | number | boolean)[],
declarations: Declaration[]
): boolean {
if (provision.length !== 1) throw new Error("Invalid arguments"); if (provision.length !== 1) throw new Error("Invalid arguments");
declarations.push({ declarations.push({
@@ -161,17 +144,13 @@ export function reboot(
pathSet: null, pathSet: null,
attrGet: { value: 1 }, attrGet: { value: 1 },
attrSet: { value: [sessionContext.timestamp] }, attrSet: { value: [sessionContext.timestamp] },
defer: true, defer: true
}); });
return true; return true;
} }
export function reset( export function reset(sessionContext, provision, declarations): boolean {
sessionContext: SessionContext,
provision: (string | number | boolean)[],
declarations: Declaration[]
): boolean {
if (provision.length !== 1) throw new Error("Invalid arguments"); if (provision.length !== 1) throw new Error("Invalid arguments");
declarations.push({ declarations.push({
@@ -180,17 +159,13 @@ export function reset(
pathSet: null, pathSet: null,
attrGet: { value: 1 }, attrGet: { value: 1 },
attrSet: { value: [sessionContext.timestamp] }, attrSet: { value: [sessionContext.timestamp] },
defer: true, defer: true
}); });
return true; return true;
} }
export function download( export function download(sessionContext, provision, declarations): boolean {
sessionContext: SessionContext,
provision: (string | number | boolean)[],
declarations: Declaration[]
): boolean {
if ( if (
(provision.length !== 3 || (provision.length !== 3 ||
typeof provision[1] !== "string" || typeof provision[1] !== "string" ||
@@ -205,7 +180,7 @@ export function download(
const alias = [ const alias = [
`FileType:${JSON.stringify(provision[1] || "")}`, `FileType:${JSON.stringify(provision[1] || "")}`,
`FileName:${JSON.stringify(provision[2] || "")}`, `FileName:${JSON.stringify(provision[2] || "")}`,
`TargetFileName:${JSON.stringify(provision[3] || "")}`, `TargetFileName:${JSON.stringify(provision[3] || "")}`
].join(","); ].join(",");
declarations.push({ declarations.push({
@@ -214,7 +189,7 @@ export function download(
pathSet: 1, pathSet: 1,
attrGet: null, attrGet: null,
attrSet: null, attrSet: null,
defer: true, defer: true
}); });
declarations.push({ declarations.push({
@@ -223,18 +198,18 @@ export function download(
pathSet: null, pathSet: null,
attrGet: { value: 1 }, attrGet: { value: 1 },
attrSet: { value: [sessionContext.timestamp] }, attrSet: { value: [sessionContext.timestamp] },
defer: true, defer: true
}); });
return true; return true;
} }
export function instances( export function instances(
sessionContext: SessionContext, sessionContext,
provision: (string | number | boolean)[], provision,
declarations: Declaration[], declarations,
startRevision: number, startRevision,
endRevision: number endRevision
): boolean { ): boolean {
if (provision.length !== 3 || typeof provision[1] !== "string") if (provision.length !== 3 || typeof provision[1] !== "string")
throw new Error("Invalid arguments"); throw new Error("Invalid arguments");
@@ -252,7 +227,7 @@ export function instances(
pathSet: null, pathSet: null,
attrGet: null, attrGet: null,
attrSet: null, attrSet: null,
defer: true, defer: true
}); });
if (endRevision === startRevision) return false; if (endRevision === startRevision) return false;
@@ -271,7 +246,7 @@ export function instances(
pathSet: count, pathSet: count,
attrGet: null, attrGet: null,
attrSet: null, attrSet: null,
defer: true, defer: true
}); });
return true; return true;
+24 -26
View File
@@ -18,20 +18,14 @@
*/ */
import Path from "./common/path"; import Path from "./common/path";
import { import { DeviceData, Declaration, Clear } from "./types";
DeviceData,
Declaration,
Clear,
AttributeTimestamps,
Attributes,
} from "./types";
const CHANGE_FLAGS = { const CHANGE_FLAGS = {
object: 2, object: 2,
writable: 4, writable: 4,
value: 8, value: 8,
notification: 16, notification: 16,
accessList: 32, accessList: 32
}; };
function parseBool(v): boolean { function parseBool(v): boolean {
@@ -44,7 +38,7 @@ function parseBool(v): boolean {
export function sanitizeParameterValue( export function sanitizeParameterValue(
parameterValue: [string | number | boolean, string] parameterValue: [string | number | boolean, string]
): [string | number | boolean, string] { ): [string | number | boolean, string?] {
if (parameterValue[0] != null) { if (parameterValue[0] != null) {
switch (parameterValue[1]) { switch (parameterValue[1]) {
case "xsd:boolean": case "xsd:boolean":
@@ -54,6 +48,13 @@ export function sanitizeParameterValue(
else parameterValue[0] = b; else parameterValue[0] = b;
} }
break; break;
case "xsd:string":
case "xsd:base64":
case "xsd:hexBinary":
if (typeof parameterValue[0] !== "string")
parameterValue[0] = "" + parameterValue[0];
break;
case "xsd:int": case "xsd:int":
case "xsd:unsignedInt": case "xsd:unsignedInt":
if (typeof parameterValue[0] !== "number") { if (typeof parameterValue[0] !== "number") {
@@ -76,7 +77,7 @@ export function sanitizeParameterValue(
} }
break; break;
default: default:
parameterValue[0] = "" + parameterValue[0]; parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0]));
break; break;
} }
} }
@@ -97,8 +98,8 @@ export function getAliasDeclarations(
pathSet: null, pathSet: null,
attrGet: attrGet, attrGet: attrGet,
attrSet: null, attrSet: null,
defer: true, defer: true
}, }
]; ];
if (path.alias) { if (path.alias) {
@@ -108,7 +109,7 @@ export function getAliasDeclarations(
for (const [p] of alias as [Path, string][]) { for (const [p] of alias as [Path, string][]) {
decs = decs.concat( decs = decs.concat(
getAliasDeclarations(parent.concat(p), timestamp, { getAliasDeclarations(parent.concat(p), timestamp, {
value: timestamp, value: timestamp
}) })
); );
} }
@@ -190,10 +191,10 @@ export function unpack(
} }
export function clear( export function clear(
deviceData: DeviceData, deviceData,
path: Path, path,
timestamp: number, timestamp,
attributes: AttributeTimestamps, attributes,
changeFlags = 0 changeFlags = 0
): void { ): void {
const changeTrackers = {}; const changeTrackers = {};
@@ -201,7 +202,7 @@ export function clear(
timestamp = timestamp || 0; timestamp = timestamp || 0;
let descendantsTimestamp = timestamp; let descendantsTimestamp = timestamp;
if (attributes?.object) { if (attributes && attributes.object) {
if (attributes.object > descendantsTimestamp) if (attributes.object > descendantsTimestamp)
descendantsTimestamp = attributes.object; descendantsTimestamp = attributes.object;
if (!(attributes.object <= attributes.value)) if (!(attributes.object <= attributes.value))
@@ -272,8 +273,8 @@ function compareEquality(a, b): boolean {
export function set( export function set(
deviceData: DeviceData, deviceData: DeviceData,
path: Path, path: Path,
timestamp: number, timestamp,
attributes: Attributes, attributes,
toClear?: Clear[] toClear?: Clear[]
): Clear[] { ): Clear[] {
path = deviceData.paths.add(path); path = deviceData.paths.add(path);
@@ -301,7 +302,7 @@ export function set(
attributes.object[1] && attributes.object[1] &&
attributes.object[0] >= (attributes.value ? attributes.value[0] : 0) attributes.object[0] >= (attributes.value ? attributes.value[0] : 0)
) )
attributes.value = [attributes.object[0], null]; attributes.value = [attributes.object[0]];
const newAttributes = Object.assign({}, currentAttributes, attributes); const newAttributes = Object.assign({}, currentAttributes, attributes);
@@ -377,7 +378,7 @@ export function track(
deviceData: DeviceData, deviceData: DeviceData,
path: Path, path: Path,
marker: string, marker: string,
attributes?: string[] attributes?
): void { ): void {
path = deviceData.paths.add(path); path = deviceData.paths.add(path);
let f = 1; let f = 1;
@@ -394,10 +395,7 @@ export function track(
cur[marker] |= f; cur[marker] |= f;
} }
export function clearTrackers( export function clearTrackers(deviceData: DeviceData, tracker): void {
deviceData: DeviceData,
tracker: string | string[]
): void {
if (Array.isArray(tracker)) { if (Array.isArray(tracker)) {
for (const v of deviceData.trackers.values()) for (const v of deviceData.trackers.values())
for (const t of tracker) delete v[t]; for (const t of tracker) delete v[t];
+16 -30
View File
@@ -22,16 +22,22 @@ 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");
const processes: { [script: string]: ChildProcess } = {}; const processes: { [script: string]: ChildProcess } = {};
const jobs = new Map(); const jobs = new Map();
export function run(args: string[]): Promise<{ fault: Fault; value: any }> { function messageHandler(message): void {
return new Promise((resolve) => { const func = jobs.get(message[0]);
if (func) {
jobs.delete(message[0]);
func({ fault: message[1], value: message[2] });
}
}
export function run(args): Promise<{ fault: Fault; value: any }> {
return new Promise(resolve => {
const scriptName = args[0]; const scriptName = args[0];
const id = crypto.randomBytes(8).toString("hex"); const id = crypto.randomBytes(8).toString("hex");
@@ -39,20 +45,19 @@ 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;
p.on("error", (err) => { p.on("error", err => {
if (processes[scriptName] === p) { if (processes[scriptName] === p) {
if (jobs.delete(id)) { if (jobs.delete(id)) {
resolve({ resolve({
fault: { code: err.name, message: err.message }, fault: { code: err.name, message: err.message },
value: null, value: null
}); });
} }
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kill(processes[scriptName]); kill(processes[scriptName]);
delete processes[scriptName]; delete processes[scriptName];
} }
@@ -62,33 +67,14 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
if (processes[scriptName] === p) delete processes[scriptName]; if (processes[scriptName] === p) delete processes[scriptName];
}); });
p.on("message", (message) => { p.on("message", messageHandler);
const func = jobs.get(message[0]);
if (func) {
jobs.delete(message[0]);
// Wait for any disconnect even to fire
setTimeout(() => {
func({ fault: message[1], value: message[2] });
});
}
});
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(() => {
if (jobs.delete(id)) { if (jobs.delete(id)) {
resolve({ resolve({
fault: { code: "timeout", message: "Extension timed out" }, fault: { code: "timeout", message: "Extension timed out" },
value: null, value: null
}); });
} }
}, TIMEOUT); }, TIMEOUT);
@@ -100,7 +86,7 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
} }
function kill(process: ChildProcess): Promise<void> { function kill(process: ChildProcess): Promise<void> {
return new Promise((resolve) => { return new Promise(resolve => {
const timeToKill = Date.now() + 5000; const timeToKill = Date.now() + 5000;
process.kill(); process.kill();
-175
View File
@@ -1,175 +0,0 @@
/**
* 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 { IncomingMessage } from "http";
import { TLSSocket } from "tls";
import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js";
import * as config from "./config";
interface RequestOrigin {
localAddress: string;
localPort: number;
remoteAddress: string;
remotePort: number;
host: string;
encrypted: boolean;
}
const FORWARDED_HEADER = "" + config.get("FORWARDED_HEADER");
const cache = new WeakMap<IncomingMessage, RequestOrigin>();
const cidrs: [IPv4 | IPv6, number][] = [];
for (const str of FORWARDED_HEADER.split(",").map((s) => s.trim())) {
try {
cidrs.push(parseCIDR(str));
} catch (err) {
// Not a valid CIDR format, try parsing as IP
try {
const ip = parse(str);
cidrs.push([ip, ip.toByteArray().length * 8]);
} catch (err) {
// Not a valid IP either, ignore
}
}
}
function parseForwardedHeader(str: string): { [name: string]: string } {
str = str.toLowerCase();
const res: { [name: string]: string } = {};
let keyIdx = 0;
let valueIdx = -1;
let key: string;
for (let i = 0; i < str.length; ++i) {
const char = str.charCodeAt(i);
if (char === 61 /* = */) {
if (keyIdx >= 0) {
key = str.slice(keyIdx, i).trim();
keyIdx = -1;
valueIdx = i + 1;
}
} else if (char === 59 /* ; */) {
if (valueIdx >= 0) res[key] = str.slice(valueIdx, i).trim();
valueIdx = -1;
keyIdx = i + 1;
} else if (char === 44 /* , */) {
if (valueIdx >= 0) res[key] = str.slice(valueIdx, i).trim();
return res;
} else if (char === 34 /* " */) {
if (valueIdx >= 0) {
const quoteIdx = i;
if (!str.slice(valueIdx, quoteIdx).trim()) {
for (i = i + 1; i < str.length; ++i) {
const c = str.charCodeAt(i);
if (c === 92 /* \ */) ++i;
if (c === 34 /* " */) {
res[key] = JSON.parse(str.slice(quoteIdx, i + 1).trim());
valueIdx = -1;
keyIdx = i + 1;
break;
}
}
}
}
}
}
if (valueIdx >= 0) res[key] = str.slice(valueIdx).trim();
return res;
}
export function getRequestOrigin(request: IncomingMessage): RequestOrigin {
let origin = cache.get(request);
if (!origin) {
const soc = request.socket;
origin = {
localAddress: soc.localAddress,
localPort: soc.localPort,
remoteAddress: soc.remoteAddress,
remotePort: soc.remotePort,
host: request.headers["host"],
encrypted: !!(request.socket as TLSSocket).encrypted,
};
const header = request.headers["forwarded"];
if (header) {
const ip = parse(soc.remoteAddress) as IPv4;
if (cidrs.some((cidr) => ip.match(cidr as [IPv4, number]))) {
const parsed = parseForwardedHeader(header);
if (parsed["proto"] === "https") {
origin.encrypted = true;
origin.localPort = 443;
} else if (parsed["proto"] === "http") {
origin.encrypted = false;
origin.localPort = 80;
}
if (parsed["host"]) {
origin.host = parsed["host"];
const [, port] = parsed["host"].split(":", 2);
origin.localPort = +port || origin.localPort;
}
if (parsed["for"]) {
if (parsed["for"].startsWith("[")) {
const i = parsed["for"].lastIndexOf("]");
if (i >= 0) {
origin.remoteAddress = parsed["for"].slice(1, i);
origin.remotePort =
parseInt(parsed["for"].slice(i + 2)) || origin.remotePort;
}
} else {
const i = parsed["for"].lastIndexOf(":");
if (i >= 0) {
origin.remoteAddress = parsed["for"].slice(0, i);
origin.remotePort =
parseInt(parsed["for"].slice(i + 1)) || origin.remotePort;
} else {
origin.remoteAddress = parsed["for"];
}
}
}
if (parsed["by"]) {
if (parsed["by"].startsWith("[")) {
const i = parsed["by"].lastIndexOf("]");
if (i >= 0) {
origin.localAddress = parsed["by"].slice(1, i);
origin.localPort =
parseInt(parsed["by"].slice(i + 2)) || origin.localPort;
}
} else {
const i = parsed["by"].lastIndexOf(":");
if (i >= 0) {
origin.localAddress = parsed["by"].slice(0, i);
origin.localPort =
parseInt(parsed["by"].slice(i + 1)) || origin.localPort;
} else {
origin.localAddress = parsed["by"];
}
}
}
}
}
cache.set(request, origin);
}
return origin;
}
+33 -67
View File
@@ -18,80 +18,46 @@
*/ */
import * as url from "url"; import * as url from "url";
import { IncomingMessage, ServerResponse } from "http"; import * as querystring from "querystring";
import { Collection, GridFSBucket } from "mongodb"; import { GridFSBucket } from "mongodb";
import { onConnect } from "./db"; import * as db from "./db";
import * as logger from "./logger"; import * as logger from "./logger";
import { getRequestOrigin } from "./forwarded";
import memoize from "./common/memoize";
let filesCollection: Collection; export function listener(request, response): void {
let filesBucket: GridFSBucket; const urlParts = url.parse(request.url, true);
if (request.method === "GET") {
const filename = querystring.unescape(urlParts.pathname.substring(1));
onConnect(async (db) => { const log = {
filesCollection = db.collection("fs.files"); message: "Fetch file",
filesBucket = new GridFSBucket(db); filename: filename,
}); remoteAddress: request.connection.remoteAddress
};
const getFile = memoize( db.filesCollection.findOne({ _id: filename }, (err, file) => {
(uploadDate: number, size: number, filename: string): Promise<Buffer> => { if (err) throw err;
return new Promise((resolve, reject) => {
const buffer = Buffer.allocUnsafe(size); if (!file) {
let i = 0; response.writeHead(404);
const downloadStream = filesBucket.openDownloadStreamByName(filename); response.end();
downloadStream.on("error", reject); log.message += " not found";
downloadStream.on("data", (data: Buffer) => { logger.accessError(log);
data.copy(buffer, i); return;
i += data.length; }
});
downloadStream.on("end", () => { response.writeHead(200, {
if (i !== size) reject(new Error("File size mismatch")); "Content-Type": file.contentType || "application/octet-stream",
else resolve(buffer); "Content-Length": file.length
}); });
const bucket = new GridFSBucket(db.client.db());
const downloadStream = bucket.openDownloadStreamByName(filename);
downloadStream.pipe(response);
logger.accessInfo(log);
}); });
} } else {
);
export async function listener(
request: IncomingMessage,
response: ServerResponse
): Promise<void> {
if (request.method !== "GET") {
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 = decodeURIComponent(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["uploadDate"].getTime(),
file.length,
filename
);
response.writeHead(200, {
"Content-Type": "application/octet-stream",
"Content-Length": file.length,
});
response.end(buffer);
logger.accessInfo(log);
} }
+1 -1
View File
@@ -31,7 +31,7 @@ const UNDISCOVERED_DEPTH = 7;
// point has been discovered. // point has been discovered.
export function estimateGpnCount( export function estimateGpnCount(
gpnPatterns: [Path, number][], gpnPatterns: [Path, number][],
depth = 0 depth: number = 0
): number { ): number {
const children: { [segment: string]: [Path, number][] } = {}; const children: { [segment: string]: [Path, number][] } = {};
const wildcardChildren: [Path, number][] = []; const wildcardChildren: [Path, number][] = [];
+51 -52
View File
@@ -59,7 +59,7 @@ const INFORM_SCRIPT = `
// Device ID as user name // Device ID as user name
const username = declare("DeviceID.ID", {value: 1}).value[0] const username = declare("DeviceID.ID", {value: 1}).value[0]
// Password will be fixed for a given device because Math.random() is seeded with device ID by default. // Password will be fixed a given device because Math.random() is seeded with devcie ID by default.
const password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString(36); const password = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
const informInterval = 300; const informInterval = 300;
@@ -95,11 +95,11 @@ export async function getStatus(): Promise<Status> {
filters: !Object.keys(ui["filters"]).length, filters: !Object.keys(ui["filters"]).length,
device: !Object.keys(ui["device"]).length, device: !Object.keys(ui["device"]).length,
index: !Object.keys(ui["index"]).length, index: !Object.keys(ui["index"]).length,
overview: !Object.keys(ui["overview"]).length, overview: !Object.keys(ui["overview"]).length
}; };
} }
export async function seed(options: Record<string, boolean>): Promise<void> { export async function seed(options): Promise<void> {
const resources = {}; const resources = {};
const proms = []; const proms = [];
@@ -117,12 +117,12 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
role: "admin", role: "admin",
resource: "virtualParameters", resource: "virtualParameters",
access: 3, access: 3,
validate: "true", validate: "true"
}, }
]; ];
resources["users"] = [ resources["users"] = [
{ username: "admin", password: "admin", roles: ["admin"] }, { username: "admin", password: "admin", roles: ["admin"] }
]; ];
} }
@@ -135,7 +135,7 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
{ _id: "ui.filters.1.parameter", value: "DeviceID.ProductClass" }, { _id: "ui.filters.1.parameter", value: "DeviceID.ProductClass" },
{ _id: "ui.filters.1.type", value: "'string'" }, { _id: "ui.filters.1.type", value: "'string'" },
{ _id: "ui.filters.2.label", value: "'Tag'" }, { _id: "ui.filters.2.label", value: "'Tag'" },
{ _id: "ui.filters.2.type", value: "'tag'" }, { _id: "ui.filters.2.type", value: "'tag'" }
]); ]);
} }
@@ -149,118 +149,118 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
{ _id: "ui.device.2.parameters.0.label", value: "'Last inform'" }, { _id: "ui.device.2.parameters.0.label", value: "'Last inform'" },
{ {
_id: "ui.device.2.parameters.0.components.0.type", _id: "ui.device.2.parameters.0.components.0.type",
value: "'parameter'", value: "'parameter'"
}, },
{ _id: "ui.device.2.parameters.0.components.1.chart", value: "'online'" }, { _id: "ui.device.2.parameters.0.components.1.chart", value: "'online'" },
{ {
_id: "ui.device.2.parameters.0.components.1.type", _id: "ui.device.2.parameters.0.components.1.type",
value: "'overview-dot'", value: "'overview-dot'"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.type", _id: "ui.device.2.parameters.0.components.2.type",
value: "'summon-button'", value: "'summon-button'"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.parameters.0", _id: "ui.device.2.parameters.0.components.2.parameters.0",
value: "InternetGatewayDevice.DeviceInfo.HardwareVersion", value: "InternetGatewayDevice.DeviceInfo.HardwareVersion"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.parameters.1", _id: "ui.device.2.parameters.0.components.2.parameters.1",
value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion", value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.parameters.2", _id: "ui.device.2.parameters.0.components.2.parameters.2",
value: value:
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress", "InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.parameters.3", _id: "ui.device.2.parameters.0.components.2.parameters.3",
value: value:
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress", "InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.parameters.4", _id: "ui.device.2.parameters.0.components.2.parameters.4",
value: "InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID", value: "InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.parameters.5", _id: "ui.device.2.parameters.0.components.2.parameters.5",
value: value:
"InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase", "InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.parameters.6", _id: "ui.device.2.parameters.0.components.2.parameters.6",
value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName", value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.parameters.7", _id: "ui.device.2.parameters.0.components.2.parameters.7",
value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress", value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress"
}, },
{ {
_id: "ui.device.2.parameters.0.components.2.parameters.8", _id: "ui.device.2.parameters.0.components.2.parameters.8",
value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress", value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress"
}, },
{ {
_id: "ui.device.2.parameters.0.parameter", _id: "ui.device.2.parameters.0.parameter",
value: "DATE_STRING(Events.Inform)", value: "DATE_STRING(Events.Inform)"
}, },
{ _id: "ui.device.2.parameters.1.label", value: "'Serial number'" }, { _id: "ui.device.2.parameters.1.label", value: "'Serial number'" },
{ {
_id: "ui.device.2.parameters.1.parameter", _id: "ui.device.2.parameters.1.parameter",
value: "DeviceID.SerialNumber", value: "DeviceID.SerialNumber"
}, },
{ _id: "ui.device.2.parameters.2.label", value: "'Product class'" }, { _id: "ui.device.2.parameters.2.label", value: "'Product class'" },
{ {
_id: "ui.device.2.parameters.2.parameter", _id: "ui.device.2.parameters.2.parameter",
value: "DeviceID.ProductClass", value: "DeviceID.ProductClass"
}, },
{ _id: "ui.device.2.parameters.3.label", value: "'OUI'" }, { _id: "ui.device.2.parameters.3.label", value: "'OUI'" },
{ {
_id: "ui.device.2.parameters.3.parameter", _id: "ui.device.2.parameters.3.parameter",
value: "DeviceID.OUI", value: "DeviceID.OUI"
}, },
{ _id: "ui.device.2.parameters.4.label", value: "'Manufacturer'" }, { _id: "ui.device.2.parameters.4.label", value: "'Manufacturer'" },
{ {
_id: "ui.device.2.parameters.4.parameter", _id: "ui.device.2.parameters.4.parameter",
value: "DeviceID.Manufacturer", value: "DeviceID.Manufacturer"
}, },
{ _id: "ui.device.2.parameters.5.label", value: "'Hardware version'" }, { _id: "ui.device.2.parameters.5.label", value: "'Hardware version'" },
{ {
_id: "ui.device.2.parameters.5.parameter", _id: "ui.device.2.parameters.5.parameter",
value: "InternetGatewayDevice.DeviceInfo.HardwareVersion", value: "InternetGatewayDevice.DeviceInfo.HardwareVersion"
}, },
{ _id: "ui.device.2.parameters.6.label", value: "'Software version'" }, { _id: "ui.device.2.parameters.6.label", value: "'Software version'" },
{ {
_id: "ui.device.2.parameters.6.parameter", _id: "ui.device.2.parameters.6.parameter",
value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion", value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion"
}, },
{ _id: "ui.device.2.parameters.7.label", value: "'MAC'" }, { _id: "ui.device.2.parameters.7.label", value: "'MAC'" },
{ {
_id: "ui.device.2.parameters.7.parameter", _id: "ui.device.2.parameters.7.parameter",
value: value:
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress", "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress"
}, },
{ _id: "ui.device.2.parameters.8.label", value: "'IP'" }, { _id: "ui.device.2.parameters.8.label", value: "'IP'" },
{ {
_id: "ui.device.2.parameters.8.parameter", _id: "ui.device.2.parameters.8.parameter",
value: value:
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress", "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress"
}, },
{ _id: "ui.device.2.parameters.9.label", value: "'WLAN SSID'" }, { _id: "ui.device.2.parameters.9.label", value: "'WLAN SSID'" },
{ {
_id: "ui.device.2.parameters.9.parameter", _id: "ui.device.2.parameters.9.parameter",
value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID"
}, },
{ _id: "ui.device.2.parameters.10.label", value: "'WLAN passphrase'" }, { _id: "ui.device.2.parameters.10.label", value: "'WLAN passphrase'" },
{ {
_id: "ui.device.2.parameters.10.parameter", _id: "ui.device.2.parameters.10.parameter",
value: value:
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase", "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase"
}, },
{ _id: "ui.device.3.type", value: "'parameter-table'" }, { _id: "ui.device.3.type", value: "'parameter-table'" },
{ _id: "ui.device.3.label", value: "'LAN hosts'" }, { _id: "ui.device.3.label", value: "'WLAN hosts'" },
{ {
_id: "ui.device.3.parameter", _id: "ui.device.3.parameter",
value: "InternetGatewayDevice.LANDevice.1.Hosts.Host", value: "InternetGatewayDevice.LANDevice.1.Hosts.Host"
}, },
{ _id: "ui.device.3.childParameters.0.label", value: "'Host name'" }, { _id: "ui.device.3.childParameters.0.label", value: "'Host name'" },
{ _id: "ui.device.3.childParameters.0.parameter", value: "HostName" }, { _id: "ui.device.3.childParameters.0.parameter", value: "HostName" },
@@ -278,12 +278,12 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
{ _id: "ui.device.5.element", value: "'div.container-full-width'" }, { _id: "ui.device.5.element", value: "'div.container-full-width'" },
{ {
_id: "ui.device.5.components.0.components.0", _id: "ui.device.5.components.0.components.0",
value: "'All parameters'", value: "'All parameters'"
}, },
{ _id: "ui.device.5.components.0.element", value: "'h3'" }, { _id: "ui.device.5.components.0.element", value: "'h3'" },
{ _id: "ui.device.5.components.0.type", value: "'container'" }, { _id: "ui.device.5.components.0.type", value: "'container'" },
{ _id: "ui.device.5.components.1.type", value: "'all-parameters'" }, { _id: "ui.device.5.components.1.type", value: "'all-parameters'" },
{ _id: "ui.device.6.type", value: "'device-actions'" }, { _id: "ui.device.6.type", value: "'device-actions'" }
]); ]);
} }
@@ -298,18 +298,18 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
{ _id: "ui.index.2.label", value: "'Software version'" }, { _id: "ui.index.2.label", value: "'Software version'" },
{ {
_id: "ui.index.2.parameter", _id: "ui.index.2.parameter",
value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion", value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion"
}, },
{ _id: "ui.index.3.label", value: "'IP'" }, { _id: "ui.index.3.label", value: "'IP'" },
{ {
_id: "ui.index.3.parameter", _id: "ui.index.3.parameter",
value: value:
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress", "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress"
}, },
{ _id: "ui.index.4.label", value: "'SSID'" }, { _id: "ui.index.4.label", value: "'SSID'" },
{ {
_id: "ui.index.4.parameter", _id: "ui.index.4.parameter",
value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID", value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID"
}, },
{ _id: "ui.index.5.type", value: "'container'" }, { _id: "ui.index.5.type", value: "'container'" },
{ _id: "ui.index.5.label", value: "'Last inform'" }, { _id: "ui.index.5.label", value: "'Last inform'" },
@@ -322,7 +322,7 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
{ _id: "ui.index.6.label", value: "'Tags'" }, { _id: "ui.index.6.label", value: "'Tags'" },
{ _id: "ui.index.6.parameter", value: "Tags" }, { _id: "ui.index.6.parameter", value: "Tags" },
{ _id: "ui.index.6.unsortable", value: "true" }, { _id: "ui.index.6.unsortable", value: "true" },
{ _id: "ui.index.6.writable", value: "false" }, { _id: "ui.index.6.writable", value: "false" }
]); ]);
} }
@@ -331,44 +331,43 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
{ _id: "ui.overview.charts.online.label", value: "'Online status'" }, { _id: "ui.overview.charts.online.label", value: "'Online status'" },
{ {
_id: "ui.overview.charts.online.slices.1_onlineNow.color", _id: "ui.overview.charts.online.slices.1_onlineNow.color",
value: "'#31a354'", value: "'#31a354'"
}, },
{ {
_id: "ui.overview.charts.online.slices.1_onlineNow.filter", _id: "ui.overview.charts.online.slices.1_onlineNow.filter",
value: "Events.Inform > NOW() - 5 * 60 * 1000", value: "Events.Inform > NOW() - 5 * 60 * 1000"
}, },
{ {
_id: "ui.overview.charts.online.slices.1_onlineNow.label", _id: "ui.overview.charts.online.slices.1_onlineNow.label",
value: "'Online now'", value: "'Online now'"
}, },
{ {
_id: "ui.overview.charts.online.slices.2_past24.color", _id: "ui.overview.charts.online.slices.2_past24.color",
value: "'#a1d99b'", value: "'#a1d99b'"
}, },
{ {
_id: "ui.overview.charts.online.slices.2_past24.filter", _id: "ui.overview.charts.online.slices.2_past24.filter",
value: value:
"Events.Inform > (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000) AND Events.Inform < (NOW() - 5 * 60 * 1000)", "Events.Inform > (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000) AND Events.Inform < (NOW() - 5 * 60 * 1000)"
}, },
{ {
_id: "ui.overview.charts.online.slices.2_past24.label", _id: "ui.overview.charts.online.slices.2_past24.label",
value: "'Past 24 hours'", value: "'Past 24 hours'"
}, },
{ {
_id: "ui.overview.charts.online.slices.3_others.color", _id: "ui.overview.charts.online.slices.3_others.color",
value: "'#e5f5e0'", value: "'#e5f5e0'"
}, },
{ {
_id: "ui.overview.charts.online.slices.3_others.filter", _id: "ui.overview.charts.online.slices.3_others.filter",
value: value: "Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)"
"Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)",
}, },
{ {
_id: "ui.overview.charts.online.slices.3_others.label", _id: "ui.overview.charts.online.slices.3_others.label",
value: "'Others'", value: "'Others'"
}, },
{ _id: "ui.overview.groups.online.label", value: "''" }, { _id: "ui.overview.groups.online.label", value: "''" },
{ _id: "ui.overview.groups.online.charts.0", value: "'online'" }, { _id: "ui.overview.groups.online.charts.0", value: "'online'" }
]); ]);
} }
@@ -379,16 +378,16 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
weight: 0, weight: 0,
channel: "bootstrap", channel: "bootstrap",
events: "0 BOOTSTRAP", events: "0 BOOTSTRAP",
provision: "bootstrap", provision: "bootstrap"
}, },
{ _id: "default", weight: 0, channel: "default", provision: "default" }, { _id: "default", weight: 0, channel: "default", provision: "default" },
{ _id: "inform", weight: 0, channel: "inform", provision: "inform" }, { _id: "inform", weight: 0, channel: "inform", provision: "inform" }
]; ];
resources["provisions"] = [ resources["provisions"] = [
{ _id: "bootstrap", script: BOOTSTRAP_SCRIPT }, { _id: "bootstrap", script: BOOTSTRAP_SCRIPT },
{ _id: "default", script: DEFAULT_SCRIPT }, { _id: "default", script: DEFAULT_SCRIPT },
{ _id: "inform", script: INFORM_SCRIPT }, { _id: "inform", script: INFORM_SCRIPT }
]; ];
} }
+1 -1
View File
@@ -22,7 +22,7 @@ interface Instance {
} }
export default class InstanceSet { export default class InstanceSet {
private declare set: Set<Instance>; private set: Set<Instance>;
public constructor() { public constructor() {
this.set = new Set(); this.set = new Set();
+72 -92
View File
@@ -22,39 +22,30 @@ import * as crypto from "crypto";
import * as config from "./config"; import * as config from "./config";
import * as db from "./db"; import * as db from "./db";
import * as cache from "./cache"; import * as cache from "./cache";
import { convertOldPrecondition } from "./mongodb-functions"; import { mongoQueryToFilter } from "./mongodb-functions";
import * as logger from "./logger"; import * as logger from "./logger";
import * as scheduling from "./scheduling"; import * as scheduling from "./scheduling";
import * as expression from "./common/expression"; import * as expression from "./common/expression";
import { parse } from "./common/expression-parser"; import { parse } from "./common/expression-parser";
import { import { Preset, Expression } from "./types";
Preset,
Expression,
Provisions,
VirtualParameters,
Files,
Users,
Permissions,
Config,
UiConfig,
} from "./types";
interface Snapshot { interface Permissions {
presets: Preset[]; [role: string]: {
provisions: Provisions; [access: number]: {
virtualParameters: VirtualParameters; [resource: string]: {
files: Files; access: number;
permissions: Permissions; filter: Expression;
users: Users; validate?: Expression;
config: Config; };
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();
let currentSnapshot: string = null; let currentSnapshot = null;
let nextRefresh = 1; let nextRefresh = 1;
function computeHash(snapshot): string { function computeHash(snapshot): string {
@@ -107,15 +98,11 @@ function computeHash(snapshot): string {
return h.digest("hex"); return h.digest("hex");
} }
function flattenObject( function flattenObject(src, prefix = "", dst = {}): {} {
src: Record<string, unknown>,
prefix = "",
dst = {}
): Record<string, unknown> {
for (const k of Object.keys(src)) { for (const k of Object.keys(src)) {
const v = src[k]; const v = src[k];
if (typeof v === "object" && !Array.isArray(v)) if (typeof v === "object" && !Array.isArray(v))
flattenObject(v as Record<string, unknown>, `${prefix}${k}.`, dst); flattenObject(v, `${prefix}${k}.`, dst);
else dst[`${prefix}${k}`] = v; else dst[`${prefix}${k}`] = v;
} }
return dst; return dst;
@@ -125,14 +112,14 @@ async function fetchPresets(): Promise<Preset[]> {
const res = await db.getPresets(); const res = await db.getPresets();
let objects = await db.getObjects(); let objects = await db.getObjects();
objects = objects.map((obj) => { objects = objects.map(obj => {
// Flatten object // Flatten object
obj = flattenObject(obj); obj = flattenObject(obj);
// If no keys are defined, consider all parameters as keys to keep the // If no keys are defined, consider all parameters as keys to keep the
// same behavior from v1.0 // same behavior from v1.0
if (!obj["_keys"]?.length) if (!obj["_keys"] || !obj["_keys"].length)
obj["_keys"] = Object.keys(obj).filter((k) => !k.startsWith("_")); obj["_keys"] = Object.keys(obj).filter(k => !k.startsWith("_"));
return obj; return obj;
}); });
@@ -149,9 +136,12 @@ async function fetchPresets(): Promise<Preset[]> {
if (preset["schedule"]) { if (preset["schedule"]) {
const parts = preset["schedule"].trim().split(/\s+/); const parts = preset["schedule"].trim().split(/\s+/);
schedule = { schedule = {
md5: crypto.createHash("md5").update(preset["schedule"]).digest("hex"), md5: crypto
.createHash("md5")
.update(preset["schedule"])
.digest("hex"),
duration: null, duration: null,
schedule: null, schedule: null
}; };
try { try {
@@ -161,7 +151,7 @@ async function fetchPresets(): Promise<Preset[]> {
logger.warn({ logger.warn({
message: "Invalid preset schedule", message: "Invalid preset schedule",
preset: preset["_id"], preset: preset["_id"],
schedule: preset["schedule"], schedule: preset["schedule"]
}); });
schedule.schedule = false; schedule.schedule = false;
} }
@@ -173,9 +163,7 @@ async function fetchPresets(): Promise<Preset[]> {
try { try {
precondition = parse(preset["precondition"]); precondition = parse(preset["precondition"]);
} catch (error) { } catch (error) {
precondition = convertOldPrecondition( precondition = mongoQueryToFilter(JSON.parse(preset["precondition"]));
JSON.parse(preset["precondition"])
);
} }
// Simplify expression // Simplify expression
@@ -188,7 +176,7 @@ async function fetchPresets(): Promise<Preset[]> {
for (const c of preset["configurations"]) { for (const c of preset["configurations"]) {
switch (c.type) { switch (c.type) {
case "age": case "age":
_provisions.push(["refresh", c.name, +c.age]); _provisions.push(["refresh", c.name, c.age]);
break; break;
case "value": case "value":
@@ -211,7 +199,7 @@ async function fetchPresets(): Promise<Preset[]> {
for (const obj of objects) { for (const obj of objects) {
if (obj["_id"] === c.object) { if (obj["_id"] === c.object) {
const alias = obj["_keys"] const alias = obj["_keys"]
.map((k) => `${k}:${JSON.stringify(obj[k])}`) .map(k => `${k}:${JSON.stringify(obj[k])}`)
.join(","); .join(",");
const p = `${c.name}.[${alias}]`; const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 1]); _provisions.push(["instances", p, 1]);
@@ -229,7 +217,7 @@ async function fetchPresets(): Promise<Preset[]> {
for (const obj of objects) { for (const obj of objects) {
if (obj["_id"] === c.object) { if (obj["_id"] === c.object) {
const alias = obj["_keys"] const alias = obj["_keys"]
.map((k) => `${k}:${JSON.stringify(obj[k])}`) .map(k => `${k}:${JSON.stringify(obj[k])}`)
.join(","); .join(",");
const p = `${c.name}.[${alias}]`; const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 0]); _provisions.push(["instances", p, 0]);
@@ -249,14 +237,14 @@ async function fetchPresets(): Promise<Preset[]> {
schedule: schedule, schedule: schedule,
events: events, events: events,
precondition: precondition, precondition: precondition,
provisions: _provisions, provisions: _provisions
}); });
} }
return presets; return presets;
} }
async function fetchProvisions(): Promise<Provisions> { async function fetchProvisions(): Promise<{}> {
const res = await db.getProvisions(); const res = await db.getProvisions();
const provisions = {}; const provisions = {};
@@ -275,7 +263,7 @@ async function fetchProvisions(): Promise<Provisions> {
return provisions; return provisions;
} }
async function fetchVirtualParameters(): Promise<VirtualParameters> { async function fetchVirtualParameters(): Promise<{}> {
const res = await db.getVirtualParameters(); const res = await db.getVirtualParameters();
const virtualParameters = {}; const virtualParameters = {};
@@ -304,7 +292,7 @@ async function fetchPermissions(): Promise<Permissions> {
permissions[p.role][p.access][p.resource] = { permissions[p.role][p.access][p.resource] = {
access: p.access, access: p.access,
filter: parse(p.filter || "true"), filter: parse(p.filter || "true")
}; };
if (p.validate) if (p.validate)
permissions[p.role][p.access][p.resource].validate = parse(p.validate); permissions[p.role][p.access][p.resource].validate = parse(p.validate);
@@ -313,7 +301,7 @@ async function fetchPermissions(): Promise<Permissions> {
return permissions; return permissions;
} }
async function fetchFiles(): Promise<Files> { async function fetchFiles(): Promise<{}> {
const res = await db.getFiles(); const res = await db.getFiles();
const files = {}; const files = {};
@@ -321,12 +309,14 @@ async function fetchFiles(): Promise<Files> {
const id = r["filename"] || r["_id"].toString(); const id = r["filename"] || r["_id"].toString();
files[id] = {}; files[id] = {};
files[id].length = r["length"]; files[id].length = r["length"];
files[id].md5 = r["md5"];
files[id].contentType = r["contentType"];
} }
return files; return files;
} }
async function fetchUsers(): Promise<Users> { async function fetchUsers(): Promise<{}> {
const _users = await db.getUsers(); const _users = await db.getUsers();
const users = {}; const users = {};
@@ -334,14 +324,14 @@ async function fetchUsers(): Promise<Users> {
users[user._id] = { users[user._id] = {
password: user.password, password: user.password,
salt: user.salt, salt: user.salt,
roles: user.roles.split(",").map((s) => s.trim()), roles: user.roles.split(",").map(s => s.trim())
}; };
} }
return users; return users;
} }
async function fetchConfig(): Promise<[Config, UiConfig]> { async function fetchConfig(): Promise<[{}, {}]> {
const conf = await db.getConfig(); const conf = await db.getConfig();
conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)); conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0));
@@ -350,8 +340,7 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
filters: {}, filters: {},
device: {}, device: {},
index: {}, index: {},
overview: {}, overview: {}
pageSize: null,
}; };
const _config = {}; const _config = {};
@@ -361,13 +350,12 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
_config[c.id] = val; _config[c.id] = val;
if (c.id.startsWith("ui.")) { if (c.id.startsWith("ui.")) {
const keys = c.id.split("."); const keys = c.id.split(".");
if (!(keys[1] in ui)) continue;
// remove the first key(ui) // remove the first key(ui)
keys.shift(); keys.shift();
let ref = ui; let ref = ui;
while (keys.length > 1) { while (keys.length > 1) {
const k = keys.shift(); const k = keys.shift();
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {}; if (typeof ref[k] !== "object") ref[k] = {};
ref = ref[k]; ref = ref[k];
} }
ref[keys[0]] = val; ref[keys[0]] = val;
@@ -379,7 +367,7 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
async function refresh(): Promise<void> { async function refresh(): Promise<void> {
if (!nextRefresh) { if (!nextRefresh) {
await new Promise((resolve) => setTimeout(resolve, 20)); await new Promise(resolve => setTimeout(resolve, 20));
await refresh(); await refresh();
return; return;
} }
@@ -394,7 +382,7 @@ async function refresh(): Promise<void> {
return; return;
} }
const lockToken = await cache.acquireLock("presets_hash_lock", 5000, 5000); const unlockOrExtend = await cache.lock("presets_hash_lock", 3);
const res = await Promise.all([ const res = await Promise.all([
fetchPresets(), fetchPresets(),
@@ -403,7 +391,7 @@ async function refresh(): Promise<void> {
fetchFiles(), fetchFiles(),
fetchPermissions(), fetchPermissions(),
fetchUsers(), fetchUsers(),
fetchConfig(), fetchConfig()
]); ]);
const snapshot = { const snapshot = {
@@ -414,7 +402,7 @@ async function refresh(): Promise<void> {
permissions: res[4], permissions: res[4],
users: res[5], users: res[5],
config: res[6][0], config: res[6][0],
ui: res[6][1], ui: res[6][1]
}; };
if (currentSnapshot) { if (currentSnapshot) {
@@ -428,7 +416,7 @@ async function refresh(): Promise<void> {
currentSnapshot = computeHash(snapshot); currentSnapshot = computeHash(snapshot);
snapshots.set(currentSnapshot, snapshot); snapshots.set(currentSnapshot, snapshot);
await cache.set("presets_hash", currentSnapshot, 300); await cache.set("presets_hash", currentSnapshot, 300);
await cache.releaseLock("presets_hash_lock", lockToken); await unlockOrExtend(0);
nextRefresh = now + (REFRESH - (now % REFRESH)); nextRefresh = now + (REFRESH - (now % REFRESH));
} }
@@ -438,40 +426,45 @@ export async function getCurrentSnapshot(): Promise<string> {
return currentSnapshot; return currentSnapshot;
} }
export function hasSnapshot(hash: string): boolean { export function hasSnapshot(hash): boolean {
return snapshots.has(hash); return snapshots.has(hash);
} }
export function getPresets(snapshotKey: string): Preset[] { export function getPresets(snapshotKey): Preset[] {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.presets; return snapshot.presets;
} }
export function getProvisions(snapshotKey: string): Provisions { export function getProvisions(
snapshotKey
): { [name: string]: { md5: string; script: vm.Script } } {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.provisions; return snapshot.provisions;
} }
export function getVirtualParameters(snapshotKey: string): VirtualParameters { export function getVirtualParameters(
snapshotKey
): { [name: string]: { md5: string; script: vm.Script } } {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.virtualParameters; return snapshot.virtualParameters;
} }
export function getFiles(snapshotKey: string): Files { export function getFiles(
snapshotKey
): { [name: string]: { length: number; md5: string; contentType: string } } {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.files; return snapshot.files;
} }
export function getConfig( export function getConfig(
snapshotKey: string, snapshotKey,
key: string, key,
context: Record<string, unknown>, context?,
now: number, now?
cb?: (e: Expression) => Expression
): string | number | boolean | null { ): string | number | boolean | null {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
@@ -491,52 +484,39 @@ export function getConfig(
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH", "CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS", "cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD", "cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT", "cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT"
}; };
if (!(key in snapshot.config)) { if (!(key in snapshot.config)) {
if (key in oldOpts) { if (key in oldOpts)
let id; return config.get(oldOpts[key], context ? context.id : null);
if (context?.["id"]) { else return null;
id = context["id"];
} else if (cb) {
id = cb(["PARAM", "DeviceID.ID"]);
if (Array.isArray(id)) id = null;
}
return config.get(oldOpts[key], id);
}
return null;
} }
return expression.evaluate(snapshot.config[key], context, now || Date.now());
const v = expression.evaluate(snapshot.config[key], context, now, cb);
return Array.isArray(v) ? null : v;
} }
export function getConfigExpression( export function getConfigExpression(snapshotKey, key): Expression {
snapshotKey: string,
key: string
): Expression {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.config[key]; return snapshot.config[key];
} }
export function getUsers(snapshotKey: string): Users { export function getUsers(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.users; return snapshot.users;
} }
export function getPermissions(snapshotKey: string): Permissions { export function getPermissions(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.permissions; return snapshot.permissions;
} }
export function getUiConfig(snapshotKey: string): UiConfig { export function getUiConfig(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
+48 -77
View File
@@ -21,15 +21,6 @@ import * as fs from "fs";
import * as os from "os"; import * as os from "os";
import * as config from "./config"; import * as config from "./config";
import { getRequestOrigin } from "./forwarded";
import {
SessionContext,
AcsRequest,
CpeRequest,
CpeFault,
InformRequest,
Fault,
} from "./types";
const REOPEN_EVERY = 60000; const REOPEN_EVERY = 60000;
@@ -75,7 +66,7 @@ function reopen(): void {
if (!(stat && stat.dev === logStat.dev && stat.ino === logStat.ino)) { if (!(stat && stat.dev === logStat.dev && stat.ino === logStat.ino)) {
logStream.end(); logStream.end();
logStream = fs.createWriteStream(null, { logStream = fs.createWriteStream(null, {
fd: fs.openSync(LOG_FILE, "a"), fd: fs.openSync(LOG_FILE, "a")
}); });
logStat = fs.fstatSync(logStream.fd); logStat = fs.fstatSync(logStream.fd);
} }
@@ -99,7 +90,7 @@ function reopen(): void {
) { ) {
accessLogStream.end(); accessLogStream.end();
accessLogStream = fs.createWriteStream(null, { accessLogStream = fs.createWriteStream(null, {
fd: fs.openSync(ACCESS_LOG_FILE, "a"), fd: fs.openSync(ACCESS_LOG_FILE, "a")
}); });
accessLogStat = fs.fstatSync(accessLogStream.fd); accessLogStat = fs.fstatSync(accessLogStream.fd);
} }
@@ -113,7 +104,7 @@ function reopen(): void {
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref(); setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
} }
export function init(service: string, version: string): void { export function init(service, version): void {
defaultMeta.hostname = os.hostname(); defaultMeta.hostname = os.hostname();
defaultMeta.pid = process.pid; defaultMeta.pid = process.pid;
defaultMeta.name = `genieacs-${service}`; defaultMeta.name = `genieacs-${service}`;
@@ -129,7 +120,7 @@ export function init(service: string, version: string): void {
if (ACCESS_LOG_FILE) { if (ACCESS_LOG_FILE) {
accessLogStream = fs.createWriteStream(null, { accessLogStream = fs.createWriteStream(null, {
fd: fs.openSync(ACCESS_LOG_FILE, "a"), fd: fs.openSync(ACCESS_LOG_FILE, "a")
}); });
accessLogStat = fs.fstatSync(accessLogStream.fd); accessLogStat = fs.fstatSync(accessLogStream.fd);
} }
@@ -155,75 +146,61 @@ export function close(): void {
logStream.end(); logStream.end();
} }
export function flatten( export function flatten(details): {} {
details: Record<string, unknown>
): Record<string, unknown> {
if (details.sessionContext) { if (details.sessionContext) {
const sessionContext = details.sessionContext as SessionContext; details.deviceId = details.sessionContext.deviceId;
details.deviceId = sessionContext.deviceId; details.remoteAddress =
details.remoteAddress = getRequestOrigin( details.sessionContext.httpRequest.connection.remoteAddress;
sessionContext.httpRequest
).remoteAddress;
delete details.sessionContext; delete details.sessionContext;
} }
if (details.exception) { if (details.exception) {
const err = details.exception as Error; details.exceptionName = details.exception.name;
details.exceptionName = err.name; details.exceptionMessage = details.exception.message;
details.exceptionMessage = err.message; details.exceptionStack = details.exception.stack;
details.exceptionStack = err.stack;
delete details.exception; delete details.exception;
} }
if (details.task) { if (details.task) {
details.taskId = details.task["_id"]; details.taskId = details.task._id;
delete details.task; delete details.task;
} }
if (details.rpc) { if (details.rpc) {
const rpc = details.rpc as { if (details.rpc.acsRequest) {
id: string; details.acsRequestId = details.rpc.id;
acsRequest?: AcsRequest; details.acsRequestName = details.rpc.acsRequest.name;
cpeRequest?: CpeRequest; if (details.rpc.acsRequest.commandKey)
cpeFault?: CpeFault; details.acsRequestCommandKey = details.rpc.acsRequest.commandKey;
}; } else if (details.rpc.cpeRequest) {
if (rpc.acsRequest) { details.cpeRequestId = details.rpc.id;
details.acsRequestId = rpc.id; if (details.rpc.cpeRequest.name === "Inform") {
details.acsRequestName = rpc.acsRequest.name; details.informEvent = details.rpc.cpeRequest.event.join(",");
if (rpc.acsRequest["commandKey"]) details.informRetryCount = details.rpc.cpeRequest.retryCount;
details.acsRequestCommandKey = rpc.acsRequest["commandKey"];
} else if (rpc.cpeRequest) {
details.cpeRequestId = rpc.id;
if (rpc.cpeRequest.name === "Inform") {
details.informEvent = (rpc.cpeRequest as InformRequest).event.join(",");
details.informRetryCount = (rpc.cpeRequest as InformRequest).retryCount;
} else { } else {
details.cpeRequestName = rpc.cpeRequest.name; details.cpeRequestName = details.rpc.cpeRequest.name;
if (rpc.cpeRequest["commandKey"]) if (details.rpc.cpeRequest.commandKey)
details.cpeRequestCommandKey = rpc.cpeRequest["commandKey"]; details.cpeRequestCommandKey = details.rpc.cpeRequest.commandKey;
} }
} else if (rpc.cpeFault) { } else if (details.rpc.cpeFault) {
details.acsRequestId = rpc.id; details.acsRequestId = details.rpc.id;
details.cpeFaultCode = rpc.cpeFault.detail.faultCode; details.cpeFaultCode = details.rpc.cpeFault.detail.faultCode;
details.cpeFaultString = rpc.cpeFault.detail.faultString; details.cpeFaultString = details.rpc.cpeFault.detail.faultString;
} }
delete details.rpc; delete details.rpc;
} }
if (details.fault) { if (details.fault) {
const fault = details.fault as Fault; details.faultCode = details.fault.code;
details.faultCode = fault.code; details.faultMessage = details.fault.message;
details.faultMessage = fault.message;
delete details.fault; delete details.fault;
} }
// For genieacs-ui // For genieacs-ui
if (details.context) { if (details.context) {
details.remoteAddress = getRequestOrigin( details.remoteAddress = details.context.request.ip;
details.context["req"] if (details.context.state.user)
).remoteAddress; details.user = details.context.state.user.username;
if (details.context["state"].user)
details.user = details.context["state"].user.username;
delete details.context; delete details.context;
} }
@@ -233,10 +210,7 @@ export function flatten(
return details; return details;
} }
function formatJson( function formatJson(details, systemd): string {
details: Record<string, unknown>,
systemd: boolean
): string {
if (systemd) { if (systemd) {
let severity = ""; let severity = "";
if (details.severity === "info") severity = "<6>"; if (details.severity === "info") severity = "<6>";
@@ -249,17 +223,14 @@ function formatJson(
return `${JSON.stringify(flatten(details))}${os.EOL}`; return `${JSON.stringify(flatten(details))}${os.EOL}`;
} }
function formatSimple( function formatSimple(details, systemd): string {
details: Record<string, unknown>,
systemd: boolean
): string {
const skip = { const skip = {
user: true, user: true,
remoteAddress: true, remoteAddress: true,
severity: true, severity: true,
timestamp: true, timestamp: true,
message: true, message: true,
deviceId: !!details.sessionContext, deviceId: !!details.sessionContext
}; };
flatten(details); flatten(details);
@@ -292,12 +263,12 @@ function formatSimple(
return `${severity}${remote}${details.message}${meta}${os.EOL}`; return `${severity}${remote}${details.message}${meta}${os.EOL}`;
} }
return `${details.timestamp} [${( return `${details.timestamp} [${details.severity.toUpperCase()}] ${remote}${
details.severity as string details.message
).toUpperCase()}] ${remote}${details.message}${meta}${os.EOL}`; }${meta}${os.EOL}`;
} }
function log(details: Record<string, unknown>): void { function log(details): void {
details.timestamp = new Date().toISOString(); details.timestamp = new Date().toISOString();
if (LOG_FORMAT === "json") { if (LOG_FORMAT === "json") {
details = Object.assign({}, defaultMeta, details); details = Object.assign({}, defaultMeta, details);
@@ -307,22 +278,22 @@ function log(details: Record<string, unknown>): void {
} }
} }
export function info(details: Record<string, unknown>): void { export function info(details): void {
details.severity = "info"; details.severity = "info";
log(details); log(details);
} }
export function warn(details: Record<string, unknown>): void { export function warn(details): void {
details.severity = "warn"; details.severity = "warn";
log(details); log(details);
} }
export function error(details: Record<string, unknown>): void { export function error(details): void {
details.severity = "error"; details.severity = "error";
log(details); log(details);
} }
export function accessLog(details: Record<string, unknown>): void { export function accessLog(details): void {
details.timestamp = new Date().toISOString(); details.timestamp = new Date().toISOString();
if (ACCESS_LOG_FORMAT === "json") { if (ACCESS_LOG_FORMAT === "json") {
Object.assign(details, defaultMeta); Object.assign(details, defaultMeta);
@@ -332,17 +303,17 @@ export function accessLog(details: Record<string, unknown>): void {
} }
} }
export function accessInfo(details: Record<string, unknown>): void { export function accessInfo(details): void {
details.severity = "info"; details.severity = "info";
accessLog(details); accessLog(details);
} }
export function accessWarn(details: Record<string, unknown>): void { export function accessWarn(details): void {
details.severity = "warn"; details.severity = "warn";
accessLog(details); accessLog(details);
} }
export function accessError(details: Record<string, unknown>): void { export function accessError(details): void {
details.severity = "error"; details.severity = "error";
accessLog(details); accessLog(details);
} }
+220 -251
View File
@@ -17,16 +17,15 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ObjectId } from "mongodb"; import { ObjectID } from "mongodb";
import { map, parse, stringify, parseList } from "./common/expression-parser"; import { map, parse, stringify } from "./common/expression-parser";
import { likePatternToRegExp } from "./common/expression"; import { likePatternToRegExp, evaluate } from "./common/expression";
import { Expression, Fault, Task } from "./types"; import { Expression, Fault, Task } from "./types";
import { decodeTag, encodeTag } from "./common";
const isArray = Array.isArray; const isArray = Array.isArray;
export function processDeviceFilter(filter: Expression): Expression { export function processDeviceFilter(filter): Expression {
return map(filter, (exp) => { return map(filter, exp => {
if (!isArray(exp)) return exp; if (!isArray(exp)) return exp;
if (exp[0] === "PARAM") { if (exp[0] === "PARAM") {
@@ -44,26 +43,17 @@ export function processDeviceFilter(filter: Expression): Expression {
exp[1][0] === "PARAM" && exp[1][0] === "PARAM" &&
exp[1][1].startsWith("Tags.") exp[1][1].startsWith("Tags.")
) { ) {
const t = decodeTag(exp[1][1].slice(5)); const t = exp[1][1].slice(5);
if (exp[0] === "IS NULL") return ["_tags", t, false]; if (exp[0] === "IS NULL") return ["<>", ["PARAM", "_tags"], t];
else if (exp[0] === "IS NOT NULL") return ["_tags", t, true]; else if (exp[0] === "IS NOT NULL") return ["=", ["PARAM", "_tags"], t];
else if (exp[0] === "=" && exp[2] === true) return ["_tags", t, true]; else if (exp[0] === "=" && exp[2] === true)
else if (exp[0] === "<>" && exp[2] !== true) return ["_tags", t, true]; return ["=", ["PARAM", "_tags"], t];
else if (exp[0] === "<>" && exp[2] !== true)
return ["=", ["PARAM", "_tags"], t];
} else if ( } else if (
[ ["=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"].includes(exp[0])
"=",
"<>",
">",
">=",
"<",
"<=",
"LIKE",
"NOT LIKE",
"IS NULL",
"IS NOT NULL",
].includes(exp[0])
) { ) {
let e = map(exp, (ee) => { let e = map(exp, ee => {
if ( if (
isArray(ee) && isArray(ee) &&
ee[0] === "PARAM" && ee[0] === "PARAM" &&
@@ -85,12 +75,12 @@ export function processDeviceFilter(filter: Expression): Expression {
}); });
} }
export function processTasksFilter(filter: Expression): Expression { export function processTasksFilter(filter): Expression {
return map(filter, (exp) => { return map(filter, exp => {
if (!isArray(exp)) return exp; if (!isArray(exp)) return exp;
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
const e = exp.slice(); const e = exp.slice();
if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectId(e[2]); if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectID(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "timestamp") else if (e[1][0] === "PARAM" && e[1][1] === "timestamp")
e[2] = new Date(e[2]); e[2] = new Date(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "expiry") else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
@@ -101,8 +91,8 @@ export function processTasksFilter(filter: Expression): Expression {
}); });
} }
export function processFaultsFilter(filter: Expression): Expression { export function processFaultsFilter(filter): Expression {
return map(filter, (exp) => { return map(filter, exp => {
if (!isArray(exp)) return exp; if (!isArray(exp)) return exp;
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) { if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
const e = exp.slice(); const e = exp.slice();
@@ -115,137 +105,140 @@ export function processFaultsFilter(filter: Expression): Expression {
}); });
} }
export function filterToMongoQuery(exp: Expression): Record<string, unknown> { export function filterToMongoQuery(exp: Expression): {} {
function recursive(filter, negate): Record<string, unknown> { const ops = {
OR: 0,
AND: 0,
NOT: 0,
"=": 1,
"<>": 1,
">": 1,
">=": 1,
"<": 1,
"<=": 1,
LIKE: 1,
"NOT LIKE": 1,
"IS NULL": 1,
"IS NOT NULL": 1
};
function recursive(filter, negate, res = {}): {} {
const op = filter[0]; const op = filter[0];
if (op === "AND") { if (ops[op] === 0) {
filter = filter.filter((f) => f !== true); for (let i = 1; i < filter.length; ++i) {
if (filter.length === 2) return recursive(filter[1], negate); if (!isArray(filter[i]) || ops[filter[i][0]] == null)
} throw new Error(`Invalid expression in ${op} clause`);
if (op === "OR") {
filter = filter.filter((f) => f !== false);
if (filter.length === 2) return recursive(filter[1], negate);
}
if ((!negate && op === "AND") || (negate && op === "OR"))
return { $and: filter.slice(1).map((f) => recursive(f, negate)) };
if ((!negate && op === "OR") || (negate && op === "AND"))
return { $or: filter.slice(1).map((f) => recursive(f, negate)) };
if (op === "NOT") return recursive(filter[1], !negate);
if (isArray(filter[2]))
throw new Error(`Invalid RHS operand of ${op} clause`);
if (op === "LIKE" || op === "NOT LIKE") {
if (op === "NOT LIKE") negate = !negate;
let param;
let flags;
if (!Array.isArray(filter[1]))
throw new Error(`Invalid LHS operand of ${op} clause`);
if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") {
if (filter[2] !== filter[2].toUpperCase())
throw new Error(`Invalid RHS operand of ${op} clause`);
param = filter[1][2][1];
flags = "i";
} else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") {
if (filter[2] !== filter[2].toLowerCase())
throw new Error(`Invalid RHS operand of ${op} clause`);
param = filter[1][2][1];
flags = "i";
} else if (filter[1][0] === "PARAM") {
param = filter[1][1];
flags = "";
} else {
throw new Error(`Invalid LHS operand of ${op} clause`);
} }
const r = likePatternToRegExp(filter[2], filter[3], flags);
if (negate) return { [param]: { $nin: [r, null] } }; if ((!negate && op === "AND") || (negate && op === "OR")) {
else return { [param]: r }; res["$and"] = res["$and"] || [];
for (let i = 1; i < filter.length; ++i)
res["$and"].push(recursive(filter[i], negate));
} else if ((!negate && op === "OR") || (negate && op === "AND")) {
res["$or"] = res["$or"] || [];
for (let i = 1; i < filter.length; ++i)
res["$or"].push(recursive(filter[i], negate));
} else if (op === "NOT") {
recursive(filter[1], !negate, res);
}
} else if (ops[op] === 1) {
if (isArray(filter[2])) {
throw new Error(`Right operand of ${op} clause is not a primitive`);
} else if (
["LIKE", "NOT LIKE"].includes(op) &&
isArray(filter[1]) &&
filter[1][0] === "FUNC" &&
["UPPER", "LOWER"].includes(filter[1][1])
) {
if (
(filter[1][1] === "UPPER" && filter[2] !== filter[2].toUpperCase()) ||
(filter[1][1] === "LOWER" && filter[2] !== filter[2].toLowerCase())
) {
throw new Error(
`Cannot compare ${
filter[1][1]
}() against non-${filter[1][1].toLowerCase()}case pattern`
);
}
} else if (!isArray(filter[1]) || filter[1][0] !== "PARAM") {
throw new Error(`Left operand of ${op} clause is not a parameter`);
}
if (op === "=") {
const param = filter[1][1];
const p = (res[param] = res[param] || {});
if (negate) p["$ne"] = filter[2];
else p["$eq"] = filter[2];
} else if (op === "<>") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$ne"] = filter[2];
if (param !== "_tags") p["$exists"] = true;
} else if (op === ">") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$gt"] = filter[2];
} else if (op === ">=") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$gte"] = filter[2];
} else if (op === "<") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$lt"] = filter[2];
} else if (op === "<=") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$lte"] = filter[2];
} else if (op === "IS NULL") {
const param = filter[1][1];
res[param] = { $exists: negate };
} else if (op === "IS NOT NULL") {
const param = filter[1][1];
res[param] = { $exists: !negate };
} else {
if (op === "NOT LIKE") negate = !negate;
let param;
let flags;
if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") {
param = filter[1][2][1];
flags = "i";
} else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") {
param = filter[1][2][1];
flags = "i";
} else {
param = filter[1][1];
flags = "";
}
const r = likePatternToRegExp(filter[2], filter[3], flags);
if (negate) res[param] = { $not: r };
else res[param] = r;
}
} else {
throw new Error(`Unrecognized operator ${op}`);
} }
if (op === "_tags") { return res;
let t = filter[2];
if (negate) t = !t;
if (t) return { _tags: filter[1] };
else return { _tags: { $ne: filter[1] } };
}
if (!isArray(filter[1]) || filter[1][0] !== "PARAM")
throw new Error(`Invalid LHS operand of ${op} clause`);
if (op === "IS NULL") {
const p = filter[1][1];
return { [p]: null };
}
if (op === "IS NOT NULL") {
const p = filter[1][1];
return { [p]: { $ne: null } };
}
if (isArray(filter[2]) || filter[2] == null)
throw new Error(`Invalid RHS operand of ${op} clause`);
if (op === "=") {
const p = filter[1][1];
const v = filter[2];
if (!negate) return { [p]: v };
else return { [p]: { $nin: [v, null] } };
}
if (op === "<>") {
const p = filter[1][1];
const v = filter[2];
if (negate) return { [p]: v };
else return { [p]: { $nin: [v, null] } };
}
if (op === ">") {
const p = filter[1][1];
const v = filter[2];
if (negate) return { [p]: { $lte: v } };
else return { [p]: { $gt: v } };
}
if (op === ">=") {
const p = filter[1][1];
const v = filter[2];
if (negate) return { [p]: { $lt: v } };
else return { [p]: { $gte: v } };
}
if (op === "<") {
const p = filter[1][1];
const v = filter[2];
if (negate) return { [p]: { $gte: v } };
else return { [p]: { $lt: v } };
}
if (op === "<=") {
const p = filter[1][1];
const v = filter[2];
if (negate) return { [p]: { $gt: v } };
else return { [p]: { $lte: v } };
}
throw new Error(`Unrecognized operator ${op}`);
} }
if (!isArray(exp)) { const _exp = evaluate(exp, null, Date.now());
if (exp === true) return {};
if (!isArray(_exp)) {
if (_exp === true) return {};
throw new Error("Primitives are not valid queries"); throw new Error("Primitives are not valid queries");
} }
return recursive(exp, false); return recursive(_exp, false);
} }
export function processDeviceProjection( export function processDeviceProjection(projection: {}): {} {
projection: Record<string, 1>
): Record<string, 1> {
if (!projection) return projection; if (!projection) return projection;
const p = {}; const p = {};
for (const [k, v] of Object.entries(projection)) { for (const [k, v] of Object.entries(projection)) {
@@ -271,9 +264,7 @@ export function processDeviceProjection(
return p; return p;
} }
export function processDeviceSort( export function processDeviceSort(sort: {}): {} {
sort: Record<string, number>
): Record<string, number> {
if (!sort) return sort; if (!sort) return sort;
const s = {}; const s = {};
for (const [k, v] of Object.entries(sort)) { for (const [k, v] of Object.entries(sort)) {
@@ -289,7 +280,7 @@ export function processDeviceSort(
return s; return s;
} }
function parseDate(d: Date): number | string { function parseDate(d): number | string {
const n = +d; const n = +d;
return isNaN(n) ? "" + d : n; return isNaN(n) ? "" + d : n;
} }
@@ -311,7 +302,7 @@ interface FlatDevice {
[param: string]: FlatAttributes; [param: string]: FlatAttributes;
} }
export function flattenDevice(device: Record<string, unknown>): FlatDevice { export function flattenDevice(device): FlatDevice {
function recursive( function recursive(
input, input,
root: string, root: string,
@@ -322,39 +313,39 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
if (!root) { if (!root) {
if (name === "_lastInform") { if (name === "_lastInform") {
output["Events.Inform"] = { output["Events.Inform"] = {
value: [parseDate(tree as Date), "xsd:dateTime"], value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp, valueTimestamp: timestamp,
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
} else if (name === "_registered") { } else if (name === "_registered") {
output["Events.Registered"] = { output["Events.Registered"] = {
value: [parseDate(tree as Date), "xsd:dateTime"], value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp, valueTimestamp: timestamp,
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
} else if (name === "_lastBoot") { } else if (name === "_lastBoot") {
output["Events.1_BOOT"] = { output["Events.1_BOOT"] = {
value: [parseDate(tree as Date), "xsd:dateTime"], value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp, valueTimestamp: timestamp,
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
} else if (name === "_lastBootstrap") { } else if (name === "_lastBootstrap") {
output["Events.0_BOOTSTRAP"] = { output["Events.0_BOOTSTRAP"] = {
value: [parseDate(tree as Date), "xsd:dateTime"], value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp, valueTimestamp: timestamp,
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
} else if (name === "_id") { } else if (name === "_id") {
output["DeviceID.ID"] = { output["DeviceID.ID"] = {
@@ -363,7 +354,7 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
} else if (name === "_deviceId") { } else if (name === "_deviceId") {
output["DeviceID.Manufacturer"] = { output["DeviceID.Manufacturer"] = {
@@ -372,7 +363,7 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
output["DeviceID.OUI"] = { output["DeviceID.OUI"] = {
value: [tree["_OUI"], "xsd:string"], value: [tree["_OUI"], "xsd:string"],
@@ -380,7 +371,7 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
output["DeviceID.ProductClass"] = { output["DeviceID.ProductClass"] = {
value: [tree["_ProductClass"], "xsd:string"], value: [tree["_ProductClass"], "xsd:string"],
@@ -388,7 +379,7 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
output["DeviceID.SerialNumber"] = { output["DeviceID.SerialNumber"] = {
value: [tree["_SerialNumber"], "xsd:string"], value: [tree["_SerialNumber"], "xsd:string"],
@@ -396,24 +387,24 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
} else if (name === "_tags") { } else if (name === "_tags") {
output["Tags"] = { output["Tags"] = {
writable: false, writable: false,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: true, object: true,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
for (const t of tree as string[]) { for (const t of tree as string[]) {
output[`Tags.${encodeTag(t)}`] = { output[`Tags.${t}`] = {
value: [true, "xsd:boolean"], value: [true, "xsd:boolean"],
valueTimestamp: timestamp, valueTimestamp: timestamp,
writable: true, writable: true,
writableTimestamp: timestamp, writableTimestamp: timestamp,
object: false, object: false,
objectTimestamp: timestamp, objectTimestamp: timestamp
}; };
} }
} }
@@ -431,7 +422,7 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
if (tree["_value"] != null) { if (tree["_value"] != null) {
attrs.value = [ attrs.value = [
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"], tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
tree["_type"], tree["_type"]
]; ];
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp); attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp);
attrs.object = false; attrs.object = false;
@@ -465,29 +456,27 @@ export function flattenDevice(device: Record<string, unknown>): FlatDevice {
} }
const newDevice: FlatDevice = {}; const newDevice: FlatDevice = {};
const timestamp = new Date((device["_lastInform"] as Date) || 1).getTime(); const timestamp = new Date(device["_lastInform"] || 1).getTime();
recursive(device, "", newDevice, timestamp); recursive(device, "", newDevice, timestamp);
return newDevice; return newDevice;
} }
export function flattenFault(fault: unknown): Fault { export function flattenFault(fault): Fault {
const f = Object.assign({}, fault) as Fault; const f = Object.assign({}, fault);
if (f.timestamp) f.timestamp = +f.timestamp; if (f.timestamp) f.timestamp = +f.timestamp;
if (f["expiry"]) f["expiry"] = +f["expiry"]; if (f.expiry) f.expiry = +f.expiry;
return f as Fault; return f;
} }
export function flattenTask(task: unknown): Task { export function flattenTask(task): Task {
const t = Object.assign({}, task) as Task; const t = Object.assign({}, task);
t._id = "" + t._id; t._id = "" + t._id;
if (t["timestamp"]) t["timestamp"] = +t["timestamp"]; if (t.timestamp) t.timestamp = +t.timestamp;
if (t.expiry) t.expiry = +t.expiry; if (t.expiry) t.expiry = +t.expiry;
return t; return t;
} }
export function convertOldPrecondition( export function mongoQueryToFilter(query): Expression {
query: Record<string, unknown>
): Expression {
function recursive(_query): Expression { function recursive(_query): Expression {
const expressions: Expression[] = []; const expressions: Expression[] = [];
for (const [k, v] of Object.entries(_query)) { for (const [k, v] of Object.entries(_query)) {
@@ -506,27 +495,14 @@ export function convertOldPrecondition(
} else if (k === "_tags") { } else if (k === "_tags") {
if (typeof v === "object") { if (typeof v === "object") {
if (isArray(v)) throw new Error(`Invalid type`); if (isArray(v)) throw new Error(`Invalid type`);
const conjs: Expression[] = [];
for (const [op, val] of Object.entries(v)) { if (v.hasOwnProperty("$ne"))
if (op === "$ne") { expressions.push(["IS NULL", ["PARAM", `Tags.${v["$ne"]}`]]);
if (typeof v["$ne"] !== "string") else if (v.hasOwnProperty("$eq"))
throw new Error("Only string values are allowed for _tags"); expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v["$eq"]}`]]);
conjs.push(["IS NULL", ["PARAM", `Tags.${encodeTag(val)}`]]); else throw new Error(`Invalid tag query`);
} else if (op === "$eq") {
if (typeof v["$eq"] !== "string")
throw new Error("Only string values are allowed for _tags");
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
} else {
throw new Error(`Invalid tag query`);
}
}
if (conjs.length === 1) expressions.push(conjs[0]);
else if (conjs.length > 1) expressions.push(["AND", ...conjs]);
} else { } else {
expressions.push([ expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]);
"IS NOT NULL",
["PARAM", `Tags.${encodeTag(v as string)}`],
]);
} }
} else if (k.startsWith("Tags.")) { } else if (k.startsWith("Tags.")) {
let exists: boolean; let exists: boolean;
@@ -542,31 +518,34 @@ export function convertOldPrecondition(
const exps: Expression[] = []; const exps: Expression[] = [];
for (const [kk, vv] of Object.entries(v)) { for (const [kk, vv] of Object.entries(v)) {
if (kk === "$eq") { let op;
exps.push(["=", ["PARAM", k], vv]); switch (kk) {
} else if (kk === "$ne") { case "$eq":
exps.push([ op = "=";
"OR", break;
["<>", ["PARAM", k], vv], case "$ne":
["IS NULL", ["PARAM", k]], op = "<>";
]); break;
} else if (kk === "$lt") { case "$lt":
exps.push(["<", ["PARAM", k], vv]); op = "<";
} else if (kk === "$lte") { break;
exps.push(["<=", ["PARAM", k], vv]); case "$lte":
} else if (kk === "$gt") { op = "<=";
exps.push([">", ["PARAM", k], vv]); break;
} else if (kk === "$gte") { case "$gt":
exps.push([">=", ["PARAM", k], vv]); op = ">";
} else { break;
throw new Error(`Operator ${kk} not supported`); case "$gte":
op = ">=";
break;
default:
throw new Error(`Operator ${kk} not supported`);
} }
if (!["string", "number", "boolean"].includes(typeof vv)) exps.push([op, ["PARAM", k], vv]);
throw new Error(`Invalid value for ${kk} operator`);
} }
if (exps.length === 1) { if (exps.length === 1) {
expressions.push(exps[0]); expressions.push(exps[0]);
} else if (exps.length > 1) { } else {
const and: Expression = ["AND"]; const and: Expression = ["AND"];
expressions.push(and.concat(exps)); expressions.push(and.concat(exps));
} }
@@ -575,8 +554,8 @@ export function convertOldPrecondition(
} }
} }
if (expressions.length === 1) return expressions[0]; if (expressions.length === 1) return expressions[0];
if (expressions.length === 0) return true; const and: Expression = ["AND"];
return ["AND", ...expressions]; return and.concat(expressions);
} }
// empty filter // empty filter
@@ -585,21 +564,15 @@ export function convertOldPrecondition(
return recursive(query); return recursive(query);
} }
export function flattenPreset( export function flattenPreset(preset): {} {
preset: Record<string, unknown>
): Record<string, unknown> {
const p = Object.assign({}, preset); const p = Object.assign({}, preset);
if (p.precondition) { if (p.precondition) {
try { try {
// Try parse to check expression validity // Try parse to check expression validity
parse(p.precondition as string); parse(p.precondition);
} catch (error) { } catch (error) {
p.precondition = convertOldPrecondition( p.precondition = mongoQueryToFilter(JSON.parse(p.precondition));
JSON.parse(p.precondition as string) p.precondition = p.precondition.length ? stringify(p.precondition) : "";
);
p.precondition = (p.precondition as string).length
? stringify(p.precondition as Expression)
: "";
} }
} }
@@ -611,14 +584,14 @@ export function flattenPreset(
const provision = p.configurations[0]; const provision = p.configurations[0];
if ( if (
(p.configurations as any[]).length === 1 && p.configurations.length === 1 &&
provision.type === "provision" && provision.type === "provision" &&
provision.name && provision.name &&
provision.name.length provision.name.length
) { ) {
p.provision = provision.name; p.provision = provision.name;
p.provisionArgs = provision.args p.provisionArgs = provision.args
? provision.args.map((a) => stringify(a)).join(", ") ? JSON.stringify(provision.args).slice(1, -1)
: ""; : "";
} }
@@ -626,34 +599,30 @@ export function flattenPreset(
return p; return p;
} }
export function flattenFile( export function flattenFile(file): {} {
file: Record<string, unknown>
): Record<string, unknown> {
const f = {}; const f = {};
f["_id"] = file["_id"]; f["_id"] = file["_id"];
if (file.metadata) { if (file.metadata) {
f["metadata.fileType"] = file["metadata"]["fileType"] || ""; f["metadata.fileType"] = file.metadata.fileType || "";
f["metadata.oui"] = file["metadata"]["oui"] || ""; f["metadata.oui"] = file.metadata.oui || "";
f["metadata.productClass"] = file["metadata"]["productClass"] || ""; f["metadata.productClass"] = file.metadata.productClass || "";
f["metadata.version"] = file["metadata"]["version"] || ""; f["metadata.version"] = file.metadata.version || "";
} }
return f; return f;
} }
export function preProcessPreset( export function preProcessPreset(data): {} {
data: Record<string, unknown>
): Record<string, unknown> {
const preset = Object.assign({}, data); const preset = Object.assign({}, data);
if (!preset.precondition) preset.precondition = ""; if (!preset.precondition) preset.precondition = "";
// Try parse to check expression validity // Try parse to check expression validity
parse(preset.precondition as string); parse(preset.precondition);
preset.weight = parseInt(preset.weight as string) || 0; preset.weight = parseInt(preset.weight) || 0;
const events = {}; const events = {};
if (preset.events) { if (preset.events) {
for (let e of (preset.events as string).split(",")) { for (let e of preset.events.split(",")) {
let v = true; let v = true;
e = e.trim(); e = e.trim();
if (e.startsWith("-")) { if (e.startsWith("-")) {
@@ -671,11 +640,11 @@ export function preProcessPreset(
const configuration = { const configuration = {
type: "provision", type: "provision",
name: preset.provision, name: preset.provision,
args: null, args: null
}; };
if (preset.provisionArgs) if (preset.provisionArgs)
configuration.args = parseList(preset.provisionArgs as string); configuration.args = JSON.parse(`[${preset.provisionArgs}]`);
delete preset.provision; delete preset.provision;
delete preset.provisionArgs; delete preset.provisionArgs;
+178 -318
View File
@@ -18,23 +18,20 @@
*/ */
import * as url from "url"; import * as url from "url";
import { Collection, GridFSBucket, ObjectId } from "mongodb"; import * as mongodb from "mongodb";
import * as querystring from "querystring";
import * as vm from "vm"; import * as vm from "vm";
import * as config from "./config"; import * as config from "./config";
import { onConnect } from "./db"; import * as db from "./db";
import * as query from "./query"; import * as query from "./query";
import * as apiFunctions from "./api-functions"; import * as apiFunctions from "./api-functions";
import { 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";
import { flattenDevice } from "./mongodb-functions";
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_]*)?$/;
const TAGS_REGEX = const TAGS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tags\/([a-zA-Z0-9\-_%]+)\/?$/;
/^\/devices\/([a-zA-Z0-9\-_%]+)\/tags\/([a-zA-Z0-9\-_%]+)\/?$/;
const PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-_%]+)\/?$/; const PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-_%]+)\/?$/;
const OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-_%]+)\/?$/; const OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-_%]+)\/?$/;
const FILES_REGEX = /^\/files\/([a-zA-Z0-9%!*'();:@&=+$,?#[\]\-_.~]+)\/?$/; const FILES_REGEX = /^\/files\/([a-zA-Z0-9%!*'();:@&=+$,?#[\]\-_.~]+)\/?$/;
@@ -42,31 +39,10 @@ const PING_REGEX = /^\/ping\/([a-zA-Z0-9\-_.:]+)\/?$/;
const QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/; const QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/;
const DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/?$/; const DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/?$/;
const PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-_%]+)\/?$/; const PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-_%]+)\/?$/;
const VIRTUAL_PARAMETERS_REGEX = const VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-_%]+)\/?$/;
/^\/virtual_parameters\/([a-zA-Z0-9\-_%]+)\/?$/;
const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/; const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/;
const collections: Record<string, Collection> = { function throwError(err, httpResponse?): never {
tasks: null as Collection,
devices: null as Collection,
presets: null as Collection,
objects: null as Collection,
files: null as Collection,
provisions: null as Collection,
virtualParameters: null as Collection,
faults: null as Collection,
};
let filesBucket: GridFSBucket;
onConnect(async (db) => {
for (const k of Object.keys(collections)) {
if (k === "files") collections[k] = db.collection("fs.files");
else collections[k] = db.collection(k);
}
filesBucket = new GridFSBucket(db);
});
function throwError(err: Error, httpResponse?: ServerResponse): never {
if (httpResponse) { if (httpResponse) {
httpResponse.writeHead(500, { Connection: "close" }); httpResponse.writeHead(500, { Connection: "close" });
httpResponse.end(`${err.name}: ${err.message}`); httpResponse.end(`${err.name}: ${err.message}`);
@@ -74,10 +50,7 @@ function throwError(err: Error, httpResponse?: ServerResponse): never {
throw err; throw err;
} }
export function listener( export function listener(request, response): void {
request: IncomingMessage,
response: ServerResponse
): void {
const chunks = []; const chunks = [];
let bytes = 0; let bytes = 0;
response.setHeader("GenieACS-Version", VERSION); response.setHeader("GenieACS-Version", VERSION);
@@ -101,33 +74,18 @@ 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.socket.remoteAddress,
message: `${request.method} ${urlParts.pathname}`,
})
);
if (PRESETS_REGEX.test(urlParts.pathname)) { if (PRESETS_REGEX.test(urlParts.pathname)) {
const presetName = decodeURIComponent( const presetName = querystring.unescape(
PRESETS_REGEX.exec(urlParts.pathname)[1] PRESETS_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
let preset; const preset = JSON.parse(body.toString());
try {
preset = JSON.parse(body.toString());
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
preset._id = presetName; preset._id = presetName;
collections.presets.replaceOne( db.presetsCollection.replaceOne(
{ _id: presetName }, { _id: presetName },
preset, preset,
{ upsert: true }, { upsert: true },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -136,7 +94,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -144,7 +102,7 @@ export function listener(
} }
); );
} else if (request.method === "DELETE") { } else if (request.method === "DELETE") {
collections.presets.deleteOne({ _id: presetName }, (err) => { db.presetsCollection.deleteOne({ _id: presetName }, err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -153,7 +111,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -164,24 +122,17 @@ export function listener(
response.end("405 Method Not Allowed"); response.end("405 Method Not Allowed");
} }
} else if (OBJECTS_REGEX.test(urlParts.pathname)) { } else if (OBJECTS_REGEX.test(urlParts.pathname)) {
const objectName = decodeURIComponent( const objectName = querystring.unescape(
OBJECTS_REGEX.exec(urlParts.pathname)[1] OBJECTS_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
let object; const object = JSON.parse(body.toString());
try {
object = JSON.parse(body.toString());
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
object._id = objectName; object._id = objectName;
collections.objects.replaceOne( db.objectsCollection.replaceOne(
{ _id: objectName }, { _id: objectName },
object, object,
{ upsert: true }, { upsert: true },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -190,7 +141,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -198,7 +149,7 @@ export function listener(
} }
); );
} else if (request.method === "DELETE") { } else if (request.method === "DELETE") {
collections.objects.deleteOne({ _id: objectName }, (err) => { db.objectsCollection.deleteOne({ _id: objectName }, err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -207,7 +158,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -218,13 +169,13 @@ export function listener(
response.end("405 Method Not Allowed"); response.end("405 Method Not Allowed");
} }
} else if (PROVISIONS_REGEX.test(urlParts.pathname)) { } else if (PROVISIONS_REGEX.test(urlParts.pathname)) {
const provisionName = decodeURIComponent( const provisionName = querystring.unescape(
PROVISIONS_REGEX.exec(urlParts.pathname)[1] PROVISIONS_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
const object = { const object = {
_id: provisionName, _id: provisionName,
script: body.toString(), script: body.toString()
}; };
try { try {
@@ -235,11 +186,11 @@ export function listener(
return; return;
} }
collections.provisions.replaceOne( db.provisionsCollection.replaceOne(
{ _id: provisionName }, { _id: provisionName },
object, object,
{ upsert: true }, { upsert: true },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -248,7 +199,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -256,7 +207,7 @@ export function listener(
} }
); );
} else if (request.method === "DELETE") { } else if (request.method === "DELETE") {
collections.provisions.deleteOne({ _id: provisionName }, (err) => { db.provisionsCollection.deleteOne({ _id: provisionName }, err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -265,7 +216,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -276,13 +227,13 @@ export function listener(
response.end("405 Method Not Allowed"); response.end("405 Method Not Allowed");
} }
} else if (VIRTUAL_PARAMETERS_REGEX.test(urlParts.pathname)) { } else if (VIRTUAL_PARAMETERS_REGEX.test(urlParts.pathname)) {
const virtualParameterName = decodeURIComponent( const virtualParameterName = querystring.unescape(
VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1] VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
const object = { const object = {
_id: virtualParameterName, _id: virtualParameterName,
script: body.toString(), script: body.toString()
}; };
try { try {
@@ -293,11 +244,11 @@ export function listener(
return; return;
} }
collections.virtualParameters.replaceOne( db.virtualParametersCollection.replaceOne(
{ _id: virtualParameterName }, { _id: virtualParameterName },
object, object,
{ upsert: true }, { upsert: true },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -306,7 +257,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -314,9 +265,9 @@ export function listener(
} }
); );
} else if (request.method === "DELETE") { } else if (request.method === "DELETE") {
collections.virtualParameters.deleteOne( db.virtualParametersCollection.deleteOne(
{ _id: virtualParameterName }, { _id: virtualParameterName },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -325,7 +276,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -338,23 +289,23 @@ export function listener(
} }
} else if (TAGS_REGEX.test(urlParts.pathname)) { } else if (TAGS_REGEX.test(urlParts.pathname)) {
const r = TAGS_REGEX.exec(urlParts.pathname); const r = TAGS_REGEX.exec(urlParts.pathname);
const deviceId = decodeURIComponent(r[1]); const deviceId = querystring.unescape(r[1]);
const tag = decodeURIComponent(r[2]); const tag = querystring.unescape(r[2]);
if (request.method === "POST") { if (request.method === "POST") {
collections.devices.updateOne( db.devicesCollection.updateOne(
{ _id: deviceId }, { _id: deviceId },
{ $addToSet: { _tags: tag } }, { $addToSet: { _tags: tag } },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
} }
); );
} else if (request.method === "DELETE") { } else if (request.method === "DELETE") {
collections.devices.updateOne( db.devicesCollection.updateOne(
{ _id: deviceId }, { _id: deviceId },
{ $pull: { _tags: tag } }, { $pull: { _tags: tag } },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
response.writeHead(200); response.writeHead(200);
@@ -367,17 +318,17 @@ export function listener(
} }
} else if (FAULTS_REGEX.test(urlParts.pathname)) { } else if (FAULTS_REGEX.test(urlParts.pathname)) {
if (request.method === "DELETE") { if (request.method === "DELETE") {
const faultId = decodeURIComponent( const faultId = querystring.unescape(
FAULTS_REGEX.exec(urlParts.pathname)[1] FAULTS_REGEX.exec(urlParts.pathname)[1]
); );
const deviceId = faultId.split(":", 1)[0]; const deviceId = faultId.split(":", 1)[0];
const channel = faultId.slice(deviceId.length + 1); const channel = faultId.slice(deviceId.length + 1);
collections.faults.deleteOne({ _id: faultId }, (err) => { db.faultsCollection.deleteOne({ _id: faultId }, err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
if (channel.startsWith("task_")) { if (channel.startsWith("task_")) {
const objId = new ObjectId(channel.slice(5)); const objId = new mongodb.ObjectID(channel.slice(5));
return void collections.tasks.deleteOne({ _id: objId }, (err) => { return void db.tasksCollection.deleteOne({ _id: objId }, err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -386,7 +337,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -400,7 +351,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -412,169 +363,100 @@ export function listener(
} }
} else if (DEVICE_TASKS_REGEX.test(urlParts.pathname)) { } else if (DEVICE_TASKS_REGEX.test(urlParts.pathname)) {
if (request.method === "POST") { if (request.method === "POST") {
const deviceId = decodeURIComponent( const deviceId = querystring.unescape(
DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1] DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1]
); );
if (body.length) { if (body.length) {
let task; const task = JSON.parse(body.toString());
try {
task = JSON.parse(body.toString());
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
task.device = deviceId; task.device = deviceId;
collections.devices apiFunctions
.findOne({ _id: deviceId }) .insertTasks(task)
.then(async (dev) => { .then(() => {
if (!dev) { cache
response.writeHead(404); .del(`${deviceId}_tasks_faults_operations`)
response.end("No such device"); .then(() => {
return; if (urlParts.query.connection_request != null) {
} apiFunctions
.connectionRequest(deviceId)
.then(() => {
const taskTimeout =
(urlParts.query.timeout &&
parseInt(urlParts.query.timeout as string)) ||
config.get("DEVICE_ONLINE_THRESHOLD", deviceId);
const device = flattenDevice(dev); apiFunctions
await apiFunctions.insertTasks(task); .watchTask(deviceId, task._id, taskTimeout)
.then(status => {
if (status === "timeout") {
response.writeHead(
202,
"Task queued but not processed",
{
"Content-Type": "application/json"
}
);
response.end(JSON.stringify(task));
} else if (status === "fault") {
db.tasksCollection.findOne(
{ _id: task._id },
(err, task2) => {
if (err)
return void throwError(err, response);
const lastInform = Date.now(); response.writeHead(202, "Task faulted", {
"Content-Type": "application/json"
const socketTimeout: number = request.socket["timeout"]; });
response.end(JSON.stringify(task2));
// Disable socket timeout while waiting for session }
if (socketTimeout) request.socket.setTimeout(0); );
} else {
const notInSession = await apiFunctions.awaitSessionEnd( response.writeHead(200, {
deviceId, "Content-Type": "application/json"
30000 });
); response.end(JSON.stringify(task));
await cache.del(`${deviceId}_tasks_faults_operations`); }
if (urlParts.query.connection_request == null) { })
if (socketTimeout) request.socket.setTimeout(socketTimeout); .catch(err => {
response.writeHead(202, { setTimeout(() => {
"Content-Type": "application/json", throwError(err, response);
});
});
})
.catch(err => {
response.writeHead(202, err.message, {
"Content-Type": "application/json"
});
response.end(JSON.stringify(task));
});
} else {
response.writeHead(202, {
"Content-Type": "application/json"
});
response.end(JSON.stringify(task));
}
})
.catch(err => {
setTimeout(() => {
throwError(err, response);
});
}); });
response.end(JSON.stringify(task));
return;
}
if (!notInSession) {
if (socketTimeout) request.socket.setTimeout(socketTimeout);
response.writeHead(202, "Task queued but not processed", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
return;
}
const status = await apiFunctions.connectionRequest(
deviceId,
device
);
if (status) {
if (socketTimeout) request.socket.setTimeout(socketTimeout);
response.writeHead(202, status, {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
return;
}
const onlineThreshold =
(urlParts.query.timeout &&
parseInt(urlParts.query.timeout as string)) ||
(config.get("DEVICE_ONLINE_THRESHOLD", deviceId) as number);
const sessionStarted = await apiFunctions.awaitSessionStart(
deviceId,
lastInform,
onlineThreshold
);
if (!sessionStarted) {
if (socketTimeout) request.socket.setTimeout(socketTimeout);
response.writeHead(202, "Task queued but not processed", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
return;
}
const sessionEnded = await apiFunctions.awaitSessionEnd(
deviceId,
120000
);
if (!sessionEnded) {
if (socketTimeout) request.socket.setTimeout(socketTimeout);
response.writeHead(202, "Task queued but not processed", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
return;
}
const prom1 = collections.tasks.findOne(
{ _id: task._id },
{ projection: { _id: 1 } }
);
const prom2 = collections.faults.findOne(
{ _id: `${deviceId}:task_${task._id}` },
{
projection: { _id: 1 },
}
);
const [t, f] = await Promise.all([prom1, prom2]);
// Restore socket timeout
if (socketTimeout) request.socket.setTimeout(socketTimeout);
if (f) {
response.writeHead(202, "Task faulted", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(t || task));
} else if (t) {
response.writeHead(202, "Task queued but not processed", {
"Content-Type": "application/json",
});
response.end(JSON.stringify(t));
} else {
response.writeHead(200, {
"Content-Type": "application/json",
});
response.end(JSON.stringify(task));
}
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
}); });
} else if (urlParts.query.connection_request != null) { } else if (urlParts.query.connection_request != null) {
// No task, send connection request only // No task, send connection request only
collections.devices apiFunctions
.findOne({ _id: deviceId }) .connectionRequest(deviceId)
.then(async (dev) => { .then(() => {
if (!dev) { response.writeHead(200);
response.writeHead(404); response.end();
response.end("No such device");
return;
}
return apiFunctions.connectionRequest(deviceId).then((status) => {
if (status) {
response.writeHead(504, status);
response.end(status);
return;
}
response.writeHead(200);
response.end();
});
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { response.writeHead(504);
throwError(err, response); response.end(`${err.name}: ${err.message}`);
});
}); });
} else { } else {
response.writeHead(400); response.writeHead(400);
@@ -586,12 +468,12 @@ export function listener(
} }
} else if (TASKS_REGEX.test(urlParts.pathname)) { } else if (TASKS_REGEX.test(urlParts.pathname)) {
const r = TASKS_REGEX.exec(urlParts.pathname); const r = TASKS_REGEX.exec(urlParts.pathname);
const taskId = decodeURIComponent(r[1]); const taskId = querystring.unescape(r[1]);
const action = r[2]; const action = r[2];
if (!action || action === "/") { if (!action || action === "/") {
if (request.method === "DELETE") { if (request.method === "DELETE") {
collections.tasks.findOne( db.tasksCollection.findOne(
{ _id: new ObjectId(taskId) }, { _id: new mongodb.ObjectID(taskId) },
{ projection: { device: 1 } }, { projection: { device: 1 } },
(err, task) => { (err, task) => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
@@ -603,14 +485,14 @@ export function listener(
} }
const deviceId = task.device; const deviceId = task.device;
collections.tasks.deleteOne( db.tasksCollection.deleteOne(
{ _id: new ObjectId(taskId) }, { _id: new mongodb.ObjectID(taskId) },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
collections.faults.deleteOne( db.faultsCollection.deleteOne(
{ _id: `${deviceId}:task_${taskId}` }, { _id: `${deviceId}:task_${taskId}` },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -619,7 +501,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -636,16 +518,16 @@ export function listener(
} }
} else if (action === "/retry") { } else if (action === "/retry") {
if (request.method === "POST") { if (request.method === "POST") {
collections.tasks.findOne( db.tasksCollection.findOne(
{ _id: new ObjectId(taskId) }, { _id: new mongodb.ObjectID(taskId) },
{ projection: { device: 1 } }, { projection: { device: 1 } },
(err, task) => { (err, task) => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
const deviceId = task.device; const deviceId = task.device;
collections.faults.deleteOne( db.faultsCollection.deleteOne(
{ _id: `${deviceId}:task_${taskId}` }, { _id: `${deviceId}:task_${taskId}` },
(err) => { err => {
if (err) return void throwError(err, response); if (err) return void throwError(err, response);
cache cache
@@ -654,7 +536,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -672,7 +554,7 @@ export function listener(
response.end(); response.end();
} }
} else if (FILES_REGEX.test(urlParts.pathname)) { } else if (FILES_REGEX.test(urlParts.pathname)) {
const filename = decodeURIComponent( const filename = querystring.unescape(
FILES_REGEX.exec(urlParts.pathname)[1] FILES_REGEX.exec(urlParts.pathname)[1]
); );
if (request.method === "PUT") { if (request.method === "PUT") {
@@ -680,36 +562,25 @@ export function listener(
fileType: request.headers.filetype, fileType: request.headers.filetype,
oui: request.headers.oui, oui: request.headers.oui,
productClass: request.headers.productclass, productClass: request.headers.productclass,
version: request.headers.version, version: request.headers.version
}; };
filesBucket.delete(filename as unknown as ObjectId, () => { const bucket = new mongodb.GridFSBucket(db.client.db());
const uploadStream = filesBucket.openUploadStreamWithId( const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
filename as unknown as ObjectId, metadata: metadata
filename, });
{
metadata: metadata,
}
);
uploadStream.on("error", (err) => { uploadStream.on("error", err => {
throwError(err, response); throwError(err, response);
}); });
uploadStream.end(body, () => { uploadStream.end(body, () => {
response.writeHead(201); response.writeHead(201);
response.end(); response.end();
});
}); });
} else if (request.method === "DELETE") { } else if (request.method === "DELETE") {
filesBucket.delete(filename as unknown as ObjectId, (err) => { const bucket = new mongodb.GridFSBucket(db.client.db());
if (err) { bucket.delete((filename as unknown) as mongodb.ObjectId, err => {
if (err.message.startsWith("FileNotFound")) { if (err) return void throwError(err, response);
response.writeHead(404);
response.end("404 Not Found");
return;
}
return void throwError(err, response);
}
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
@@ -719,14 +590,10 @@ export function listener(
response.end("405 Method Not Allowed"); response.end("405 Method Not Allowed");
} }
} else if (PING_REGEX.test(urlParts.pathname)) { } else if (PING_REGEX.test(urlParts.pathname)) {
const host = decodeURIComponent(PING_REGEX.exec(urlParts.pathname)[1]); const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]);
ping(host, (err, res, stdout) => { ping(host, (err, res, stdout) => {
if (err) { if (err) {
if (!res) { if (!res) throwError(err, response);
response.writeHead(500, { Connection: "close" });
response.end(`${err.name}: ${err.message}`);
return;
}
response.writeHead(404, { "Cache-Control": "no-cache" }); response.writeHead(404, { "Cache-Control": "no-cache" });
response.end(`${err.name}: ${err.message}`); response.end(`${err.name}: ${err.message}`);
return; return;
@@ -734,7 +601,7 @@ export function listener(
response.writeHead(200, { response.writeHead(200, {
"Content-Type": "text/plain", "Content-Type": "text/plain",
"Cache-Control": "no-cache", "Cache-Control": "no-cache"
}); });
response.end(stdout); response.end(stdout);
}); });
@@ -745,7 +612,7 @@ export function listener(
return; return;
} }
const deviceId = decodeURIComponent( const deviceId = querystring.unescape(
DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1] DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1]
); );
apiFunctions apiFunctions
@@ -754,7 +621,7 @@ export function listener(
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
}) })
.catch((err) => { .catch(err => {
setTimeout(() => { setTimeout(() => {
throwError(err, response); throwError(err, response);
}); });
@@ -778,7 +645,7 @@ export function listener(
return; return;
} }
const collection = collections[collectionName]; const collection = db[`${collectionName}Collection`];
if (!collection) { if (!collection) {
response.writeHead(404); response.writeHead(404);
response.end("404 Not Found"); response.end("404 Not Found");
@@ -802,15 +669,15 @@ export function listener(
break; break;
case "tasks": case "tasks":
q = query.sanitizeQueryTypes(q, { q = query.sanitizeQueryTypes(q, {
_id: (v) => new ObjectId(v as string), _id: v => new mongodb.ObjectID(v),
timestamp: (v) => new Date(v as number), timestamp: v => new Date(v),
retries: Number, retries: Number
}); });
break; break;
case "faults": case "faults":
q = query.sanitizeQueryTypes(q, { q = query.sanitizeQueryTypes(q, {
timestamp: (v) => new Date(v as number), timestamp: v => new Date(v),
retries: Number, retries: Number
}); });
} }
@@ -824,14 +691,7 @@ export function listener(
const cur = collection.find(q, { projection: projection }); const cur = collection.find(q, { projection: projection });
if (urlParts.query.sort) { if (urlParts.query.sort) {
let s; const s = JSON.parse(urlParts.query.sort as string);
try {
s = JSON.parse(urlParts.query.sort as string);
} catch (err) {
response.writeHead(400);
response.end(`${err.name}: ${err.message}`);
return;
}
const sort = {}; const sort = {};
for (const [k, v] of Object.entries(s)) { for (const [k, v] of Object.entries(s)) {
if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices") if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices")
@@ -842,12 +702,18 @@ export function listener(
cur.sort(sort); cur.sort(sort);
} }
cur.count((err, total) => { if (urlParts.query.skip)
cur.skip(parseInt(urlParts.query.skip as string));
if (urlParts.query.limit)
cur.limit(parseInt(urlParts.query.limit as string));
cur.count(false, (err, total) => {
if (err) return void throwError(err); if (err) return void throwError(err);
response.writeHead(200, { response.writeHead(200, {
"Content-Type": "application/json", "Content-Type": "application/json",
total: total, total: total
}); });
if (request.method === "HEAD") { if (request.method === "HEAD") {
@@ -855,20 +721,14 @@ export function listener(
return; return;
} }
if (urlParts.query.skip)
cur.skip(parseInt(urlParts.query.skip as string));
if (urlParts.query.limit)
cur.limit(parseInt(urlParts.query.limit as string));
response.write("[\n"); response.write("[\n");
i = 0; i = 0;
cur.forEach( cur.forEach(
(item) => { item => {
if (i++) response.write(",\n"); if (i++) response.write(",\n");
response.write(JSON.stringify(item)); response.write(JSON.stringify(item));
}, },
(err) => { err => {
if (err) return void throwError(err); if (err) return void throwError(err);
response.end("\n]"); response.end("\n]");
} }
+9 -27
View File
@@ -19,9 +19,8 @@
import { platform } from "os"; import { platform } from "os";
import { exec } from "child_process"; import { exec } from "child_process";
import { domainToASCII } from "url";
export interface PingResult { interface Ping {
packetsTransmitted: number; packetsTransmitted: number;
packetsReceived: number; packetsReceived: number;
packetLoss: number; packetLoss: number;
@@ -31,41 +30,24 @@ export interface PingResult {
mdev: number; mdev: number;
} }
function isValidHost(host: string): boolean {
// Valid chars in IPv4, IPv6, domain names
if (/^[a-zA-Z0-9\-.:[\]-]+$/.test(host)) return true;
// Check if input is an IDN convert to Punycode
// Can't merge with above because domainToASCII doesn't accept IP addresses
return /^[a-zA-Z0-9\-.:[\]-]+$/.test(domainToASCII(host));
}
export function ping( export function ping(
host: string, host: string,
callback: (err: Error, res?: PingResult, stdout?: string) => void callback: (err, res?, stdout?) => void
): void { ): void {
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp; let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
// Validate input to prevent possible remote code execution
// Credit to Alex Hordijk for reporting this vulnerability
if (!isValidHost(host)) return callback(new Error("Invalid host"));
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}`;
parseRegExp1 = parseRegExp1 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
/(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/; parseRegExp2 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/;
parseRegExp2 =
/(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/;
break; break;
case "freebsd": case "freebsd":
// Send a single packet because on FreeBSD only superuser can send // Send a single packet because on FreeBSD only superuser can send
// packets that are only 200 ms apart. // packets that are only 200 ms apart.
cmd = `ping -t 1 -c 3 ${host}`; cmd = `ping -t 1 -c 3 ${host}`;
parseRegExp1 = parseRegExp1 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/; parseRegExp2 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
parseRegExp2 =
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
break; break;
default: default:
@@ -73,7 +55,7 @@ export function ping(
} }
exec(cmd, (err, stdout) => { exec(cmd, (err, stdout) => {
let parsed: PingResult; let parsed: Ping;
if (stdout) { if (stdout) {
const m1 = stdout.match(parseRegExp1); const m1 = stdout.match(parseRegExp1);
if (m1) { if (m1) {
@@ -84,7 +66,7 @@ export function ping(
min: +m1[4], min: +m1[4],
avg: +m1[5], avg: +m1[5],
max: +m1[6], max: +m1[6],
mdev: +m1[7], mdev: +m1[7]
}; };
} else { } else {
const m2 = stdout.match(parseRegExp2); const m2 = stdout.match(parseRegExp2);
@@ -96,7 +78,7 @@ export function ping(
min: null, min: null,
avg: null, avg: null,
max: null, max: null,
mdev: null, mdev: null
}; };
} }
} }
+98 -18
View File
@@ -17,9 +17,7 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
function isObject(obj: any): boolean { import * as common from "./common";
return Object.prototype.toString.call(obj) === "[object Object]";
}
function stringToRegexp(input, flags?): RegExp | false { function stringToRegexp(input, flags?): RegExp | false {
if (input.indexOf("*") === -1) return false; if (input.indexOf("*") === -1) return false;
@@ -36,8 +34,8 @@ function stringToRegexp(input, flags?): RegExp | false {
} }
function normalize(input): any { function normalize(input): any {
if (typeof input === "string") { if (common.typeOf(input) === common.STRING_TYPE) {
const vals: any = [input]; const vals = [input];
const m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input); const m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input);
if (m) vals.push({ $regex: new RegExp(m[1], m[2]) }); if (m) vals.push({ $regex: new RegExp(m[1], m[2]) });
@@ -54,14 +52,14 @@ function normalize(input): any {
return input; return input;
} }
function expandValue(value: unknown): unknown[] { export function expandValue(value): any[] {
if (Array.isArray(value)) { if (common.typeOf(value) === common.ARRAY_TYPE) {
let a = []; let a = [];
for (const j of value) a = a.concat(expandValue(j)); for (const j of value) a = a.concat(expandValue(j));
return [a]; return [a];
} else if (!isObject(value)) { } else if (common.typeOf(value) !== common.OBJECT_TYPE) {
const n = normalize(value); const n = normalize(value);
if (!Array.isArray(n)) return [n]; if (common.typeOf(n) !== common.ARRAY_TYPE) return [n];
else return n; else return n;
} }
@@ -105,14 +103,12 @@ function permute(param, val): any[] {
return conditions; return conditions;
} }
export function expand( export function expand(query): {} {
query: Record<string, unknown>
): Record<string, unknown> {
const newQuery = {}; const newQuery = {};
for (const [k, v] of Object.entries(query)) { for (const [k, v] of Object.entries(query)) {
if (k[0] === "$") { if (k[0] === "$") {
// Operator // Operator
newQuery[k] = (v as any[]).map((e) => expand(e)); newQuery[k] = (v as any[]).map(e => expand(e));
} else { } else {
const conditions = permute(k, v); const conditions = permute(k, v);
if (conditions.length > 1) { if (conditions.length > 1) {
@@ -133,16 +129,100 @@ export function expand(
return newQuery; return newQuery;
} }
export function sanitizeQueryTypes( function testExpressions(params, expressions, lop): boolean {
query: Record<string, unknown>, for (const f of expressions) {
types: Record<string, (v: unknown) => unknown> const res = test(params, f);
): Record<string, unknown> { switch (lop) {
case "$and":
if (!res) return false;
break;
case "$or":
if (res) return true;
break;
case "$nor":
if (res) return false;
}
}
switch (lop) {
case "$and":
return true;
case "$or":
return false;
case "$nor":
return true;
default:
throw new Error("Unknown logical operator");
}
}
export function test(params, query): boolean {
let res;
for (const [k, v] of Object.entries(query)) {
if (k.charAt(0) === "$") {
// Logical operator
res = testExpressions(params, v, k);
} else {
const value = params[k];
if (common.typeOf(v) !== common.OBJECT_TYPE) {
if (common.typeOf(value) === common.ARRAY_TYPE) {
// TODO comparing array to regex, array to array, and object to object
res = value.indexOf(v) !== -1;
} else {
if (common.typeOf(v) === common.REGEXP_TYPE)
res = (v as RegExp).test(value);
else res = v === value;
}
} else {
for (const [k2, v2] of Object.entries(v)) {
switch (k2) {
case "$ne":
if (common.typeOf(value) === common.ARRAY_TYPE)
res = value.indexOf(v2) === -1;
else res = value !== v2;
break;
case "$lt":
res = value < v2;
break;
case "$lte":
res = value <= v2;
break;
case "$gt":
res = value > v2;
break;
case "$gte":
res = value >= v2;
break;
case "$regex":
res = v2.test(value);
break;
case "$in":
throw new Error("Operator not supported");
case "$nin":
throw new Error("Operator not supported");
case "$all":
throw new Error("Operator not supported");
case "$exists":
throw new Error("Operator not supported");
default:
throw new Error("Operator not supported");
}
}
}
}
if (!res) return false;
}
return true;
}
export function sanitizeQueryTypes(query, types): {} {
for (const [k, v] of Object.entries(query)) { for (const [k, v] of Object.entries(query)) {
if (k[0] === "$") { if (k[0] === "$") {
// Logical operator // Logical operator
for (const vv of v as any[]) sanitizeQueryTypes(vv, types); for (const vv of v as any[]) sanitizeQueryTypes(vv, types);
} else if (k in types) { } else if (k in types) {
if (isObject(v)) { if (common.typeOf(v) === common.OBJECT_TYPE) {
for (const [kk, vv] of Object.entries(v)) { for (const [kk, vv] of Object.entries(v)) {
switch (kk) { switch (kk) {
case "$in": case "$in":
+20 -30
View File
@@ -109,16 +109,6 @@ class SandboxDate {
return t; return t;
} }
public static parse(dateString: string): number {
return Date.parse(dateString);
}
public static UTC(
...args: [number, number?, number?, number?, number?, number?, number?]
): number {
return Date.UTC(...args);
}
} }
function random(): number { function random(): number {
@@ -127,7 +117,7 @@ function random(): number {
return state.rng(); return state.rng();
} }
random.seed = function (s) { random.seed = function(s) {
state.rng = seedrandom(s); state.rng = seedrandom(s);
}; };
@@ -135,7 +125,7 @@ class ParameterWrapper {
public constructor(path: Path, attributes, unpacked?, unpackedRevision?) { public constructor(path: Path, attributes, unpacked?, unpackedRevision?) {
for (const attrName of attributes) { for (const attrName of attributes) {
Object.defineProperty(this, attrName, { Object.defineProperty(this, attrName, {
get: function () { get: function() {
if (state.uncommitted) commit(); if (state.uncommitted) commit();
if (state.revision !== unpackedRevision) { if (state.revision !== unpackedRevision) {
@@ -157,12 +147,12 @@ class ParameterWrapper {
if (!attr) return UNDEFINED; if (!attr) return UNDEFINED;
return attr[1]; return attr[1];
}, }
}); });
} }
Object.defineProperty(this, "path", { Object.defineProperty(this, "path", {
get: function () { get: function() {
if (state.uncommitted) commit(); if (state.uncommitted) commit();
if (state.revision !== unpackedRevision) { if (state.revision !== unpackedRevision) {
@@ -177,11 +167,11 @@ class ParameterWrapper {
if (!unpacked.length) return UNDEFINED; if (!unpacked.length) return UNDEFINED;
return unpacked[0].toString(); return unpacked[0].toString();
}, }
}); });
Object.defineProperty(this, "size", { Object.defineProperty(this, "size", {
get: function () { get: function() {
if (state.uncommitted) commit(); if (state.uncommitted) commit();
if (state.revision !== unpackedRevision) { if (state.revision !== unpackedRevision) {
@@ -196,10 +186,10 @@ class ParameterWrapper {
if (!unpacked.length) return UNDEFINED; if (!unpacked.length) return UNDEFINED;
return unpacked.length; return unpacked.length;
}, }
}); });
this[Symbol.iterator] = function* () { this[Symbol.iterator] = function*() {
if (state.uncommitted) commit(); if (state.uncommitted) commit();
if (state.revision !== unpackedRevision) { if (state.revision !== unpackedRevision) {
@@ -235,7 +225,7 @@ function declare(
pathSet: null, pathSet: null,
attrGet: null, attrGet: null,
attrSet: null, attrSet: null,
defer: true, defer: true
}; };
const attrs = new Set(); const attrs = new Set();
@@ -290,9 +280,9 @@ function commit(): void {
} }
} }
function ext(...args: unknown[]): any { function ext(): any {
++state.extCounter; ++state.extCounter;
const extCall = args.map(String); const extCall = Array.from(arguments).map(String);
const key = `${state.revision}: ${JSON.stringify(extCall)}`; const key = `${state.revision}: ${JSON.stringify(extCall)}`;
if (key in state.sessionContext.extensionsCache) if (key in state.sessionContext.extensionsCache)
@@ -302,11 +292,11 @@ function ext(...args: unknown[]): any {
throw EXT; throw EXT;
} }
function log(msg: string, meta: Record<string, unknown>): void { function log(msg: string, meta: {}): void {
if (state.revision === state.maxRevision && state.extCounter >= 0) { if (state.revision === state.maxRevision && state.extCounter >= 0) {
const details = Object.assign({}, meta, { const details = Object.assign({}, meta, {
sessionContext: state.sessionContext, sessionContext: state.sessionContext,
message: `Script: ${msg}`, message: `Script: ${msg}`
}); });
delete details["hostname"]; delete details["hostname"];
@@ -342,8 +332,8 @@ function errorToFault(err: Error): Fault {
message: err.message, message: err.message,
detail: { detail: {
name: err.name, name: err.name,
message: err.message, message: err.message
}, }
}; };
if (err.stack) { if (err.stack) {
@@ -365,7 +355,7 @@ function errorToFault(err: Error): Fault {
export async function run( export async function run(
script: vm.Script, script: vm.Script,
globals: Record<string, unknown>, globals: {},
sessionContext: SessionContext, sessionContext: SessionContext,
startRevision: number, startRevision: number,
maxRevision: number, maxRevision: number,
@@ -380,7 +370,7 @@ export async function run(
extensions: {}, extensions: {},
clear: [], clear: [],
rng: null, rng: null,
extCounter: extCounter, extCounter: extCounter
}; };
for (const n of Object.keys(context)) delete context[n]; for (const n of Object.keys(context)) delete context[n];
@@ -403,7 +393,7 @@ export async function run(
clear: null, clear: null,
declare: null, declare: null,
done: false, done: false,
returnValue: null, returnValue: null
}; };
} }
} }
@@ -423,7 +413,7 @@ export async function run(
clear: null, clear: null,
declare: null, declare: null,
done: false, done: false,
returnValue: null, returnValue: null
}; };
} }
@@ -443,6 +433,6 @@ export async function run(
clear: _state.clear, clear: _state.clear,
declare: _state.declarations, declare: _state.declarations,
done: status === 0, done: status === 0,
returnValue: ret, returnValue: ret
}; };
} }
+11 -16
View File
@@ -18,10 +18,13 @@
*/ */
import * as crypto from "crypto"; import * as crypto from "crypto";
import * as later from "@breejs/later"; import * as later from "later";
function md532(str): number { function md532(str): number {
const digest = crypto.createHash("md5").update(str).digest(); const digest = crypto
.createHash("md5")
.update(str)
.digest();
return ( return (
digest.readUInt32LE(0) ^ digest.readUInt32LE(0) ^
digest.readUInt32LE(4) ^ digest.readUInt32LE(4) ^
@@ -30,37 +33,29 @@ function md532(str): number {
); );
} }
export function variance(deviceId: string, vrnc: number): number { export function variance(deviceId, vrnc): number {
return (md532(deviceId) >>> 0) % vrnc; return (md532(deviceId) >>> 0) % vrnc;
} }
export function interval( export function interval(timestamp, intrvl, offset = 0): number {
timestamp: number,
intrvl: number,
offset = 0
): number {
return Math.trunc((timestamp + offset) / intrvl) * intrvl - offset; return Math.trunc((timestamp + offset) / intrvl) * intrvl - offset;
} }
export function parseCron(cronExp: string): any { export function parseCron(cronExp): any {
const parts = cronExp.trim().split(/\s+/); const parts = cronExp.trim().split(/\s+/);
if (parts.length === 5) parts.unshift("*"); if (parts.length === 5) parts.unshift("*");
return later.schedule(later.parse.cron(parts.join(" "), true)); return later.schedule(later.parse.cron(parts.join(" "), true));
} }
export function cron( export function cron(timestamp, schedule, offset = 0): number[] {
timestamp: number,
schedule: unknown,
offset = 0
): number[] {
// TODO later.js doesn't throw erorr if expression is invalid! // TODO later.js doesn't throw erorr if expression is invalid!
const ret = [0, 0]; const ret = [0, 0];
const prev = (schedule as any).prev(1, new Date(timestamp + offset)); const prev = schedule.prev(1, new Date(timestamp + offset));
if (prev) ret[0] = prev.setMilliseconds(0) - offset; if (prev) ret[0] = prev.setMilliseconds(0) - offset;
const next = (schedule as any).next(1, new Date(timestamp + offset + 1000)); const next = schedule.next(1, new Date(timestamp + offset + 1000));
if (next) ret[1] = next.setMilliseconds(0) - offset; if (next) ret[1] = next.setMilliseconds(0) - offset;
return ret; return ret;
+17 -26
View File
@@ -20,7 +20,6 @@
import * as fs from "fs"; import * as fs from "fs";
import * as http from "http"; import * as http from "http";
import * as https from "https"; import * as https from "https";
import { Socket } from "net";
import * as path from "path"; import * as path from "path";
import { ROOT_DIR } from "./config"; import { ROOT_DIR } from "./config";
@@ -52,43 +51,35 @@ function closeServer(timeout, callback): void {
}); });
} }
interface ServerOptions {
port?: number;
host?: string;
ssl?: { key: string; cert: string };
timeout?: number;
keepAliveTimeout?: number;
onConnection?: (socket: Socket) => void;
}
export function start( export function start(
options: ServerOptions, port,
_listener: http.RequestListener networkInterface,
ssl,
_listener,
onConnection?,
keepAliveTimeout: number = -1
): void { ): void {
listener = _listener; listener = _listener;
if (options.ssl) { if (ssl && ssl.key && ssl.cert) {
const opts = { const options = {
key: options.ssl.key key: ssl.key
.split(":") .split(":")
.map((f) => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))), .map(f => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
cert: options.ssl.cert cert: ssl.cert
.split(":") .split(":")
.map((f) => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))), .map(f => fs.readFileSync(path.resolve(ROOT_DIR, f.trim())))
}; };
server = https.createServer(opts, listener); server = https.createServer(options, listener);
if (options.onConnection) if (onConnection != null) server.on("secureConnection", onConnection);
server.on("secureConnection", options.onConnection);
} else { } else {
server = http.createServer(listener); server = http.createServer(listener);
if (options.onConnection) server.on("connection", options.onConnection); if (onConnection != null) server.on("connection", onConnection);
} }
server.timeout = options.timeout || 0; if (keepAliveTimeout >= 0) server.keepAliveTimeout = keepAliveTimeout;
if (options.keepAliveTimeout != null) server.listen(port, networkInterface);
server.keepAliveTimeout = options.keepAliveTimeout;
server.listen({ port: options.port, host: options.host });
} }
export function stop(): Promise<void> { export function stop(): Promise<void> {
+477 -688
View File
File diff suppressed because it is too large. Load diff
+745
View File
@@ -0,0 +1,745 @@
/**
* 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 { parseXml, Document, Element, ParserOptions } from "libxmljs";
import * as config from "./config";
import { version as VERSION } from "../package.json";
import {
CpeGetResponse,
CpeSetResponse,
InformRequest,
AcsResponse,
CpeRequest,
FaultStruct,
SpvFault,
CpeFault,
SoapMessage,
TransferCompleteRequest
} from "./types";
const SERVER_NAME = `GenieACS/${VERSION}`;
const NAMESPACES = {
"1.0": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-0"
},
"1.1": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-1"
},
"1.2": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-2"
},
"1.3": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-2"
},
"1.4": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-3"
}
};
const LIBXMLJS_PARSE_OPTIONS: ParserOptions = {
nocdata: true,
recover: !!config.get("XML_RECOVER"),
/* eslint-disable-next-line @typescript-eslint/camelcase */
ignore_enc: !!config.get("XML_IGNORE_ENC")
};
interface WriterOptions {
declaration?: boolean;
format?: boolean;
selfCloseEmpty?: boolean;
whitespace?: boolean;
type?: "XML" | "HTML" | "XHTML";
}
declare module "libxmljs" {
interface Document {
toString(formatting: boolean | WriterOptions): string;
}
}
const LIBXMLJS_SAVE_OPTIONS: WriterOptions = {
format: !!config.get("XML_FORMAT"),
declaration: !config.get("XML_NO_DECL"),
selfCloseEmpty: !config.get("XML_NO_EMPTY")
};
let warnings;
function parseBool(v): boolean {
v = "" + v;
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
return false;
else return null;
}
function event(xml): string[] {
return xml
.childNodes()
.filter(n => n.name() === "EventStruct")
.map(c =>
c
.childNodes()
.find(n => n.name() === "EventCode")
.text()
.trim()
);
}
function parameterInfoList(xml): [string, boolean][] {
return xml
.childNodes()
.filter(e => e.name() === "ParameterInfoStruct")
.map(e => {
let param, value;
for (const c of e.childNodes()) {
switch (c.name()) {
case "Name":
param = c.text();
break;
case "Writable":
value = c.text();
break;
}
}
let parsed = parseBool(value);
if (parsed === null) {
warnings.push({
message: "Invalid writable attribute",
parameter: param
});
parsed = false;
}
return [param, parsed];
});
}
function parameterValueList(
xml
): [string, string | number | boolean, string][] {
return xml
.childNodes()
.filter(e => e.name() === "ParameterValueStruct")
.map(e => {
let valueElement, param;
for (const c of e.childNodes()) {
switch (c.name()) {
case "Name":
param = c.text();
break;
case "Value":
valueElement = c;
break;
}
}
const valueType = valueElement.attr("type").value();
const value = valueElement.text();
let parsed = value;
if (valueType === "xsd:boolean") {
parsed = parseBool(value);
if (parsed === null) {
warnings.push({
message: "Invalid value attribute",
parameter: param
});
parsed = value;
}
} else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt") {
parsed = parseInt(value);
if (isNaN(parsed)) {
warnings.push({
message: "Invalid value attribute",
parameter: param
});
parsed = value;
}
} else if (valueType === "xsd:dateTime") {
parsed = Date.parse(value);
if (isNaN(parsed)) {
warnings.push({
message: "Invalid value attribute",
parameter: param
});
parsed = value;
}
}
return [param, parsed, valueType];
});
}
function GetParameterNames(xml, methodRequest): void {
const el = xml.node("cwmp:GetParameterNames");
el.node("ParameterPath").text(methodRequest.parameterPath);
el.node("NextLevel").text(+methodRequest.nextLevel);
}
function GetParameterNamesResponse(xml): CpeGetResponse {
return {
name: "GetParameterNamesResponse",
parameterList: parameterInfoList(
xml.childNodes().find(n => n.name() === "ParameterList")
)
};
}
function GetParameterValues(xml, methodRequest): void {
const el = xml.node("cwmp:GetParameterValues").node("ParameterNames");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]`
});
for (const p of methodRequest.parameterNames) el.node("string").text(p);
}
function GetParameterValuesResponse(xml): CpeGetResponse {
return {
name: "GetParameterValuesResponse",
parameterList: parameterValueList(
xml.childNodes().find(n => n.name() === "ParameterList")
)
};
}
function SetParameterValues(xml, methodRequest): void {
const el = xml.node("cwmp:SetParameterValues");
const paramList = el.node("ParameterList");
paramList.attr({
"soap-enc:arrayType": `cwmp:ParameterValueStruct[${methodRequest.parameterList.length}]`
});
for (const p of methodRequest.parameterList) {
const pvs = paramList.node("ParameterValueStruct");
pvs.node("Name").text(p[0]);
let val = p[1];
if (p[2] === "xsd:dateTime" && typeof val === "number") {
val = new Date(val).toISOString();
if (methodRequest.DATETIME_MILLISECONDS === false)
val = val.replace(".000", "");
}
if (p[2] === "xsd:boolean" && typeof val === "boolean")
if (methodRequest.BOOLEAN_LITERAL === false) val = +val;
pvs
.node("Value")
.attr({ "xsi:type": p[2] })
.text(val);
}
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function SetParameterValuesResponse(xml): CpeSetResponse {
return {
name: "SetParameterValuesResponse",
status: parseInt(
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
};
}
function AddObject(xml, methodRequest): void {
const el = xml.node("cwmp:AddObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function AddObjectResponse(xml): CpeSetResponse {
let instanceNumber, status;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "InstanceNumber":
instanceNumber = parseInt(c.text());
break;
case "Status":
status = parseInt(c.text());
break;
}
}
return {
name: "AddObjectResponse",
instanceNumber: instanceNumber,
status: status
};
}
function DeleteObject(xml, methodRequest): void {
const el = xml.node("cwmp:DeleteObject");
el.node("ObjectName").text(methodRequest.objectName);
el.node("ParameterKey").text(methodRequest.parameterKey || "");
}
function DeleteObjectResponse(xml): CpeSetResponse {
return {
name: "DeleteObjectResponse",
status: parseInt(
xml
.childNodes()
.find(n => n.name() === "Status")
.text()
)
};
}
function Reboot(xml, methodRequest): void {
const el = xml.node("cwmp:Reboot");
el.node("CommandKey").text(methodRequest.commandKey || "");
}
function RebootResponse(): CpeSetResponse {
return {
name: "RebootResponse"
};
}
function FactoryReset(xml): void {
xml.node("cwmp:FactoryReset");
}
function FactoryResetResponse(): CpeSetResponse {
return {
name: "FactoryResetResponse"
};
}
function Download(xml, methodRequest): void {
const el = xml.node("cwmp:Download");
el.node("CommandKey").text(methodRequest.commandKey || "");
el.node("FileType").text(methodRequest.fileType);
el.node("URL").text(methodRequest.url);
el.node("Username").text(methodRequest.username || "");
el.node("Password").text(methodRequest.password || "");
el.node("FileSize").text(methodRequest.fileSize || "0");
el.node("TargetFileName").text(methodRequest.targetFileName || "");
el.node("DelaySeconds").text(methodRequest.delaySeconds || "0");
el.node("SuccessURL").text(methodRequest.successUrl || "");
el.node("FailureURL").text(methodRequest.failureUrl || "");
}
function DownloadResponse(xml): CpeSetResponse {
let status, startTime, completeTime;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "Status":
status = parseInt(c.text());
break;
case "StartTime":
startTime = Date.parse(c.text());
break;
case "CompleteTime":
completeTime = Date.parse(c.text());
break;
}
}
return {
name: "DownloadResponse",
status: status,
startTime: startTime,
completeTime: completeTime
};
}
function Inform(xml): InformRequest {
let retryCount, evnt, parameterList;
const deviceId = {
Manufacturer: null,
OUI: null,
ProductClass: null,
SerialNumber: null
};
for (const c of xml.childNodes()) {
switch (c.name()) {
case "ParameterList":
parameterList = parameterValueList(c);
break;
case "DeviceId":
for (const cc of c.childNodes()) {
const n = cc.name();
if (n in deviceId) deviceId[n] = cc.text();
}
break;
case "Event":
evnt = event(c);
break;
case "RetryCount":
retryCount = parseInt(c.text());
break;
}
}
return {
name: "Inform",
parameterList: parameterList,
deviceId: deviceId,
event: evnt,
retryCount: retryCount
};
}
function InformResponse(xml): void {
xml
.node("cwmp:InformResponse")
.node("MaxEnvelopes")
.text(1);
}
function GetRPCMethods(): AcsResponse {
return { name: "GetRPCMethods" };
}
function GetRPCMethodsResponse(xml, methodResponse): void {
const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList");
el.attr({
"soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]`
});
for (const m of methodResponse.methodList) el.node("string").text(m);
}
function TransferComplete(xml): TransferCompleteRequest {
let commandKey, _faultStruct, startTime, completeTime;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "CommandKey":
commandKey = c.text();
break;
case "FaultStruct":
_faultStruct = faultStruct(c);
break;
case "StartTime":
startTime = Date.parse(c.text());
break;
case "CompleteTime":
completeTime = Date.parse(c.text());
break;
}
}
return {
name: "TransferComplete",
commandKey: commandKey,
faultStruct: _faultStruct,
startTime: startTime,
completeTime: completeTime
};
}
function TransferCompleteResponse(xml): void {
xml.node("cwmp:TransferCompleteResponse").text("");
}
function RequestDownload(xml): CpeRequest {
return {
name: "RequestDownload",
fileType: xml
.childNodes()
.find(n => n.name() === "FileType")
.text()
};
}
function RequestDownloadResponse(xml): void {
xml.node("cwmp:RequestDownloadResponse").text("");
}
function faultStruct(xml): FaultStruct {
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "FaultCode":
faultCode = c.text();
break;
case "FaultString":
faultString = c.text();
break;
case "SetParameterValuesFault":
setParameterValuesFault = setParameterValuesFault || [];
for (const cc of c.childNodes()) {
switch (cc.name()) {
case "ParameterName":
pn = cc.text();
break;
case "FaultCode":
fc = cc.text();
break;
case "FaultString":
fs = cc.text();
break;
}
}
setParameterValuesFault.push({
parameterName: pn,
faultCode: fc,
faultString: fs
});
}
}
return { faultCode, faultString, setParameterValuesFault };
}
function fault(xml): CpeFault {
let faultCode, faultString, detail;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "faultcode":
faultCode = c.text();
break;
case "faultstring":
faultString = c.text();
break;
case "detail":
detail = faultStruct(c.childNodes().find(n => n.name() === "Fault"));
break;
}
}
return { faultCode, faultString, detail };
}
export function request(data, cwmpVersion, warn): SoapMessage {
warnings = warn;
const rpc = {
id: null,
cwmpVersion: cwmpVersion,
sessionTimeout: null,
cpeRequest: null,
cpeFault: null,
cpeResponse: null
};
if (!data.length) return rpc;
const xml = parseXml(data, LIBXMLJS_PARSE_OPTIONS);
let headerElement, bodyElement;
for (const c of xml.childNodes()) {
switch (c.name()) {
case "Header":
headerElement = c;
break;
case "Body":
bodyElement = c;
break;
}
}
if (headerElement) {
for (const c of headerElement.childNodes()) {
switch (c.name()) {
case "ID":
rpc.id = c.text();
break;
case "sessionTimeout":
rpc.sessionTimeout = parseInt(c.text());
break;
}
}
}
for (const methodElement of bodyElement.childNodes()) {
const methodName = methodElement.name();
if (methodName === "text") continue;
if (!rpc.cwmpVersion && methodName !== "Fault") {
switch (methodElement.namespace().href()) {
case "urn:dslforum-org:cwmp-1-0":
rpc.cwmpVersion = "1.0";
break;
case "urn:dslforum-org:cwmp-1-1":
rpc.cwmpVersion = "1.1";
break;
case "urn:dslforum-org:cwmp-1-2":
if (rpc.sessionTimeout) rpc.cwmpVersion = "1.3";
else rpc.cwmpVersion = "1.2";
break;
case "urn:dslforum-org:cwmp-1-3":
rpc.cwmpVersion = "1.4";
break;
default:
throw new Error("Unrecognized CWMP version");
}
}
switch (methodName) {
case "Inform":
rpc.cpeRequest = Inform(methodElement);
break;
case "GetRPCMethods":
rpc.cpeRequest = GetRPCMethods();
break;
case "TransferComplete":
rpc.cpeRequest = TransferComplete(methodElement);
break;
case "RequestDownload":
rpc.cpeRequest = RequestDownload(methodElement);
break;
case "GetParameterNamesResponse":
rpc.cpeResponse = GetParameterNamesResponse(methodElement);
break;
case "GetParameterValuesResponse":
rpc.cpeResponse = GetParameterValuesResponse(methodElement);
break;
case "SetParameterValuesResponse":
rpc.cpeResponse = SetParameterValuesResponse(methodElement);
break;
case "AddObjectResponse":
rpc.cpeResponse = AddObjectResponse(methodElement);
break;
case "DeleteObjectResponse":
rpc.cpeResponse = DeleteObjectResponse(methodElement);
break;
case "RebootResponse":
rpc.cpeResponse = RebootResponse();
break;
case "FactoryResetResponse":
rpc.cpeResponse = FactoryResetResponse();
break;
case "DownloadResponse":
rpc.cpeResponse = DownloadResponse(methodElement);
break;
case "Fault":
rpc.cpeFault = fault(methodElement);
break;
default:
throw new Error(`8000 Method not supported ${methodName}`);
}
}
return rpc;
}
function createSoapEnv(cwmpVersion: string): Element {
const xml = new Document();
const env = xml.node("soap-env:Envelope");
for (const prefix of Object.keys(NAMESPACES[cwmpVersion]))
env.defineNamespace(prefix, NAMESPACES[cwmpVersion][prefix]);
return env;
}
export function response(rpc): { code: number; headers: {}; data: string } {
const headers = {
Server: SERVER_NAME,
SOAPServer: SERVER_NAME
};
if (!rpc) return { code: 204, headers: headers, data: "" };
const env = createSoapEnv(rpc.cwmpVersion);
const header = env.node("soap-env:Header");
header
.node("cwmp:ID")
.attr({
"soap-env:mustUnderstand": "1"
})
.text(rpc.id);
const body = env.node("soap-env:Body");
if (rpc.acsResponse) {
switch (rpc.acsResponse.name) {
case "InformResponse":
InformResponse(body);
break;
case "GetRPCMethodsResponse":
GetRPCMethodsResponse(body, rpc.acsResponse);
break;
case "TransferCompleteResponse":
TransferCompleteResponse(body);
break;
case "RequestDownloadResponse":
RequestDownloadResponse(body);
break;
default:
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
}
} else if (rpc.acsRequest) {
switch (rpc.acsRequest.name) {
case "GetParameterNames":
GetParameterNames(body, rpc.acsRequest);
break;
case "GetParameterValues":
GetParameterValues(body, rpc.acsRequest);
break;
case "SetParameterValues":
SetParameterValues(body, rpc.acsRequest);
break;
case "AddObject":
AddObject(body, rpc.acsRequest);
break;
case "DeleteObject":
DeleteObject(body, rpc.acsRequest);
break;
case "Reboot":
Reboot(body, rpc.acsRequest);
break;
case "FactoryReset":
FactoryReset(body);
break;
case "Download":
Download(body, rpc.acsRequest);
break;
default:
throw new Error(`Unknown method request ${rpc.acsRequest.name}`);
}
}
headers["Content-Type"] = 'text/xml; charset="utf-8"';
return {
code: 200,
headers: headers,
data: env.doc().toString(LIBXMLJS_SAVE_OPTIONS)
};
}
+135 -398
View File
@@ -22,34 +22,22 @@ import {
Element, Element,
parseAttrs, parseAttrs,
encodeEntities, encodeEntities,
decodeEntities, decodeEntities
} from "./xml-parser"; } from "./xml-parser";
import memoize from "./common/memoize"; import memoize from "./common/memoize";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
import { import {
CpeGetResponse,
CpeSetResponse,
InformRequest, InformRequest,
AcsResponse,
CpeRequest,
FaultStruct, FaultStruct,
SpvFault, SpvFault,
CpeFault, CpeFault,
SoapMessage, SoapMessage,
TransferCompleteRequest, TransferCompleteRequest
AcsRequest,
GetParameterNamesResponse,
GetParameterValuesResponse,
GetParameterAttributesResponse,
SetParameterValuesResponse,
SetParameterAttributesResponse,
AddObjectResponse,
DeleteObject,
DeleteObjectResponse,
RebootResponse,
FactoryResetResponse,
DownloadResponse,
GetRPCMethodsRequest,
RequestDownloadRequest,
AcsResponse,
} from "./types"; } from "./types";
import Path from "./common/path";
const SERVER_NAME = `GenieACS/${VERSION}`; const SERVER_NAME = `GenieACS/${VERSION}`;
@@ -59,58 +47,60 @@ const NAMESPACES = {
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema", xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance", xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-0", cwmp: "urn:dslforum-org:cwmp-1-0"
}, },
"1.1": { "1.1": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/", "soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema", xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance", xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-1", cwmp: "urn:dslforum-org:cwmp-1-1"
}, },
"1.2": { "1.2": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/", "soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema", xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance", xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-2", cwmp: "urn:dslforum-org:cwmp-1-2"
}, },
"1.3": { "1.3": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/", "soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema", xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance", xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-2", cwmp: "urn:dslforum-org:cwmp-1-2"
}, },
"1.4": { "1.4": {
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/", "soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/", "soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
xsd: "http://www.w3.org/2001/XMLSchema", xsd: "http://www.w3.org/2001/XMLSchema",
xsi: "http://www.w3.org/2001/XMLSchema-instance", xsi: "http://www.w3.org/2001/XMLSchema-instance",
cwmp: "urn:dslforum-org:cwmp-1-3", cwmp: "urn:dslforum-org:cwmp-1-3"
}, }
}; };
let warnings: Record<string, unknown>[]; let warnings;
const memoizedParseAttrs = memoize(parseAttrs); const memoizedParseAttrs = memoize(parseAttrs);
function parseBool(v: string): boolean { function parseBool(v): boolean {
if (v === "true" || v === "1") return true; v = "" + v;
if (v === "false" || v === "0") return false; if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
return null; else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
return false;
else return null;
} }
function event(xml: Element): string[] { function event(xml: Element): string[] {
return xml.children return xml.children
.filter((n) => n.localName === "EventStruct") .filter(n => n.localName === "EventStruct")
.map((c) => c.children.find((n) => n.localName === "EventCode").text); .map(c => c.children.find(n => n.localName === "EventCode").text.trim());
} }
function parameterInfoList(xml: Element): [Path, boolean, boolean][] { function parameterInfoList(xml: Element): [string, boolean][] {
return xml.children return xml.children
.map<[Path, boolean, boolean]>((e) => { .filter(e => e.localName === "ParameterInfoStruct")
if (e.localName !== "ParameterInfoStruct") return null; .map<[string, boolean]>(e => {
let param: string, value: string; let param: string, value: string;
for (const c of e.children) { for (const c of e.children) {
switch (c.localName) { switch (c.localName) {
@@ -125,44 +115,30 @@ function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
let parsed: boolean = parseBool(value); let parsed: boolean = parseBool(value);
if (parsed == null) { if (parsed === null) {
warnings.push({ warnings.push({
message: "Missing or invalid XML node", message: "Invalid writable attribute",
element: "Writable", parameter: param
parameter: param,
}); });
parsed = false; parsed = false;
} }
try { return [param, parsed];
if (param && !param.endsWith(".")) });
return [Path.parse(param), false, parsed];
else return [Path.parse(param.slice(0, -1)), true, parsed];
} catch (err) {
warnings.push({
message: "Missing or invalid XML node",
element: "Name",
parameter: param,
});
return null;
}
})
.filter((e) => e != null);
} }
const getValueType = memoize((str: string) => { const getValueType = memoize((str: string) =>
const attrs = parseAttrs(str); parseAttrs(str)
for (const attr of attrs) if (attr.localName === "type") return attr.value; .find(s => s.localName === "type")
.value.trim()
return null; );
});
function parameterValueList( function parameterValueList(
xml: Element xml: Element
): [Path, string | number | boolean, string][] { ): [string, string | number | boolean, string][] {
return xml.children return xml.children
.map<[Path, string | number | boolean, string]>((e) => { .filter(e => e.localName === "ParameterValueStruct")
if (e.localName !== "ParameterValueStruct") return null; .map<[string, string | number | boolean, string]>(e => {
let valueElement: Element, param: string; let valueElement: Element, param: string;
for (const c of e.children) { for (const c of e.children) {
switch (c.localName) { switch (c.localName) {
@@ -175,25 +151,16 @@ function parameterValueList(
} }
} }
let valueType = getValueType(valueElement.attrs); const valueType = getValueType(valueElement.attrs);
if (!valueType) {
warnings.push({
message: "Missing or invalid XML node",
attribute: "type",
parameter: param,
});
valueType = "xsd:string";
}
const value = decodeEntities(valueElement.text); const value = decodeEntities(valueElement.text);
let parsed: string | number | boolean = value; let parsed: string | number | boolean = value;
if (valueType === "xsd:boolean") { if (valueType === "xsd:boolean") {
parsed = parseBool(value); parsed = parseBool(value);
if (parsed == null) { if (parsed === null) {
warnings.push({ warnings.push({
message: "Missing or invalid XML node", message: "Invalid value attribute",
element: "Value", parameter: param
parameter: param,
}); });
parsed = value; parsed = value;
} }
@@ -201,9 +168,8 @@ function parameterValueList(
parsed = parseInt(value); parsed = parseInt(value);
if (isNaN(parsed)) { if (isNaN(parsed)) {
warnings.push({ warnings.push({
message: "Missing or invalid XML node", message: "Invalid value attribute",
element: "Value", parameter: param
parameter: param,
}); });
parsed = value; parsed = value;
} }
@@ -211,31 +177,21 @@ function parameterValueList(
parsed = Date.parse(value); parsed = Date.parse(value);
if (isNaN(parsed)) { if (isNaN(parsed)) {
warnings.push({ warnings.push({
message: "Missing or invalid XML node", message: "Invalid value attribute",
element: "Value", parameter: param
parameter: param,
}); });
parsed = value; parsed = value;
} }
} }
try {
return [Path.parse(param), parsed, valueType]; return [param, parsed, valueType];
} catch (err) { });
warnings.push({
message: "Missing or invalid XML node",
element: "Name",
parameter: param,
});
return null;
}
})
.filter((e) => e != null);
} }
function parameterAttributeList(xml: Element): [Path, number, string[]][] { function parameterAttributeList(xml: Element): [string, number, string[]][] {
return xml.children return xml.children
.map<[Path, number, string[]]>((e) => { .filter(e => e.localName === "ParameterAttributeStruct")
if (e.localName !== "ParameterAttributeStruct") return null; .map<[string, number, string[]]>(e => {
let notificationElement: Element, let notificationElement: Element,
accessListElement: Element, accessListElement: Element,
param: string; param: string;
@@ -253,32 +209,22 @@ function parameterAttributeList(xml: Element): [Path, number, string[]][] {
} }
} }
let notification = parseInt(notificationElement.text); const notification = decodeEntities(notificationElement.text);
if (isNaN(notification)) { const notif = +notification;
if (notif !== parseInt(notification)) {
warnings.push({ warnings.push({
message: "Missing or invalid XML node", message: "Invalid notification attribute",
element: "Notification", parameter: param
parameter: param,
}); });
notification = 0;
} }
const accessList = accessListElement.children const accessList = accessListElement.children
.filter((c) => c.localName === "string") .filter(c => c.localName === "string")
.map((c) => decodeEntities(c.text)); .map(c => decodeEntities(c.text));
try { return [param, notif, accessList];
return [Path.parse(param), notification, accessList]; });
} catch (err) {
warnings.push({
message: "Missing or invalid XML node",
element: "Name",
parameter: param,
});
return null;
}
})
.filter((e) => e != null);
} }
function GetParameterNames(methodRequest): string { function GetParameterNames(methodRequest): string {
@@ -287,12 +233,12 @@ function GetParameterNames(methodRequest): string {
}</ParameterPath><NextLevel>${+methodRequest.nextLevel}</NextLevel></cwmp:GetParameterNames>`; }</ParameterPath><NextLevel>${+methodRequest.nextLevel}</NextLevel></cwmp:GetParameterNames>`;
} }
function GetParameterNamesResponse(xml): GetParameterNamesResponse { function GetParameterNamesResponse(xml): CpeGetResponse {
return { return {
name: "GetParameterNamesResponse", name: "GetParameterNamesResponse",
parameterList: parameterInfoList( parameterList: parameterInfoList(
xml.children.find((n) => n.localName === "ParameterList") xml.children.find(n => n.localName === "ParameterList")
), )
}; };
} }
@@ -300,16 +246,16 @@ function GetParameterValues(methodRequest): string {
return `<cwmp:GetParameterValues><ParameterNames soap-enc:arrayType="xsd:string[${ return `<cwmp:GetParameterValues><ParameterNames soap-enc:arrayType="xsd:string[${
methodRequest.parameterNames.length methodRequest.parameterNames.length
}]">${methodRequest.parameterNames }]">${methodRequest.parameterNames
.map((p) => `<string>${p}</string>`) .map(p => `<string>${p}</string>`)
.join("")}</ParameterNames></cwmp:GetParameterValues>`; .join("")}</ParameterNames></cwmp:GetParameterValues>`;
} }
function GetParameterValuesResponse(xml: Element): GetParameterValuesResponse { function GetParameterValuesResponse(xml: Element): CpeGetResponse {
return { return {
name: "GetParameterValuesResponse", name: "GetParameterValuesResponse",
parameterList: parameterValueList( parameterList: parameterValueList(
xml.children.find((n) => n.localName === "ParameterList") xml.children.find(n => n.localName === "ParameterList")
), )
}; };
} }
@@ -317,23 +263,21 @@ function GetParameterAttributes(methodRequest): string {
return `<cwmp:GetParameterAttributes><ParameterNames soap-enc:arrayType="xsd:string[${ return `<cwmp:GetParameterAttributes><ParameterNames soap-enc:arrayType="xsd:string[${
methodRequest.parameterNames.length methodRequest.parameterNames.length
}]">${methodRequest.parameterNames }]">${methodRequest.parameterNames
.map((p) => `<string>${p}</string>`) .map(p => `<string>${p}</string>`)
.join("")}</ParameterNames></cwmp:GetParameterAttributes>`; .join("")}</ParameterNames></cwmp:GetParameterAttributes>`;
} }
function GetParameterAttributesResponse( function GetParameterAttributesResponse(xml: Element): CpeGetResponse {
xml: Element
): GetParameterAttributesResponse {
return { return {
name: "GetParameterAttributesResponse", name: "GetParameterAttributesResponse",
parameterList: parameterAttributeList( parameterList: parameterAttributeList(
xml.children.find((n) => n.localName === "ParameterList") xml.children.find(n => n.localName === "ParameterList")
), )
}; };
} }
function SetParameterValues(methodRequest): string { function SetParameterValues(methodRequest): string {
const params = methodRequest.parameterList.map((p) => { const params = methodRequest.parameterList.map(p => {
let val = p[1]; let val = p[1];
if (p[2] === "xsd:dateTime" && typeof val === "number") { if (p[2] === "xsd:dateTime" && typeof val === "number") {
val = new Date(val).toISOString(); val = new Date(val).toISOString();
@@ -349,38 +293,21 @@ function SetParameterValues(methodRequest): string {
return `<cwmp:SetParameterValues><ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[${ return `<cwmp:SetParameterValues><ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[${
methodRequest.parameterList.length methodRequest.parameterList.length
}]">${params.join("")}</ParameterList><ParameterKey>${ }]">${params.join(
methodRequest.parameterKey || "" ""
}</ParameterKey></cwmp:SetParameterValues>`; )}</ParameterList><ParameterKey>${methodRequest.parameterKey ||
""}</ParameterKey></cwmp:SetParameterValues>`;
} }
function SetParameterValuesResponse(xml: Element): SetParameterValuesResponse { function SetParameterValuesResponse(xml: Element): CpeSetResponse {
let status: number;
for (const c of xml.children) {
switch (c.localName) {
case "Status":
status = parseInt(c.text);
break;
}
}
if (!(status >= 0)) {
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
return { return {
name: "SetParameterValuesResponse", name: "SetParameterValuesResponse",
status: status, status: parseInt(xml.children.find(n => n.localName === "Status").text)
}; };
} }
function SetParameterAttributes(methodRequest): string { function SetParameterAttributes(methodRequest): string {
const params = methodRequest.parameterList.map((p) => { const params = methodRequest.parameterList.map(p => {
return `<SetParameterAttributesStruct><Name>${ return `<SetParameterAttributesStruct><Name>${
p[0] p[0]
}</Name><NotificationChange>${ }</Name><NotificationChange>${
@@ -394,7 +321,7 @@ function SetParameterAttributes(methodRequest): string {
}]">${ }]">${
p[2] == null p[2] == null
? "" ? ""
: p[2].map((s) => `<string>${encodeEntities(s)}</string>`).join("") : p[2].map(s => `<string>${encodeEntities(s)}</string>`).join("")
}</AccessList></SetParameterAttributesStruct>`; }</AccessList></SetParameterAttributesStruct>`;
}); });
@@ -403,26 +330,25 @@ function SetParameterAttributes(methodRequest): string {
}]">${params.join("")}</ParameterList></cwmp:SetParameterAttributes>`; }]">${params.join("")}</ParameterList></cwmp:SetParameterAttributes>`;
} }
function SetParameterAttributesResponse(): SetParameterAttributesResponse { function SetParameterAttributesResponse(): CpeSetResponse {
return { return {
name: "SetParameterAttributesResponse", name: "SetParameterAttributesResponse"
}; };
} }
function AddObject(methodRequest): string { function AddObject(methodRequest): string {
return `<cwmp:AddObject><ObjectName>${ return `<cwmp:AddObject><ObjectName>${
methodRequest.objectName methodRequest.objectName
}</ObjectName><ParameterKey>${ }</ObjectName><ParameterKey>${methodRequest.parameterKey ||
methodRequest.parameterKey || "" ""}</ParameterKey></cwmp:AddObject>`;
}</ParameterKey></cwmp:AddObject>`;
} }
function AddObjectResponse(xml: Element): AddObjectResponse { function AddObjectResponse(xml: Element): CpeSetResponse {
let instanceNumber: string, status: number; let instanceNumber, status;
for (const c of xml.children) { for (const c of xml.children) {
switch (c.localName) { switch (c.localName) {
case "InstanceNumber": case "InstanceNumber":
instanceNumber = c.text; instanceNumber = parseInt(c.text);
break; break;
case "Status": case "Status":
status = parseInt(c.text); status = parseInt(c.text);
@@ -430,66 +356,35 @@ function AddObjectResponse(xml: Element): AddObjectResponse {
} }
} }
if (!/^[0-9]+$/.test(instanceNumber))
throw new Error("Missing or invalid instance number");
if (!(status >= 0)) {
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
return { return {
name: "AddObjectResponse", name: "AddObjectResponse",
instanceNumber: instanceNumber, instanceNumber: instanceNumber,
status: status, status: status
}; };
} }
function DeleteObject(methodRequest): string { function DeleteObject(methodRequest): string {
return `<cwmp:DeleteObject><ObjectName>${ return `<cwmp:DeleteObject><ObjectName>${
methodRequest.objectName methodRequest.objectName
}</ObjectName><ParameterKey>${ }</ObjectName><ParameterKey>${methodRequest.parameterKey ||
methodRequest.parameterKey || "" ""}</ParameterKey></cwmp:DeleteObject>`;
}</ParameterKey></cwmp:DeleteObject>`;
} }
function DeleteObjectResponse(xml: Element): DeleteObjectResponse { function DeleteObjectResponse(xml: Element): CpeSetResponse {
let status: number;
for (const c of xml.children) {
switch (c.localName) {
case "Status":
status = parseInt(c.text);
break;
}
}
if (!(status >= 0)) {
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
return { return {
name: "DeleteObjectResponse", name: "DeleteObjectResponse",
status: status, status: parseInt(xml.children.find(n => n.localName === "Status").text)
}; };
} }
function Reboot(methodRequest): string { function Reboot(methodRequest): string {
return `<cwmp:Reboot><CommandKey>${ return `<cwmp:Reboot><CommandKey>${methodRequest.commandKey ||
methodRequest.commandKey || "" ""}</CommandKey></cwmp:Reboot>`;
}</CommandKey></cwmp:Reboot>`;
} }
function RebootResponse(): RebootResponse { function RebootResponse(): CpeSetResponse {
return { return {
name: "RebootResponse", name: "RebootResponse"
}; };
} }
@@ -497,36 +392,33 @@ function FactoryReset(): string {
return "<cwmp:FactoryReset></cwmp:FactoryReset>"; return "<cwmp:FactoryReset></cwmp:FactoryReset>";
} }
function FactoryResetResponse(): FactoryResetResponse { function FactoryResetResponse(): CpeSetResponse {
return { return {
name: "FactoryResetResponse", name: "FactoryResetResponse"
}; };
} }
function Download(methodRequest): string { function Download(methodRequest): string {
return `<cwmp:Download><CommandKey>${ return `<cwmp:Download><CommandKey>${methodRequest.commandKey ||
methodRequest.commandKey || "" ""}</CommandKey><FileType>${methodRequest.fileType}</FileType><URL>${
}</CommandKey><FileType>${methodRequest.fileType}</FileType><URL>${
methodRequest.url methodRequest.url
}</URL><Username>${encodeEntities( }</URL><Username>${encodeEntities(
methodRequest.username || "" methodRequest.username || ""
)}</Username><Password>${encodeEntities( )}</Username><Password>${encodeEntities(
methodRequest.password || "" methodRequest.password || ""
)}</Password><FileSize>${ )}</Password><FileSize>${methodRequest.fileSize ||
methodRequest.fileSize || "0" "0"}</FileSize><TargetFileName>${encodeEntities(
}</FileSize><TargetFileName>${encodeEntities(
methodRequest.targetFileName || "" methodRequest.targetFileName || ""
)}</TargetFileName><DelaySeconds>${ )}</TargetFileName><DelaySeconds>${methodRequest.delaySeconds ||
methodRequest.delaySeconds || "0" "0"}</DelaySeconds><SuccessURL>${encodeEntities(
}</DelaySeconds><SuccessURL>${encodeEntities(
methodRequest.successUrl || "" methodRequest.successUrl || ""
)}</SuccessURL><FailureURL>${encodeEntities( )}</SuccessURL><FailureURL>${encodeEntities(
methodRequest.failureUrl || "" methodRequest.failureUrl || ""
)}</FailureURL></cwmp:Download>`; )}</FailureURL></cwmp:Download>`;
} }
function DownloadResponse(xml: Element): DownloadResponse { function DownloadResponse(xml: Element): CpeSetResponse {
let status: number, startTime: number, completeTime: number; let status, startTime, completeTime;
for (const c of xml.children) { for (const c of xml.children) {
switch (c.localName) { switch (c.localName) {
case "Status": case "Status":
@@ -541,46 +433,21 @@ function DownloadResponse(xml: Element): DownloadResponse {
} }
} }
if (!(status >= 0)) {
warnings.push({
message: "Missing or invalid XML node",
element: "Status",
});
status = 0;
}
if (startTime == null || isNaN(startTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "StartTime",
});
startTime = Date.parse("0001-01-01T00:00:00Z");
}
if (completeTime == null || isNaN(completeTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "CompleteTime",
});
completeTime = Date.parse("0001-01-01T00:00:00Z");
}
return { return {
name: "DownloadResponse", name: "DownloadResponse",
status: status, status: status,
startTime: startTime, startTime: startTime,
completeTime: completeTime, completeTime: completeTime
}; };
} }
function Inform(xml: Element): InformRequest { function Inform(xml: Element): InformRequest {
let retryCount: number, evnt: string[]; let retryCount, evnt, parameterList;
let parameterList: [Path, string | number | boolean, string][];
const deviceId = { const deviceId = {
Manufacturer: null, Manufacturer: null,
OUI: null, OUI: null,
ProductClass: null, ProductClass: null,
SerialNumber: null, SerialNumber: null
}; };
for (const c of xml.children) { for (const c of xml.children) {
@@ -603,36 +470,12 @@ function Inform(xml: Element): InformRequest {
} }
} }
if (!deviceId || !deviceId.SerialNumber || !deviceId.OUI)
throw new Error("Missing or invalid DeviceId element");
if (!parameterList) {
warnings.push({
message: "Missing or invalid XML node",
element: "ParameterList",
});
parameterList = [];
}
if (!evnt) {
warnings.push({ message: "Missing or invalid XML node", element: "Event" });
evnt = [];
}
if (retryCount == null || isNaN(retryCount)) {
warnings.push({
message: "Missing or invalid XML node",
element: "RetryCount",
});
retryCount = 0;
}
return { return {
name: "Inform", name: "Inform",
parameterList: parameterList, parameterList: parameterList,
deviceId: deviceId, deviceId: deviceId,
event: evnt, event: evnt,
retryCount: retryCount, retryCount: retryCount
}; };
} }
@@ -640,7 +483,7 @@ function InformResponse(): string {
return "<cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse>"; return "<cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse>";
} }
function GetRPCMethods(): GetRPCMethodsRequest { function GetRPCMethods(): AcsResponse {
return { name: "GetRPCMethods" }; return { name: "GetRPCMethods" };
} }
@@ -648,15 +491,12 @@ function GetRPCMethodsResponse(methodResponse): string {
return `<cwmp:GetRPCMethodsResponse><MethodList soap-enc:arrayType="xsd:string[${ return `<cwmp:GetRPCMethodsResponse><MethodList soap-enc:arrayType="xsd:string[${
methodResponse.methodList.length methodResponse.methodList.length
}]">${methodResponse.methodList }]">${methodResponse.methodList
.map((m) => `<string>${m}</string>`) .map(m => `<string>${m}</string>`)
.join("")}</MethodList></cwmp:GetRPCMethodsResponse>`; .join("")}</MethodList></cwmp:GetRPCMethodsResponse>`;
} }
function TransferComplete(xml: Element): TransferCompleteRequest { function TransferComplete(xml: Element): TransferCompleteRequest {
let commandKey: string, let commandKey, _faultStruct, startTime, completeTime;
_faultStruct: FaultStruct,
startTime: number,
completeTime: number;
for (const c of xml.children) { for (const c of xml.children) {
switch (c.localName) { switch (c.localName) {
case "CommandKey": case "CommandKey":
@@ -674,44 +514,12 @@ function TransferComplete(xml: Element): TransferCompleteRequest {
} }
} }
if (commandKey == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "CommandKey",
});
commandKey = "";
}
if (!_faultStruct) {
warnings.push({
message: "Missing or invalid XML node",
element: "FaultStruct",
});
_faultStruct = { faultCode: "0", faultString: "" };
}
if (startTime == null || isNaN(startTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "StartTime",
});
startTime = Date.parse("0001-01-01T00:00:00Z");
}
if (completeTime == null || isNaN(completeTime)) {
warnings.push({
message: "Missing or invalid XML node",
element: "CompleteTime",
});
completeTime = Date.parse("0001-01-01T00:00:00Z");
}
return { return {
name: "TransferComplete", name: "TransferComplete",
commandKey: commandKey, commandKey: commandKey,
faultStruct: _faultStruct, faultStruct: _faultStruct,
startTime: startTime, startTime: startTime,
completeTime: completeTime, completeTime: completeTime
}; };
} }
@@ -719,10 +527,10 @@ function TransferCompleteResponse(): string {
return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>"; return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>";
} }
function RequestDownload(xml: Element): RequestDownloadRequest { function RequestDownload(xml: Element): CpeRequest {
return { return {
name: "RequestDownload", name: "RequestDownload",
fileType: xml.children.find((n) => n.localName === "FileType").text, fileType: xml.children.find(n => n.localName === "FileType").text
}; };
} }
@@ -730,25 +538,8 @@ function RequestDownloadResponse(): string {
return "<cwmp:RequestDownloadResponse></cwmp:RequestDownloadResponse>"; return "<cwmp:RequestDownloadResponse></cwmp:RequestDownloadResponse>";
} }
function AcsFault(f: CpeFault): string {
return `<soap-env:Body:Fault><faultcode>${encodeEntities(
f.faultCode
)}</faultcode><faultstring>${encodeEntities(
f.faultString
)}</faultstring><detail><cwmp:Fault><FaultCode>${encodeEntities(
f.detail.faultCode
)}</FaultCode><FaultString>${encodeEntities(
f.detail.faultString
)}</FaultString></cwmp:Fault></detail></soap-env:Body:Fault>`;
}
function faultStruct(xml: Element): FaultStruct { function faultStruct(xml: Element): FaultStruct {
let faultCode: string, let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
faultString: string,
setParameterValuesFault: SpvFault[],
pn: string,
fc: string,
fs: string;
for (const c of xml.children) { for (const c of xml.children) {
switch (c.localName) { switch (c.localName) {
case "FaultCode": case "FaultCode":
@@ -759,7 +550,6 @@ function faultStruct(xml: Element): FaultStruct {
break; break;
case "SetParameterValuesFault": case "SetParameterValuesFault":
setParameterValuesFault = setParameterValuesFault || []; setParameterValuesFault = setParameterValuesFault || [];
pn = fc = fs = null;
for (const cc of c.children) { for (const cc of c.children) {
switch (cc.localName) { switch (cc.localName) {
case "ParameterName": case "ParameterName":
@@ -776,32 +566,16 @@ function faultStruct(xml: Element): FaultStruct {
setParameterValuesFault.push({ setParameterValuesFault.push({
parameterName: pn, parameterName: pn,
faultCode: fc, faultCode: fc,
faultString: fs, faultString: fs
}); });
} }
} }
if (faultCode == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "FaultCode",
});
faultCode = "";
}
if (faultString == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "FaultString",
});
faultString = "";
}
return { faultCode, faultString, setParameterValuesFault }; return { faultCode, faultString, setParameterValuesFault };
} }
function fault(xml: Element): CpeFault { function fault(xml: Element): CpeFault {
let faultCode: string, faultString: string, detail: FaultStruct; let faultCode, faultString, detail;
for (const c of xml.children) { for (const c of xml.children) {
switch (c.localName) { switch (c.localName) {
case "faultcode": case "faultcode":
@@ -811,46 +585,24 @@ function fault(xml: Element): CpeFault {
faultString = decodeEntities(c.text); faultString = decodeEntities(c.text);
break; break;
case "detail": case "detail":
detail = faultStruct(c.children.find((n) => n.localName === "Fault")); detail = faultStruct(c.children.find(n => n.localName === "Fault"));
break; break;
} }
} }
if (!detail) throw new Error("Missing detail element"); return { faultCode, faultString, detail };
if (faultCode == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "faultcode",
});
faultCode = "Client";
}
if (faultString == null) {
warnings.push({
message: "Missing or invalid XML node",
element: "faultstring",
});
faultString = "CWMP fault";
}
return { faultCode, faultString, detail } as CpeFault;
} }
export function request( export function request(body: string, cwmpVersion, warn): SoapMessage {
body: string,
warn: Record<string, unknown>[]
): SoapMessage {
warnings = warn; warnings = warn;
const rpc = { const rpc = {
id: null, id: null,
cwmpVersion: null, cwmpVersion: cwmpVersion,
sessionTimeout: null, sessionTimeout: null,
cpeRequest: null, cpeRequest: null,
cpeFault: null, cpeFault: null,
cpeResponse: null, cpeResponse: null
unknownMethod: null,
}; };
if (!body.length) return rpc; if (!body.length) return rpc;
@@ -889,7 +641,7 @@ export function request(
const methodElement = bodyElement.children[0]; const methodElement = bodyElement.children[0];
if (methodElement.localName === "Inform") { if (!rpc.cwmpVersion && methodElement.localName !== "Fault") {
let namespace, namespaceHref; let namespace, namespaceHref;
for (const e of [methodElement, bodyElement, envelope]) { for (const e of [methodElement, bodyElement, envelope]) {
namespace = namespace || e.namespace; namespace = namespace || e.namespace;
@@ -897,9 +649,9 @@ export function request(
const attrs = memoizedParseAttrs(e.attrs); const attrs = memoizedParseAttrs(e.attrs);
const attr = namespace const attr = namespace
? attrs.find( ? attrs.find(
(s) => s.namespace === "xmlns" && s.localName === namespace s => s.namespace === "xmlns" && s.localName === namespace
) )
: attrs.find((s) => s.name === "xmlns"); : attrs.find(s => s.name === "xmlns");
if (attr) namespaceHref = attr.value; if (attr) namespaceHref = attr.value;
} }
@@ -972,8 +724,7 @@ export function request(
rpc.cpeFault = fault(methodElement); rpc.cpeFault = fault(methodElement);
break; break;
default: default:
rpc.unknownMethod = methodElement.localName; throw new Error(`8000 Method not supported ${methodElement.localName}`);
break;
} }
return rpc; return rpc;
@@ -994,19 +745,13 @@ const namespacesAttrs = {
.join(" "), .join(" "),
"1.4": Object.entries(NAMESPACES["1.4"]) "1.4": Object.entries(NAMESPACES["1.4"])
.map(([k, v]) => `xmlns:${k}="${v}"`) .map(([k, v]) => `xmlns:${k}="${v}"`)
.join(" "), .join(" ")
}; };
export function response(rpc: { export function response(rpc): { code: number; headers: {}; data: string } {
id: string;
acsRequest?: AcsRequest;
acsResponse?: AcsResponse;
acsFault?: CpeFault;
cwmpVersion?: string;
}): { code: number; headers: Record<string, string>; data: string } {
const headers = { const headers = {
Server: SERVER_NAME, Server: SERVER_NAME,
SOAPServer: SERVER_NAME, SOAPServer: SERVER_NAME
}; };
if (!rpc) return { code: 204, headers: headers, data: "" }; if (!rpc) return { code: 204, headers: headers, data: "" };
@@ -1027,11 +772,7 @@ export function response(rpc: {
body = RequestDownloadResponse(); body = RequestDownloadResponse();
break; break;
default: default:
throw new Error( throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
`Unknown method response type ${
(rpc.acsResponse as AcsResponse).name
}`
);
} }
} else if (rpc.acsRequest) { } else if (rpc.acsRequest) {
switch (rpc.acsRequest.name) { switch (rpc.acsRequest.name) {
@@ -1066,12 +807,8 @@ export function response(rpc: {
body = Download(rpc.acsRequest); body = Download(rpc.acsRequest);
break; break;
default: default:
throw new Error( throw new Error(`Unknown method request ${rpc.acsRequest.name}`);
`Unknown method request ${(rpc.acsRequest as AcsRequest).name}`
);
} }
} else if (rpc.acsFault) {
body = AcsFault(rpc.acsFault);
} }
headers["Content-Type"] = 'text/xml; charset="utf-8"'; headers["Content-Type"] = 'text/xml; charset="utf-8"';
@@ -1082,6 +819,6 @@ export function response(rpc: {
namespacesAttrs[rpc.cwmpVersion] namespacesAttrs[rpc.cwmpVersion]
}><soap-env:Header><cwmp:ID soap-env:mustUnderstand="1">${ }><soap-env:Header><cwmp:ID soap-env:mustUnderstand="1">${
rpc.id rpc.id
}</cwmp:ID></soap-env:Header><soap-env:Body>${body}</soap-env:Body></soap-env:Envelope>`, }</cwmp:ID></soap-env:Header><soap-env:Body>${body}</soap-env:Body></soap-env:Envelope>`
}; };
} }
+70 -216
View File
@@ -22,7 +22,6 @@ import PathSet from "./common/path-set";
import VersionedMap from "./versioned-map"; import VersionedMap from "./versioned-map";
import InstanceSet from "./instance-set"; import InstanceSet from "./instance-set";
import { IncomingMessage, ServerResponse } from "http"; import { IncomingMessage, ServerResponse } from "http";
import { Script } from "vm";
export type Expression = string | number | boolean | null | any[]; export type Expression = string | number | boolean | null | any[];
@@ -150,6 +149,7 @@ export interface SessionContext {
faults?: { [channel: string]: SessionFault }; faults?: { [channel: string]: SessionFault };
retries?: { [channel: string]: number }; retries?: { [channel: string]: number };
cacheSnapshot?: string; cacheSnapshot?: string;
configContext?: { [name: string]: any };
httpResponse?: ServerResponse; httpResponse?: ServerResponse;
httpRequest?: IncomingMessage; httpRequest?: IncomingMessage;
faultsTouched?: { [channel: string]: boolean }; faultsTouched?: { [channel: string]: boolean };
@@ -157,13 +157,11 @@ export interface SessionContext {
new?: boolean; new?: boolean;
debug?: boolean; debug?: boolean;
state: number; state: number;
authState: number;
tasks?: Task[]; tasks?: Task[];
operations?: { [commandKey: string]: Operation }; operations?: { [commandKey: string]: Operation };
cacheUntil?: number; cacheUntil?: number;
syncState?: SyncState; syncState?: SyncState;
lastActivity?: number; lastActivity?: number;
extendLock?: number;
rpcRequest?: AcsRequest; rpcRequest?: AcsRequest;
operationsTouched?: { [commandKey: string]: 1 | 0 }; operationsTouched?: { [commandKey: string]: 1 | 0 };
provisionsRet?: any[]; provisionsRet?: any[];
@@ -171,7 +169,7 @@ export interface SessionContext {
} }
export interface Task { export interface Task {
_id?: string; _id: string;
name: string; name: string;
parameterNames?: string[]; parameterNames?: string[];
parameterValues?: [string, string | number | boolean, string?][]; parameterValues?: [string, string | number | boolean, string?][];
@@ -197,84 +195,47 @@ export interface Operation {
}; };
} }
export type AcsRequest = export interface AcsRequest {
| GetParameterNames name: string;
| GetParameterValues next?: string;
| GetParameterAttributes
| SetParameterValues
| SetParameterAttributes
| AddObject
| DeleteObject
| FactoryReset
| Reboot
| Download;
export interface GetParameterNames {
name: "GetParameterNames";
parameterPath: string;
nextLevel: boolean;
} }
export interface GetParameterValues { export interface GetAcsRequest extends AcsRequest {
name: "GetParameterValues"; name: "GetParameterNames" | "GetParameterValues" | "GetParameterAttributes";
parameterNames: string[]; objectName?: string;
parameterNames?: string[];
parameterPath?: string;
nextLevel?: boolean;
instanceValues?: { [name: string]: string };
} }
export interface GetParameterAttributes { export interface SetAcsRequest extends AcsRequest {
name: "GetParameterAttributes"; name:
parameterNames: string[]; | "SetParameterValues"
} | "SetParameterAttributes"
| "AddObject"
export interface SetParameterValues { | "DeleteObject"
name: "SetParameterValues"; | "FactoryReset"
parameterList: [string, boolean | number | string, string][]; | "Reboot"
parameterKey?: string; | "Download";
parameterList?:
| [string, string | number | boolean, string][]
| [string, number, string[]][];
instanceValues?: { [param: string]: string | number | boolean };
objectName?: string;
DATETIME_MILLISECONDS?: boolean; DATETIME_MILLISECONDS?: boolean;
BOOLEAN_LITERAL?: boolean; BOOLEAN_LITERAL?: boolean;
}
export interface SetParameterAttributes {
name: "SetParameterAttributes";
parameterList: [string, number, string[]][];
}
export interface AddObject {
name: "AddObject";
objectName: string;
parameterKey?: string;
instanceValues: Record<string, string>;
}
export interface DeleteObject {
name: "DeleteObject";
objectName: string;
parameterKey?: string;
}
export interface FactoryReset {
name: "FactoryReset";
commandKey?: string; commandKey?: string;
} instance?: string;
fileType?: string;
export interface Reboot {
name: "Reboot";
commandKey?: string;
}
export interface Download {
name: "Download";
commandKey: string;
instance: string;
fileType: string;
fileName?: string;
url?: string;
username?: string;
password?: string;
fileSize?: number; fileSize?: number;
url?: string;
fileName?: string;
targetFileName?: string; targetFileName?: string;
delaySecods?: number; }
successUrl?: string;
failureUrl?: string; export interface CpeResponse {
name: string;
} }
export interface SpvFault { export interface SpvFault {
@@ -290,80 +251,43 @@ export interface FaultStruct {
} }
export interface CpeFault { export interface CpeFault {
faultCode: "Client" | "Server"; faultCode: string;
faultString: "CWMP fault"; faultString: string;
detail?: FaultStruct; detail?: FaultStruct;
} }
export type CpeResponse = export interface CpeGetResponse extends CpeResponse {
| GetParameterNamesResponse name:
| GetParameterValuesResponse | "GetParameterNamesResponse"
| GetParameterAttributesResponse | "GetParameterValuesResponse"
| SetParameterValuesResponse | "GetParameterAttributesResponse";
| SetParameterAttributesResponse parameterList?:
| AddObjectResponse | [string, boolean][]
| DeleteObjectResponse | [string, string | number | boolean, string][]
| RebootResponse | [string, number, string[]][];
| FactoryResetResponse
| DownloadResponse;
export interface GetParameterNamesResponse {
name: "GetParameterNamesResponse";
parameterList: [Path, boolean, boolean][];
} }
export interface GetParameterValuesResponse { export interface CpeSetResponse extends CpeResponse {
name: "GetParameterValuesResponse"; name:
parameterList: [Path, string | number | boolean, string][]; | "SetParameterValuesResponse"
} | "SetParameterAttributesResponse"
| "AddObjectResponse"
export interface GetParameterAttributesResponse { | "DeleteObjectResponse"
name: "GetParameterAttributesResponse"; | "RebootResponse"
parameterList: [Path, number, string[]][]; | "FactoryResetResponse"
} | "DownloadResponse";
status?: number;
export interface SetParameterValuesResponse { instanceNumber?: string;
name: "SetParameterValuesResponse";
status: number;
}
export interface SetParameterAttributesResponse {
name: "SetParameterAttributesResponse";
}
export interface AddObjectResponse {
name: "AddObjectResponse";
instanceNumber: string;
status: number;
}
export interface DeleteObjectResponse {
name: "DeleteObjectResponse";
status: number;
}
export interface RebootResponse {
name: "RebootResponse";
}
export interface FactoryResetResponse {
name: "FactoryResetResponse";
}
export interface DownloadResponse {
name: "DownloadResponse";
status: number;
startTime?: number; startTime?: number;
completeTime?: number; completeTime?: number;
} }
export type CpeRequest = export interface CpeRequest {
| InformRequest name: string;
| TransferCompleteRequest fileType?: string;
| GetRPCMethodsRequest }
| RequestDownloadRequest;
export interface InformRequest { export interface InformRequest extends CpeRequest {
name: "Inform"; name: "Inform";
deviceId: { deviceId: {
Manufacturer: string; Manufacturer: string;
@@ -373,10 +297,10 @@ export interface InformRequest {
}; };
event: string[]; event: string[];
retryCount: number; retryCount: number;
parameterList: [Path, string | number | boolean, string][]; parameterList: [string, string | number | boolean, string][];
} }
export interface TransferCompleteRequest { export interface TransferCompleteRequest extends CpeRequest {
name: "TransferComplete"; name: "TransferComplete";
commandKey?: string; commandKey?: string;
faultStruct?: FaultStruct; faultStruct?: FaultStruct;
@@ -384,36 +308,10 @@ export interface TransferCompleteRequest {
completeTime?: number; completeTime?: number;
} }
export interface GetRPCMethodsRequest { export interface AcsResponse {
name: "GetRPCMethods"; name: string;
} commandKey?: string;
faultStruct?: FaultStruct;
export interface RequestDownloadRequest {
name: "RequestDownload";
fileType: string;
}
export type AcsResponse =
| InformResponse
| GetRPCMethodsResponse
| TransferCompleteResponse
| RequestDownloadResponse;
export interface InformResponse {
name: "InformResponse";
}
export interface GetRPCMethodsResponse {
name: "GetRPCMethodsResponse";
methodList: string[];
}
export interface TransferCompleteResponse {
name: "TransferCompleteResponse";
}
export interface RequestDownloadResponse {
name: "RequestDownloadResponse";
} }
export interface QueryOptions { export interface QueryOptions {
@@ -428,7 +326,7 @@ export interface QueryOptions {
export interface Declaration { export interface Declaration {
path: Path; path: Path;
pathGet: number; pathGet: number;
pathSet?: number | [number, number]; pathSet?: [number, number];
attrGet?: { attrGet?: {
object?: number; object?: number;
writable?: number; writable?: number;
@@ -464,36 +362,8 @@ export interface Preset {
channel: string; channel: string;
schedule?: { md5: string; duration: number; schedule: any }; schedule?: { md5: string; duration: number; schedule: any };
events?: { [event: string]: boolean }; events?: { [event: string]: boolean };
precondition?: Expression; precondition?: {};
provisions: [string, ...Expression[]][]; provisions: string[][];
}
export interface Provisions {
[name: string]: { md5: string; script: Script };
}
export interface VirtualParameters {
[name: string]: { md5: string; script: Script };
}
export interface Files {
[name: string]: { length: number };
}
export interface Users {
[name: string]: { password: string; salt: string; roles: string[] };
}
export interface Permissions {
[role: string]: {
[access: number]: {
[resource: string]: {
access: number;
filter: Expression;
validate?: Expression;
};
};
};
} }
export type PermissionSet = { export type PermissionSet = {
@@ -504,21 +374,6 @@ export type PermissionSet = {
}; };
}[]; }[];
export interface Config {
[name: string]: Expression;
}
export interface UiConfig {
filters: Record<string, unknown>;
device: Record<string, unknown>;
index: Record<string, unknown>;
overview: {
charts?: Record<string, unknown>;
groups?: Record<string, unknown>;
};
pageSize?: Expression;
}
export interface SoapMessage { export interface SoapMessage {
id: string; id: string;
cwmpVersion: string; cwmpVersion: string;
@@ -526,7 +381,6 @@ export interface SoapMessage {
cpeRequest?: CpeRequest; cpeRequest?: CpeRequest;
cpeFault?: CpeFault; cpeFault?: CpeFault;
cpeResponse?: CpeResponse; cpeResponse?: CpeResponse;
unknownMethod?: string;
} }
export interface ScriptResult { export interface ScriptResult {
+24 -50
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 { constants } from "zlib"; import { Z_SYNC_FLUSH } from "zlib";
import Koa from "koa"; import Koa from "koa";
import Router from "koa-router"; import Router from "koa-router";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
@@ -34,7 +34,6 @@ import { PermissionSet } from "./types";
import { authLocal } from "./ui/api-functions"; import { authLocal } from "./ui/api-functions";
import * as init from "./init"; import * as init from "./init";
import { version as VERSION } from "../package.json"; import { version as VERSION } from "../package.json";
import memoize from "./common/memoize";
declare module "koa" { declare module "koa" {
interface Request { interface Request {
@@ -48,23 +47,8 @@ const router = new Router();
const JWT_SECRET = "" + config.get("UI_JWT_SECRET"); const JWT_SECRET = "" + config.get("UI_JWT_SECRET");
const JWT_COOKIE = "genieacs-ui-jwt"; const JWT_COOKIE = "genieacs-ui-jwt";
const getAuthorizer = memoize( koa.on("error", async err => {
(snapshot: string, rolesStr: string): Authorizer => { throw err;
const roles: string[] = JSON.parse(rolesStr);
const allPermissions = localCache.getPermissions(snapshot);
const permissionSets: PermissionSet[] = roles.map((r) =>
Object.values(allPermissions[r] || {})
);
return new Authorizer(permissionSets);
}
);
koa.on("error", async (err, ctx) => {
setTimeout(() => {
// Ignored errors resulting from aborted requests
if (ctx?.req.aborted) return;
throw err;
});
}); });
koa.use(async (ctx, next) => { koa.use(async (ctx, next) => {
@@ -88,14 +72,16 @@ koa.use(
} }
return true; return true;
}, }
}) })
); );
koa.use(async (ctx, next) => { koa.use(async (ctx, next) => {
let roles: string[] = []; const permissionSets: PermissionSet[] = [];
if (ctx.state.user && ctx.state.user.username) {
const allPermissions = localCache.getPermissions(ctx.state.configSnapshot);
if (ctx.state.user?.username) {
let user; let user;
if (ctx.state.user.authMethod === "local") { if (ctx.state.user.authMethod === "local") {
user = localCache.getUsers(ctx.state.configSnapshot)[ user = localCache.getUsers(ctx.state.configSnapshot)[
@@ -104,18 +90,17 @@ koa.use(async (ctx, next) => {
} else { } else {
throw new Error("Invalid auth method"); throw new Error("Invalid auth method");
} }
roles = user.roles || [];
for (const role of (user.roles || []).sort())
permissionSets.push(Object.values(allPermissions[role] || {}));
} }
ctx.state.authorizer = getAuthorizer( ctx.state.authorizer = new Authorizer(permissionSets);
ctx.state.configSnapshot,
JSON.stringify(roles)
);
return next(); return next();
}); });
router.post("/login", async (ctx) => { router.post("/login", async ctx => {
if (!JWT_SECRET) { if (!JWT_SECRET) {
ctx.status = 500; ctx.status = 500;
ctx.body = "UI_JWT_SECRET is not set"; ctx.body = "UI_JWT_SECRET is not set";
@@ -130,7 +115,7 @@ router.post("/login", async (ctx) => {
message: "Log in", message: "Log in",
context: ctx, context: ctx,
username: username, username: username,
method: null, method: null
}; };
function success(authMethod): void { function success(authMethod): void {
@@ -154,13 +139,13 @@ router.post("/login", async (ctx) => {
failure(); failure();
}); });
router.post("/logout", async (ctx) => { router.post("/logout", async ctx => {
ctx.cookies.set(JWT_COOKIE); // Delete cookie ctx.cookies.set(JWT_COOKIE); // Delete cookie
ctx.body = ""; ctx.body = "";
logger.accessInfo({ logger.accessInfo({
message: "Log out", message: "Log out",
context: ctx, context: ctx
}); });
}); });
@@ -174,11 +159,11 @@ koa.use(async (ctx, next) => {
koa.use(koaBodyParser()); koa.use(koaBodyParser());
router.use("/api", api.routes(), api.allowedMethods()); router.use("/api", api.routes(), api.allowedMethods());
router.get("/status", (ctx) => { router.get("/status", ctx => {
ctx.body = "OK"; ctx.body = "OK";
}); });
router.get("/init", async (ctx) => { router.get("/init", async ctx => {
const status = await init.getStatus(); const status = await init.getStatus();
if (Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) { if (Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) {
if (!ctx.state.authorizer.hasAccess("users", 3)) status["users"] = false; if (!ctx.state.authorizer.hasAccess("users", 3)) status["users"] = false;
@@ -199,7 +184,7 @@ router.get("/init", async (ctx) => {
ctx.body = status; ctx.body = status;
}); });
router.post("/init", async (ctx) => { router.post("/init", async ctx => {
const status = ctx.request.body; const status = ctx.request.body;
if (Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) { if (Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) {
if (!ctx.state.authorizer.hasAccess("users", 3)) status["users"] = false; if (!ctx.state.authorizer.hasAccess("users", 3)) status["users"] = false;
@@ -220,9 +205,8 @@ router.post("/init", async (ctx) => {
ctx.body = ""; ctx.body = "";
}); });
router.get("/", async (ctx) => { router.get("/", async ctx => {
const permissionSets: PermissionSet[] = const permissionSets = ctx.state.authorizer.getPermissionSets();
ctx.state.authorizer.getPermissionSets();
let wizard = ""; let wizard = "";
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
@@ -239,7 +223,7 @@ router.get("/", async (ctx) => {
<noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript> <noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript>
<script> <script>
window.clientConfig = ${JSON.stringify({ window.clientConfig = ${JSON.stringify({
ui: localCache.getUiConfig(ctx.state.configSnapshot), ui: localCache.getUiConfig(ctx.state.configSnapshot)
})}; })};
window.configSnapshot = ${JSON.stringify(ctx.state.configSnapshot)}; window.configSnapshot = ${JSON.stringify(ctx.state.configSnapshot)};
window.genieacsVersion = ${JSON.stringify(VERSION)}; window.genieacsVersion = ${JSON.stringify(VERSION)};
@@ -248,7 +232,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>
`; `;
@@ -256,17 +240,7 @@ router.get("/", async (ctx) => {
koa.use( koa.use(
koaCompress({ koaCompress({
gzip: { flush: Z_SYNC_FLUSH
flush: constants.Z_SYNC_FLUSH,
},
deflate: {
flush: constants.Z_SYNC_FLUSH,
},
br: {
params: {
[constants.BROTLI_PARAM_QUALITY]: 5,
},
},
}) })
); );
+37 -65
View File
@@ -17,19 +17,17 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ObjectId } from "mongodb"; import { ObjectID } from "mongodb";
import * as db from "./db"; import * as db from "./db";
import { del } from "../cache"; import { del } from "../cache";
import { getUsers } from "../local-cache"; import { getUsers } from "../local-cache";
import { hashPassword } from "../auth"; import { hashPassword } from "../auth";
import { import {
insertTasks, insertTasks,
awaitSessionEnd, watchTask,
awaitSessionStart,
connectionRequest, connectionRequest,
deleteDevice, deleteDevice
} from "../api-functions"; } from "../api-functions";
import { Task } from "../types";
async function deleteFault(id): Promise<void> { async function deleteFault(id): Promise<void> {
const deviceId = id.split(":", 1)[0]; const deviceId = id.split(":", 1)[0];
@@ -38,17 +36,14 @@ async function deleteFault(id): Promise<void> {
await Promise.all([ await Promise.all([
db.deleteFault(id), db.deleteFault(id),
channel.startsWith("task_") channel.startsWith("task_")
? db.deleteTask(new ObjectId(channel.slice(5))) ? db.deleteTask(new ObjectID(channel.slice(5)))
: null, : null
]); ]);
await del(`${deviceId}_tasks_faults_operations`); await del(`${deviceId}_tasks_faults_operations`);
} }
export async function deleteResource( export async function deleteResource(resource, id): Promise<void> {
resource: string,
id: string
): Promise<void> {
switch (resource) { switch (resource) {
case "devices": case "devices":
await deleteDevice(id); await deleteDevice(id);
@@ -91,59 +86,35 @@ export async function deleteResource(
} }
export async function postTasks( export async function postTasks(
deviceId: string, deviceId,
tasks: Task[], tasks,
timeout: number, timeout
device: Record<string, { value?: [boolean | number | string, string] }>
): Promise<{ connectionRequest: string; tasks: any[] }> { ): Promise<{ connectionRequest: string; tasks: any[] }> {
for (const task of tasks) { for (const task of tasks) {
delete task._id; delete task._id;
task["device"] = deviceId; task.device = deviceId;
} }
tasks = await insertTasks(tasks); tasks = await insertTasks(tasks);
const statuses = tasks.map((t) => { const statuses = tasks.map(t => {
return { _id: t._id, status: "pending" }; return { _id: t._id, status: "pending" };
}); });
const lastInform = Date.now();
const notInSession = await awaitSessionEnd(deviceId, 30000);
if (!notInSession) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return {
connectionRequest: "Session took too long to complete",
tasks: statuses,
};
}
await del(`${deviceId}_tasks_faults_operations`); await del(`${deviceId}_tasks_faults_operations`);
const status = await connectionRequest(deviceId, device); try {
await connectionRequest(deviceId);
if (status) { } catch (err) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return { return {
connectionRequest: status, connectionRequest: err.message,
tasks: statuses, tasks: statuses
}; };
} }
const sessionStarted = await awaitSessionStart(deviceId, lastInform, timeout); const sample = tasks[tasks.length - 1];
if (!sessionStarted) {
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return {
connectionRequest: "No contact from CPE",
tasks: statuses,
};
}
const sessionEnded = await awaitSessionEnd(deviceId, 120000); // Waiting for session to finish or timeout
if (!sessionEnded) { await watchTask(deviceId, sample._id, timeout);
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return {
connectionRequest: "Session took too long to complete",
tasks: statuses,
};
}
const promises = []; const promises = [];
for (const s of statuses) { for (const s of statuses) {
@@ -159,20 +130,25 @@ export async function postTasks(
r.status = "done"; r.status = "done";
} else if (res[i * 2 + 1].length === 1) { } else if (res[i * 2 + 1].length === 1) {
r.status = "fault"; r.status = "fault";
r["fault"] = res[i * 2 + 1][0]; r.fault = res[i * 2 + 1][0];
} }
db.deleteTask(r._id);
} }
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
return { connectionRequest: "OK", tasks: statuses }; return { connectionRequest: "OK", tasks: statuses };
} }
export async function putResource( interface PingResponse {
resource: string, packetsTransmitted: number;
id: string, packetsReceived: number;
data: Record<string, unknown> packetLoss: string;
): Promise<void> { min: number;
avg: number;
max: number;
mdev: number;
}
export async function putResource(resource, id, data): Promise<void> {
if (resource === "presets") { if (resource === "presets") {
await db.putPreset(id, data); await db.putPreset(id, data);
} else if (resource === "provisions") { } else if (resource === "provisions") {
@@ -184,25 +160,21 @@ export async function putResource(
} else if (resource === "permissions") { } else if (resource === "permissions") {
await db.putPermission(id, data); await db.putPermission(id, data);
} else if (resource === "users") { } else if (resource === "users") {
delete data["password"]; delete data.password;
delete data["salt"]; delete data.salt;
await db.putUser(id, data); await db.putUser(id, data);
} }
await del("presets_hash"); await del("presets_hash");
} }
export function authLocal( export function authLocal(snapshot, username, password): Promise<boolean> {
snapshot: string,
username: string,
password: string
): Promise<boolean> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const users = getUsers(snapshot); const users = getUsers(snapshot);
const user = users[username]; const user = users[username];
if (!user?.password) return void resolve(null); if (!user || !user.password) return void resolve(null);
hashPassword(password, user.salt) hashPassword(password, user.salt)
.then((hash) => { .then(hash => {
if (hash === user.password) resolve(true); if (hash === user.password) resolve(true);
else resolve(false); else resolve(false);
}) })
+96 -192
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 { PassThrough } from "stream"; import * as stream from "stream";
import Router from "koa-router"; import Router from "koa-router";
import * as db from "./db"; import * as db from "./db";
import * as apiFunctions from "./api-functions"; import * as apiFunctions from "./api-functions";
@@ -30,8 +30,6 @@ import { generateSalt, hashPassword } from "../auth";
import { del } from "../cache"; import { del } from "../cache";
import Authorizer from "../common/authorizer"; import Authorizer from "../common/authorizer";
import { ping } from "../ping"; import { ping } from "../ping";
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;
@@ -54,7 +52,7 @@ const RESOURCE_IDS = {
permissions: "_id", permissions: "_id",
users: "_id", users: "_id",
faults: "_id", faults: "_id",
tasks: "_id", tasks: "_id"
}; };
const resources = { const resources = {
@@ -67,30 +65,26 @@ const resources = {
permissions: 0 | RESOURCE_DELETE | RESOURCE_PUT, permissions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
users: 0 | RESOURCE_DELETE | RESOURCE_PUT, users: 0 | RESOURCE_DELETE | RESOURCE_PUT,
faults: 0 | RESOURCE_DELETE, faults: 0 | RESOURCE_DELETE,
tasks: 0, tasks: 0
}; };
function singleParam(p: string | string[]): string { router.get(`/devices/:id.csv`, async ctx => {
return Array.isArray(p) ? p[p.length - 1] : p;
}
router.get(`/devices/:id.csv`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const log = { const log = {
message: "Query device (CSV)", message: "Query device (CSV)",
context: ctx, context: ctx,
id: ctx.params.id, id: ctx.params.id
}; };
const filter = and(authorizer.getFilter("devices", 2), [ const filter = and(authorizer.getFilter("devices", 2), [
"=", "=",
["PARAM", RESOURCE_IDS.devices], ["PARAM", RESOURCE_IDS.devices],
ctx.params.id, ctx.params.id
]); ]);
if (!authorizer.hasAccess("devices", 2)) { if (!authorizer.hasAccess("devices", 2)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void (ctx.status = 404);
} }
const res = await db.query("devices", filter); const res = await db.query("devices", filter);
@@ -103,67 +97,57 @@ router.get(`/devices/:id.csv`, async (ctx) => {
.replace(/[:.]/g, "")}.csv` .replace(/[:.]/g, "")}.csv`
); );
ctx.body = new PassThrough(); ctx.body = new stream.PassThrough();
ctx.body.write( ctx.body.write(
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n" "Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n"
); );
for (const k of Object.keys(res[0]).sort()) { for (const k of Object.keys(res[0]).sort()) {
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");
} }
ctx.body.end(); ctx.body.end();
logger.accessInfo(log); logger.accessInfo(log);
}); });
for (const [resource, flags] of Object.entries(resources)) { for (const [resource, flags] of Object.entries(resources)) {
router.head(`/${resource}`, async (ctx) => { router.head(`/${resource}`, async (ctx, next) => {
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}`,
context: ctx, context: ctx,
filter: ctx.request.query.filter, filter: ctx.request.query.filter,
count: null, count: null
}; };
if (!authorizer.hasAccess(resource, 1)) { if (!authorizer.hasAccess(resource, 1)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
// Exclude temporary tasks and faults // Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") { if (resource === "tasks" || resource === "faults") {
filter = and(filter, [ filter = and(filter, [
"OR", "NOT",
[">=", ["PARAM", "expiry"], Date.now() + 60000], ["<", ["PARAM", "expiry"], Date.now() + 60000]
["IS NULL", ["PARAM", "expiry"]],
]); ]);
} }
@@ -176,18 +160,18 @@ for (const [resource, flags] of Object.entries(resources)) {
logger.accessInfo(log); logger.accessInfo(log);
}); });
router.get(`/${resource}`, async (ctx) => { router.get(`/${resource}`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
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 }), {});
} }
@@ -199,29 +183,28 @@ for (const [resource, flags] of Object.entries(resources)) {
limit: options.limit, limit: options.limit,
skip: options.skip, skip: options.skip,
sort: options.sort, sort: options.sort,
projection: options.projection, projection: options.projection
}; };
if (!authorizer.hasAccess(resource, 2)) { if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
// Exclude temporary tasks and faults // Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") { if (resource === "tasks" || resource === "faults") {
filter = and(filter, [ filter = and(filter, [
"OR", "NOT",
[">=", ["PARAM", "expiry"], Date.now() + 60000], ["<", ["PARAM", "expiry"], Date.now() + 60000]
["IS NULL", ["PARAM", "expiry"]],
]); ]);
} }
ctx.body = new PassThrough(); ctx.body = new stream.PassThrough();
ctx.type = "application/json"; ctx.type = "application/json";
let c = 0; let c = 0;
ctx.body.write("[\n"); ctx.body.write("[\n");
await db.query(resource, filter, options, (obj) => { await db.query(resource, filter, options, obj => {
ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n"); ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n");
}); });
ctx.body.end("]"); ctx.body.end("]");
@@ -230,16 +213,16 @@ for (const [resource, flags] of Object.entries(resources)) {
}); });
// CSV download // CSV download
router.get(`/${resource}.csv`, async (ctx) => { router.get(`/${resource}.csv`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
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)`,
@@ -247,17 +230,15 @@ for (const [resource, flags] of Object.entries(resources)) {
filter: ctx.request.query.filter, filter: ctx.request.query.filter,
limit: options.limit, limit: options.limit,
skip: options.skip, skip: options.skip,
sort: options.sort, sort: options.sort
}; };
if (!authorizer.hasAccess(resource, 2)) { if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
const columns: Record<string, Expression> = JSON.parse( const columns = JSON.parse(ctx.request.query.columns);
singleParam(ctx.request.query.columns)
);
const now = Date.now(); const now = Date.now();
for (const [k, v] of Object.entries(columns)) { for (const [k, v] of Object.entries(columns)) {
@@ -270,51 +251,38 @@ for (const [resource, flags] of Object.entries(resources)) {
// Exclude temporary tasks and faults // Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") { if (resource === "tasks" || resource === "faults") {
filter = and(filter, [ filter = and(filter, [
"OR", "NOT",
[">=", ["PARAM", "expiry"], Date.now() + 60000], ["<", ["PARAM", "expiry"], Date.now() + 60000]
["IS NULL", ["PARAM", "expiry"]],
]); ]);
} }
ctx.body = new PassThrough(); ctx.body = new stream.PassThrough();
ctx.type = "text/csv"; ctx.type = "text/csv";
ctx.attachment( ctx.attachment(
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv` `${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`
); );
ctx.body.write( ctx.body.write(
Object.keys(columns).map((k) => `"${k.replace(/"/, '""')}"`) + "\n" Object.keys(columns).map(k => `"${k.replace(/"/, '""')}"`) + "\n"
); );
await db.query(resource, filter, options, (obj) => { await db.query(resource, filter, options, obj => {
const arr = Object.values(columns).map((exp) => { const arr = Object.values(columns).map(exp => {
const v = evaluate(exp, obj, null, (e) => { const v = evaluate(exp, obj, null, e => {
if (Array.isArray(e)) { if (Array.isArray(e)) {
if (e[0] === "PARAM") { if (e[0] === "PARAM") {
if (resource === "devices") { if (resource === "devices") {
if (e[1] === "Tags") { if (e[1] === "Tags") {
const tags = []; const tags = [];
for (const p in obj) for (const p in obj)
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5))); if (p.startsWith("Tags.")) tags.push(p.slice(5));
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();
} }
} }
} }
@@ -333,78 +301,78 @@ for (const [resource, flags] of Object.entries(resources)) {
logger.accessInfo(log); logger.accessInfo(log);
}); });
router.head(`/${resource}/:id`, async (ctx) => { router.head(`/${resource}/:id`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const log = { const log = {
message: `Count ${resource}`, message: `Count ${resource}`,
context: ctx, context: ctx,
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`, filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
}; };
const filter = and(authorizer.getFilter(resource, 2), [ const filter = and(authorizer.getFilter(resource, 2), [
"=", "=",
["PARAM", RESOURCE_IDS[resource]], ["PARAM", RESOURCE_IDS[resource]],
ctx.params.id, ctx.params.id
]); ]);
if (!authorizer.hasAccess(resource, 2)) { if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
const res = await db.query(resource, filter); const res = await db.query(resource, filter);
if (!res.length) return void (ctx.status = 404); if (!res.length) return void next();
logger.accessInfo(log); logger.accessInfo(log);
ctx.body = ""; ctx.body = "";
}); });
router.get(`/${resource}/:id`, async (ctx) => { router.get(`/${resource}/:id`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const log = { const log = {
message: `Query ${resource}`, message: `Query ${resource}`,
context: ctx, context: ctx,
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`, filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
}; };
const filter = and(authorizer.getFilter(resource, 2), [ const filter = and(authorizer.getFilter(resource, 2), [
"=", "=",
["PARAM", RESOURCE_IDS[resource]], ["PARAM", RESOURCE_IDS[resource]],
ctx.params.id, ctx.params.id
]); ]);
if (!authorizer.hasAccess(resource, 2)) { if (!authorizer.hasAccess(resource, 2)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
const res = await db.query(resource, filter); const res = await db.query(resource, filter);
if (!res.length) return void (ctx.status = 404); if (!res.length) return void next();
logger.accessInfo(log); logger.accessInfo(log);
ctx.body = res[0]; ctx.body = res[0];
}); });
if (flags & RESOURCE_DELETE) { if (flags & RESOURCE_DELETE) {
router.delete(`/${resource}/:id`, async (ctx) => { router.delete(`/${resource}/:id`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const log = { const log = {
message: `Delete ${resource}`, message: `Delete ${resource}`,
context: ctx, context: ctx,
id: ctx.params.id, id: ctx.params.id
}; };
const filter = and(authorizer.getFilter(resource, 3), [ const filter = and(authorizer.getFilter(resource, 3), [
"=", "=",
["PARAM", RESOURCE_IDS[resource]], ["PARAM", RESOURCE_IDS[resource]],
ctx.params.id, ctx.params.id
]); ]);
if (!authorizer.hasAccess(resource, 3)) { if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
const res = await db.query(resource, filter); const res = await db.query(resource, filter);
if (!res.length) return void (ctx.status = 404); if (!res.length) return void next();
const validate = authorizer.getValidator(resource, res[0]); const validate = authorizer.getValidator(resource, res[0]);
if (!validate("delete")) { if (!validate("delete")) {
@@ -421,19 +389,19 @@ for (const [resource, flags] of Object.entries(resources)) {
} }
if (flags & RESOURCE_PUT) { if (flags & RESOURCE_PUT) {
router.put(`/${resource}/:id`, async (ctx) => { router.put(`/${resource}/:id`, async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const id = ctx.params.id; const id = ctx.params.id;
const log = { const log = {
message: `Put ${resource}`, message: `Put ${resource}`,
context: ctx, context: ctx,
id: id, id: id
}; };
if (!authorizer.hasAccess(resource, 3)) { if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
const obj = ctx.request.body; const obj = ctx.request.body;
@@ -460,37 +428,7 @@ for (const [resource, flags] of Object.entries(resources)) {
} }
} }
router.get("/blob/files/:id", async (ctx) => { router.put("/files/:id", async (ctx, next) => {
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) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const resource = "files"; const resource = "files";
const id = ctx.params.id; const id = ctx.params.id;
@@ -499,20 +437,19 @@ router.put("/files/:id", async (ctx) => {
message: `Upload ${resource}`, message: `Upload ${resource}`,
context: ctx, context: ctx,
id: id, id: id,
metadata: null, metadata: null
}; };
if (!authorizer.hasAccess(resource, 3)) { if (!authorizer.hasAccess(resource, 3)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
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);
@@ -521,12 +458,6 @@ router.put("/files/:id", async (ctx) => {
return void (ctx.status = 403); return void (ctx.status = 403);
} }
try {
await db.deleteFile(id);
} catch (err) {
// File doesn't exist, ignore
}
await db.putFile(id, metadata, ctx.req); await db.putFile(id, metadata, ctx.req);
log.metadata = metadata; log.metadata = metadata;
logger.accessInfo(log); logger.accessInfo(log);
@@ -534,29 +465,28 @@ router.put("/files/:id", async (ctx) => {
ctx.body = ""; ctx.body = "";
}); });
router.post("/devices/:id/tasks", async (ctx) => { router.post("/devices/:id/tasks", async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const log = { const log = {
message: "Commit tasks", message: "Commit tasks",
context: ctx, context: ctx,
deviceId: ctx.params.id, deviceId: ctx.params.id,
tasks: null, tasks: null
}; };
const filter = and(authorizer.getFilter("devices", 3), [ const filter = and(authorizer.getFilter("devices", 3), [
"=", "=",
["PARAM", "DeviceID.ID"], ["PARAM", "DeviceID.ID"],
ctx.params.id, ctx.params.id
]); ]);
if (!authorizer.hasAccess("devices", 3)) { if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
const devices = await db.query("devices", filter); const devices = await db.query("devices", filter);
if (!devices.length) return void (ctx.status = 404); if (!devices.length) return void next();
const device = devices[0];
const validate = authorizer.getValidator("devices", device); const validate = authorizer.getValidator("devices", devices[0]);
for (const t of ctx.request.body) { for (const t of ctx.request.body) {
if (!validate("task", t)) { if (!validate("task", t)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
@@ -564,45 +494,19 @@ router.post("/devices/:id/tasks", async (ctx) => {
} }
} }
// TODO Pass context and timestamp
const onlineThreshold = getConfig( const onlineThreshold = getConfig(
ctx.state.configSnapshot, ctx.state.configSnapshot,
"cwmp.deviceOnlineThreshold", "cwmp.deviceOnlineThreshold"
{}, );
Date.now(),
(exp) => {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM") {
const p = device[exp[1]];
if (p?.value) return p.value[0];
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") {
for (const root of ["InternetGatewayDevice", "Device"]) {
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
if (p?.value) return new URL(p.value[0]).hostname;
}
return null;
}
}
return exp;
}
) as number;
const socketTimeout: number = ctx.socket["timeout"];
// Disable socket timeout while waiting for postTasks()
if (socketTimeout) ctx.socket.setTimeout(0);
const res = await apiFunctions.postTasks( const res = await apiFunctions.postTasks(
ctx.params.id, ctx.params.id,
ctx.request.body, ctx.request.body,
onlineThreshold, onlineThreshold
device
); );
// Restore socket timeout log.tasks = res.tasks.map(t => t._id).join(",");
if (socketTimeout) ctx.socket.setTimeout(socketTimeout);
log.tasks = res.tasks.map((t) => t._id).join(",");
logger.accessInfo(log); logger.accessInfo(log);
@@ -610,26 +514,26 @@ router.post("/devices/:id/tasks", async (ctx) => {
ctx.body = res.tasks; ctx.body = res.tasks;
}); });
router.post("/devices/:id/tags", async (ctx) => { router.post("/devices/:id/tags", async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const log = { const log = {
message: "Update tags", message: "Update tags",
context: ctx, context: ctx,
deviceId: ctx.params.id, deviceId: ctx.params.id,
tags: ctx.request.body, tags: ctx.request.body
}; };
const filter = and(authorizer.getFilter("devices", 3), [ const filter = and(authorizer.getFilter("devices", 3), [
"=", "=",
["PARAM", "DeviceID.ID"], ["PARAM", "DeviceID.ID"],
ctx.params.id, ctx.params.id
]); ]);
if (!authorizer.hasAccess("devices", 3)) { if (!authorizer.hasAccess("devices", 3)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
const res = await db.query("devices", filter); const res = await db.query("devices", filter);
if (!res.length) return void (ctx.status = 404); if (!res.length) return void next();
const validate = authorizer.getValidator("devices", res[0]); const validate = authorizer.getValidator("devices", res[0]);
if (!validate("tags", ctx.request.body)) { if (!validate("tags", ctx.request.body)) {
@@ -651,27 +555,27 @@ router.post("/devices/:id/tags", async (ctx) => {
ctx.body = ""; ctx.body = "";
}); });
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;
} else { } else {
ctx.status = 500; ctx.status = 500;
ctx.body = err ? `${err.name}: ${err.message}` : "Unknown error"; ctx.body = `${err.name}: ${err.message}`;
} }
resolve(); resolve();
}); });
}); });
}); });
router.put("/users/:id/password", async (ctx) => { router.put("/users/:id/password", async (ctx, next) => {
const authorizer: Authorizer = ctx.state.authorizer; const authorizer: Authorizer = ctx.state.authorizer;
const username = ctx.params.id; const username = ctx.params.id;
const log = { const log = {
message: "Change password", message: "Change password",
context: ctx, context: ctx,
username: username, username: username
}; };
if (!ctx.state.user) { if (!ctx.state.user) {
@@ -690,16 +594,16 @@ router.put("/users/:id/password", async (ctx) => {
} }
} else if (!authorizer.hasAccess("users", 3)) { } else if (!authorizer.hasAccess("users", 3)) {
logUnauthorizedWarning(log); logUnauthorizedWarning(log);
return void (ctx.status = 403); return void next();
} }
const filter = and(authorizer.getFilter("users", 3), [ const filter = and(authorizer.getFilter("users", 3), [
"=", "=",
["PARAM", RESOURCE_IDS.users], ["PARAM", RESOURCE_IDS.users],
username, username
]); ]);
const res = await db.query("users", filter); const res = await db.query("users", filter);
if (!res.length) return void (ctx.status = 404); if (!res.length) return void next();
const newPassword = ctx.request.body.newPassword; const newPassword = ctx.request.body.newPassword;
if (ctx.state.user) { if (ctx.state.user) {
+254 -215
View File
@@ -17,46 +17,89 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Db, GridFSBucket, ObjectId } from "mongodb"; import { MongoClient, GridFSBucket } from "mongodb";
import { Script } from "vm"; import { Script } from "vm";
import { onConnect } from "../db"; import * as config from "../config";
import * as mongodbFunctions from "../mongodb-functions"; import * as mongodbFunctions from "../mongodb-functions";
import * as expression from "../common/expression"; import * as expression from "../common/expression";
import { QueryOptions, Expression } from "../types"; import { QueryOptions } from "../types";
import { Readable, Writable } from "stream";
import { minimize } from "../common/boolean-expression"; const CACHE_TTL = 300000;
let clientPromise: Promise<MongoClient>;
const RESOURCE_COLLECTION = { const RESOURCE_COLLECTION = {
files: "fs.files", files: "fs.files"
}; };
let db: Db; function ensureIndexes(client): void {
client
.db()
.collection("cache")
.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
}
onConnect(async (_db) => { function getClient(): Promise<MongoClient> {
db = _db; if (!clientPromise) {
}); clientPromise = new Promise((resolve, reject) => {
const CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
MongoClient.connect(
CONNECTION_URL,
{ useNewUrlParser: true },
(err, client) => {
if (err) return void reject(err);
ensureIndexes(client);
resolve(client);
}
);
});
}
return clientPromise;
}
export function cache<T>(key, valueGetter: () => Promise<T>, ttl): Promise<T> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const collection = client.db().collection("cache");
collection.findOne({ _id: key }, (err, doc) => {
if (err) return void reject(err);
if (doc != null) return void resolve(JSON.parse(doc.value) as T);
valueGetter()
.then(res => {
const expire = Date.now() + (ttl || CACHE_TTL);
const cacheDoc = {
_id: key,
value: JSON.stringify(res),
expire: new Date(expire)
};
collection.updateOne(
{ _id: key },
{ $set: cacheDoc },
{ upsert: true },
err => {
if (err) reject(err);
else resolve(res);
}
);
})
.catch(reject);
});
})
.catch(reject);
});
}
export function query( export function query(
resource: string, resource,
filter: Expression, filter,
options?: QueryOptions
): Promise<any[]>;
export function query(
resource: string,
filter: Expression,
options: QueryOptions,
callback: (doc: any) => void
): Promise<void>;
export function query(
resource: string,
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());
filter = minimize(filter, true);
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
if (resource === "devices") { if (resource === "devices") {
@@ -73,85 +116,89 @@ export function query(
} }
q = mongodbFunctions.filterToMongoQuery(filter); q = mongodbFunctions.filterToMongoQuery(filter);
} else if (!filter) { } else if (filter != null && !filter) {
return Promise.resolve([]); return Promise.resolve([]);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource); getClient().then(client => {
const cursor = collection.find(q); const collection = client
if (options.projection) { .db()
let projection = options.projection; .collection(RESOURCE_COLLECTION[resource] || resource);
if (resource === "devices") { const cursor = collection.find(q);
projection = mongodbFunctions.processDeviceProjection( if (options.projection) {
options.projection let projection = options.projection;
); if (resource === "devices") {
projection = mongodbFunctions.processDeviceProjection(
options.projection
);
}
if (resource === "presets") projection.configurations = 1;
cursor.project(projection);
} }
if (resource === "presets") projection.configurations = 1; if (resource === "users") cursor.project({ password: 0, salt: 0 });
cursor.project(projection);
}
if (resource === "users") cursor.project({ password: 0, salt: 0 }); if (options.skip) cursor.skip(options.skip);
if (options.limit) cursor.limit(options.limit);
if (options.skip) cursor.skip(options.skip); if (options.sort) {
if (options.limit) cursor.limit(options.limit); let s = Object.entries(options.sort)
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
.reduce(
(obj, [k, v]) =>
Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }),
{}
);
if (options.sort) { if (resource === "devices") s = mongodbFunctions.processDeviceSort(s);
let s = Object.entries(options.sort) cursor.sort(s);
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1])) }
.reduce(
(obj, [k, v]) =>
Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }),
{}
);
if (resource === "devices") s = mongodbFunctions.processDeviceSort(s); if (!callback) {
cursor.sort(s); cursor.toArray((err, docs) => {
} if (err) return reject(err);
if (resource === "devices")
if (!callback) { docs = docs.map(d => mongodbFunctions.flattenDevice(d));
cursor.toArray((err, docs) => {
if (err) return reject(err);
if (resource === "devices")
docs = docs.map((d) => mongodbFunctions.flattenDevice(d));
else if (resource === "faults")
docs = docs.map((d) => mongodbFunctions.flattenFault(d));
else if (resource === "tasks")
docs = docs.map((d) => mongodbFunctions.flattenTask(d));
else if (resource === "presets")
docs = docs.map((d) => mongodbFunctions.flattenPreset(d));
else if (resource === "files")
docs = docs.map((d) => mongodbFunctions.flattenFile(d));
return resolve(docs);
});
} else {
cursor.forEach(
(doc) => {
if (resource === "devices") doc = mongodbFunctions.flattenDevice(doc);
else if (resource === "faults") else if (resource === "faults")
doc = mongodbFunctions.flattenFault(doc); docs = docs.map(d => mongodbFunctions.flattenFault(d));
else if (resource === "tasks") else if (resource === "tasks")
doc = mongodbFunctions.flattenTask(doc); docs = docs.map(d => mongodbFunctions.flattenTask(d));
else if (resource === "presets") else if (resource === "presets")
doc = mongodbFunctions.flattenPreset(doc); docs = docs.map(d => mongodbFunctions.flattenPreset(d));
else if (resource === "files") else if (resource === "files")
doc = mongodbFunctions.flattenFile(doc); docs = docs.map(d => mongodbFunctions.flattenFile(d));
callback(doc); return resolve(docs);
}, });
(err) => { } else {
if (err) reject(err); cursor.forEach(
else resolve(); doc => {
} if (resource === "devices")
); doc = mongodbFunctions.flattenDevice(doc);
} else if (resource === "faults")
doc = mongodbFunctions.flattenFault(doc);
else if (resource === "tasks")
doc = mongodbFunctions.flattenTask(doc);
else if (resource === "presets")
doc = mongodbFunctions.flattenPreset(doc);
else if (resource === "files")
doc = mongodbFunctions.flattenFile(doc);
callback(doc);
},
err => {
if (err) reject(err);
else resolve();
}
);
}
});
}); });
} }
export function count(resource: string, filter: Expression): Promise<number> { export function count(resource, filter): Promise<number> {
let q; let q;
filter = expression.evaluate(filter, null, Date.now()); filter = expression.evaluate(filter, null, Date.now());
filter = minimize(filter, true);
if (Array.isArray(filter)) { if (Array.isArray(filter)) {
if (resource === "devices") if (resource === "devices")
@@ -161,211 +208,203 @@ export function count(resource: string, filter: Expression): Promise<number> {
else if (resource === "faults") else if (resource === "faults")
filter = mongodbFunctions.processFaultsFilter(filter); filter = mongodbFunctions.processFaultsFilter(filter);
q = mongodbFunctions.filterToMongoQuery(filter); q = mongodbFunctions.filterToMongoQuery(filter);
} else if (!filter) { } else if (filter != null && !filter) {
return Promise.resolve(0); return Promise.resolve(0);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource); getClient().then(client => {
collection.find(q).count((err, c) => { const collection = client
if (err) reject(err); .db()
else resolve(c); .collection(RESOURCE_COLLECTION[resource] || resource);
collection.find(q).count((err, c) => {
if (err) reject(err);
else resolve(c);
});
}); });
}); });
} }
export async function updateDeviceTags( export function updateDeviceTags(deviceId, tags): Promise<void> {
deviceId: string, return new Promise((resolve, reject) => {
tags: Record<string, boolean> const add = [];
): Promise<void> { const pull = [];
const add = [];
const pull = [];
for (let [tag, onOff] of Object.entries(tags)) { const regex = /^[0-9a-zA-Z_]+$/;
tag = tag.trim(); for (let [tag, onOff] of Object.entries(tags)) {
if (onOff) add.push(tag); tag = tag.trim();
else pull.push(tag); if (onOff) {
} if (!tag.match(regex))
return void reject(new Error(`Invalid tag '${tag}'`));
add.push(tag);
} else {
pull.push(tag);
}
}
getClient()
.then(client => {
const collection = client.db().collection("devices");
const collection = db.collection("devices"); const object = {};
const object = {}; if (add && add.length) object["$addToSet"] = { _tags: { $each: add } };
if (pull && pull.length) object["$pullAll"] = { _tags: pull };
if (add?.length) object["$addToSet"] = { _tags: { $each: add } }; collection.updateOne({ _id: deviceId }, object, err => {
if (pull?.length) object["$pullAll"] = { _tags: pull }; if (err) return void reject(err);
resolve();
await collection.updateOne({ _id: deviceId }, object); });
})
.catch(reject);
});
} }
function putResource(resource, id, object): Promise<void> { function putResource(resource, id, object): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource); getClient()
collection.replaceOne({ _id: id }, object, { upsert: true }, (err) => { .then(client => {
if (err) return void reject(err); const collection = client
resolve(); .db()
}); .collection(RESOURCE_COLLECTION[resource] || resource);
collection.replaceOne({ _id: id }, object, { upsert: true }, err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
}); });
} }
function deleteResource( function deleteResource(resource, id): Promise<void> {
resource: string,
id: string | ObjectId
): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource); getClient()
collection.deleteOne({ _id: id }, (err) => { .then(client => {
if (err) return void reject(err); const collection = client
resolve(); .db()
}); .collection(RESOURCE_COLLECTION[resource] || resource);
collection.deleteOne({ _id: id }, err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
}); });
} }
export function putPreset( export function putPreset(id, object): Promise<void> {
id: string,
object: Record<string, unknown>
): Promise<void> {
object = mongodbFunctions.preProcessPreset(object); object = mongodbFunctions.preProcessPreset(object);
return putResource("presets", id, object); return putResource("presets", id, object);
} }
export function deletePreset(id: string): Promise<void> { export function deletePreset(id): Promise<void> {
return deleteResource("presets", id); return deleteResource("presets", id);
} }
export function putProvision( export function putProvision(id, object): Promise<void> {
id: string,
object: Record<string, unknown>
): 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?.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);
} }
export function deleteProvision(id: string): Promise<void> { export function deleteProvision(id): Promise<void> {
return deleteResource("provisions", id); return deleteResource("provisions", id);
} }
export function putVirtualParameter( export function putVirtualParameter(id, object): Promise<void> {
id: string,
object: Record<string, unknown>
): 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?.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);
} }
export function deleteVirtualParameter(id: string): Promise<void> { export function deleteVirtualParameter(id): Promise<void> {
return deleteResource("virtualParameters", id); return deleteResource("virtualParameters", id);
} }
export function putConfig( export function putConfig(id, object): Promise<void> {
id: string,
object: Record<string, unknown>
): Promise<void> {
return putResource("config", id, object); return putResource("config", id, object);
} }
export function deleteConfig(id: string): Promise<void> { export function deleteConfig(id): Promise<void> {
return deleteResource("config", id); return deleteResource("config", id);
} }
export function putPermission( export function putPermission(id, object): Promise<void> {
id: string,
object: Record<string, unknown>
): Promise<void> {
return putResource("permissions", id, object); return putResource("permissions", id, object);
} }
export function deletePermission(id: string): Promise<void> { export function deletePermission(id): Promise<void> {
return deleteResource("permissions", id); return deleteResource("permissions", id);
} }
export function putUser( export function putUser(id, object): Promise<void> {
id: string,
object: Record<string, unknown>
): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const collection = db.collection("users"); getClient()
// update instead of replace to keep the password if not set by user .then(client => {
collection.updateOne( const collection = client.db().collection("users");
{ _id: id }, // update instead of replace to keep the password if not set by user
{ $set: object }, collection.updateOne(
{ upsert: true }, { _id: id },
(err) => { { $set: object },
if (err) return void reject(err); { upsert: true },
resolve(); err => {
} if (err) return void reject(err);
); resolve();
}
);
})
.catch(reject);
}); });
} }
export function deleteUser(id: string): Promise<void> { export function deleteUser(id): Promise<void> {
return deleteResource("users", id); return deleteResource("users", id);
} }
export function downloadFile(filename: string): Readable { export function putFile(filename, metadata, contentStream): Promise<void> {
const bucket = new GridFSBucket(db);
return bucket.openDownloadStreamByName(filename);
}
export function putFile(
filename: string,
metadata: Record<string, string>,
contentStream: Readable
): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const bucket = new GridFSBucket(db); getClient()
const uploadStream = bucket.openUploadStreamWithId( .then(client => {
filename as unknown as ObjectId, const bucket = new GridFSBucket(client.db());
filename, const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
{ metadata: metadata
metadata: metadata, });
} uploadStream.on("error", reject);
); contentStream.on("error", reject);
uploadStream.on("error", reject); uploadStream.on("finish", resolve);
contentStream.on("error", reject); contentStream.pipe(uploadStream);
uploadStream.on("finish", resolve); })
contentStream.pipe(uploadStream as Writable); .catch(reject);
}); });
} }
export function deleteFile(filename: string): Promise<void> { export function deleteFile(filename): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const bucket = new GridFSBucket(db); getClient()
bucket.delete(filename as any, (err) => { .then(client => {
if (err) return void reject(err); const bucket = new GridFSBucket(client.db());
resolve(); bucket.delete(filename, err => {
}); if (err) return void reject(err);
resolve();
});
})
.catch(reject);
}); });
} }
export function deleteFault(id: string): Promise<void> { export function deleteFault(id): Promise<void> {
return deleteResource("faults", id); return deleteResource("faults", id);
} }
export function deleteTask(id: ObjectId): Promise<void> { export function deleteTask(id): Promise<void> {
return deleteResource("tasks", id); return deleteResource("tasks", id);
} }
export async function disconnect(): Promise<void> {
if (clientPromise) await (await clientPromise).close();
}
+6 -6
View File
@@ -26,10 +26,10 @@ interface Revisions<V> {
} }
export default class VersionedMap<K, V> { export default class VersionedMap<K, V> {
private declare _sizeDiff: number[]; private _sizeDiff: number[];
private declare _revision: number; private _revision: number;
private declare map: Map<K, (V | symbol)[]>; private map: Map<K, (V | symbol)[]>;
public declare dirty: number; public dirty: number;
public constructor() { public constructor() {
this._sizeDiff = [0]; this._sizeDiff = [0];
@@ -46,7 +46,7 @@ export default class VersionedMap<K, V> {
return this._revision; return this._revision;
} }
public set revision(rev: number) { public set revision(rev) {
for (let i = this._sizeDiff.length; i <= rev; ++i) for (let i = this._sizeDiff.length; i <= rev; ++i)
this._sizeDiff[i] = this._sizeDiff[i - 1]; this._sizeDiff[i] = this._sizeDiff[i - 1];
@@ -193,7 +193,7 @@ export default class VersionedMap<K, V> {
const last = v[l]; const last = v[l];
v.splice(revision, l - revision); v.splice(revision, l - revision);
if (last === NONEXISTENT && !v.some((vv) => vv !== NONEXISTENT)) if (last === NONEXISTENT && !v.some(vv => vv !== NONEXISTENT))
this.map.delete(k); this.map.delete(k);
} }
} }
+11 -14
View File
@@ -53,13 +53,12 @@ export interface Element {
} }
export function parseXmlDeclaration(buffer: Buffer): Attribute[] { export function parseXmlDeclaration(buffer: Buffer): Attribute[] {
const encodings: BufferEncoding[] = ["utf16le", "utf8", "latin1", "ascii"]; for (const enc of ["utf16le", "utf8", "latin1", "ascii"]) {
for (const enc of encodings) {
let str = buffer.toString(enc, 0, 150); let str = buffer.toString(enc, 0, 150);
if (str.startsWith("<?xml")) { if (str.startsWith("<?xml")) {
str = str.slice(0, str.indexOf("?>")); str = str.split("\n")[0].trim();
try { try {
return parseAttrs(str.slice(5)); return parseAttrs(str.slice(5, -2));
} catch (err) { } catch (err) {
// Ignore // Ignore
} }
@@ -91,7 +90,7 @@ export function parseAttrs(string: string): Attribute[] {
name: name, name: name,
namespace: namespace, namespace: namespace,
localName: localName, localName: localName,
value: value, value: value
}; };
attrs.push(e); attrs.push(e);
name = ""; name = "";
@@ -104,12 +103,10 @@ export function parseAttrs(string: string): Attribute[] {
continue; continue;
case CHAR_COLON: case CHAR_COLON:
if (state) continue;
if (idx >= colonIdx) colonIdx = i; if (idx >= colonIdx) colonIdx = i;
continue; continue;
case CHAR_EQUAL: case CHAR_EQUAL:
if (state) continue;
if (name) throw new Error(`Unexpected character at ${i}`); if (name) throw new Error(`Unexpected character at ${i}`);
name = string.slice(idx, i).trim(); name = string.slice(idx, i).trim();
// TODO validate name // TODO validate name
@@ -131,8 +128,8 @@ export function parseAttrs(string: string): Attribute[] {
return attrs; return attrs;
} }
export function decodeEntities(string: string): string { export function decodeEntities(string): string {
return string.replace(/&[0-9a-z#]+;/gi, (match) => { return string.replace(/&[0-9a-z#]+;/gi, match => {
switch (match) { switch (match) {
case "&quot;": case "&quot;":
return '"'; return '"';
@@ -164,15 +161,15 @@ export function decodeEntities(string: string): string {
}); });
} }
export function encodeEntities(string: string): string { export function encodeEntities(string): string {
const entities = { const entities = {
"&": "&amp;", "&": "&amp;",
'"': "&quot;", '"': "&quot;",
"'": "&apos;", "'": "&apos;",
"<": "&lt;", "<": "&lt;",
">": "&gt;", ">": "&gt;"
}; };
return string.replace(/[&"'<>]/g, (m) => entities[m]); return string.replace(/[&"'<>]/g, m => entities[m]);
} }
export function parseXml(string: string): Element { export function parseXml(string: string): Element {
@@ -189,7 +186,7 @@ export function parseXml(string: string): Element {
attrs: "", attrs: "",
text: "", text: "",
bodyIndex: 0, bodyIndex: 0,
children: [], children: []
}; };
const stack: Element[] = [root]; const stack: Element[] = [root];
@@ -333,7 +330,7 @@ export function parseXml(string: string): Element {
: "", : "",
text: "", text: "",
bodyIndex: i + 1, bodyIndex: i + 1,
children: [], children: []
}; };
parent.children.push(e); parent.children.push(e);
if (!selfClosing) stack.push(e); if (!selfClosing) stack.push(e);
+7615 -11312
View File
File diff suppressed because it is too large. Load diff
+55 -51
View File
@@ -1,6 +1,6 @@
{ {
"name": "genieacs", "name": "genieacs",
"version": "1.2.8", "version": "1.2.0-beta.0",
"description": "A TR-069 Auto Configuration Server (ACS)", "description": "A TR-069 Auto Configuration Server (ACS)",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -14,71 +14,75 @@
], ],
"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": {
"@breejs/later": "^4.1.0", "iconv-lite": "^0.5.0",
"espresso-iisojs": "^1.0.4",
"iconv-lite": "^0.6.3",
"ipaddr.js": "^2.0.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.13.4", "koa": "^2.7.0",
"koa-bodyparser": "^4.3.0", "koa-bodyparser": "^4.2.1",
"koa-compress": "^5.1.0", "koa-compress": "^3.0.0",
"koa-jwt": "^4.0.3", "koa-jwt": "^3.6.0",
"koa-router": "^10.1.1", "koa-router": "^7.4.0",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"mongodb": "^4.1.3", "later": "^1.2.0",
"parsimmon": "^1.18.0", "mongodb": "^3.2.6",
"seedrandom": "^3.0.5" "parsimmon": "^1.13.0",
"seedrandom": "^3.0.1",
"yaml": "^1.6.0"
},
"optionalDependencies": {
"libxmljs": "^0.19.5"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^21.0.1", "@types/jsonwebtoken": "^8.3.2",
"@rollup/plugin-json": "^4.1.0", "@types/koa": "^2.0.49",
"@rollup/plugin-node-resolve": "^13.0.6", "@types/koa-compress": "^2.0.9",
"@rollup/plugin-typescript": "^8.3.0", "@types/koa-router": "^7.0.42",
"@types/codemirror": "^5.60.5", "@types/koa-static": "^4.0.1",
"@types/jsonwebtoken": "^8.5.5", "@types/libxmljs": "^0.18.3",
"@types/koa": "^2.13.4", "@types/mithril": "^2.0.0",
"@types/koa-compress": "^4.0.3", "@types/mongodb": "^3.1.31",
"@types/koa-router": "^7.4.4", "@types/parsimmon": "^1.10.0",
"@types/koa-static": "^4.0.2", "@types/seedrandom": "^2.4.28",
"@types/mithril": "^2.0.8", "@types/svgo": "^1.2.1",
"@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "^1.13.0",
"@types/parsimmon": "^1.10.6", "@typescript-eslint/parser": "^1.13.0",
"@types/seedrandom": "^3.0.1", "ava": "^2.2.0",
"@typescript-eslint/eslint-plugin": "^5.2.0", "codemirror": "^5.48.2",
"@typescript-eslint/parser": "^5.2.0", "cssnano": "^4.1.10",
"ava": "^3.15.0", "eslint": "^6.1.0",
"codemirror": "^5.63.3", "eslint-config-prettier": "^6.0.0",
"cssnano": "^5.0.8",
"eslint": "^8.1.0",
"eslint-config-prettier": "^8.3.0",
"esm": "^3.2.25", "esm": "^3.2.25",
"mithril": "^2.0.4", "mithril": "^2.0.3",
"postcss": "^8.3.11", "postcss": "^7.0.17",
"postcss-import": "^14.0.2", "postcss-import": "^12.0.1",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.4.1", "prettier": "^1.18.2",
"rollup": "^2.58.3", "rollup": "^1.17.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-json": "^4.0.0",
"sql.js": "^1.6.2", "rollup-plugin-replace": "^2.2.0",
"svgo": "^2.7.0", "rollup-plugin-terser": "^5.1.1",
"ts-node": "^10.4.0", "rollup-plugin-typescript": "^1.0.1",
"typescript": "^4.4.4", "source-map-loader": "^0.2.4",
"yaml": "^1.10.2" "svgo": "^1.3.0",
"ts-node": "^8.3.0",
"typescript": "^3.5.3",
"webpack": "^4.38.0"
}, },
"engines": { "engines": {
"node": ">=12.13.0" "node": ">=8.14.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-ln": "find bin -type f -exec sh -c 'ln -fs \"$(sh -c pwd)/$1\" \"bin/.${1#bin/}.ts\"' _ {} \\;; find tools -type f -exec sh -c 'ln -fs \"$(sh -c pwd)/$1\" \"tools/.${1#tools/}.ts\"' _ {} \\;",
"lint-prettier": "prettier --prose-wrap always --write .", "lint-eslint": "eslint --fix \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"",
"lint-prettier": "prettier --parser typescript --write \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"",
"lint-tsc": "tsc --noEmit", "lint-tsc": "tsc --noEmit",
"lint": "npm run lint-eslint && npm run lint-prettier && npm run lint-tsc", "lint": "npm run lint-ln && npm run lint-eslint && npm run lint-prettier && npm run lint-tsc",
"build": "node -r esm -r ts-node/register/transpile-only build/build" "build": "node -r esm -r ts-node/register/transpile-only build/build"
} }
} }
+2 -2
View File
@@ -2,7 +2,7 @@ import ava from "ava";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import * as auth from "../lib/auth"; import * as auth from "../lib/auth";
ava("digest", (t) => { ava("digest", t => {
const username = "test"; const username = "test";
const password = "test"; const password = "test";
const uri = "/"; const uri = "/";
@@ -14,7 +14,7 @@ ava("digest", (t) => {
const challenges = [ const challenges = [
`Digest realm="${realm}",nonce="${nonce}"`, `Digest realm="${realm}",nonce="${nonce}"`,
`Digest realm="${realm}",nonce="${nonce}",qop="auth"`, `Digest realm="${realm}",nonce="${nonce}",qop="auth"`,
`Digest realm="${realm}",nonce="${nonce}",qop="auth-int"`, `Digest realm="${realm}",nonce="${nonce}",qop="auth-int"`
]; ];
t.plan(challenges.length); t.plan(challenges.length);
-118
View File
@@ -1,118 +0,0 @@
import ava, { TestInterface } from "ava";
import { minimize, unionDiff } from "../lib/common/boolean-expression";
import { parse, stringify } from "../lib/common/expression-parser";
import initSqlJs from "sql.js";
const test = ava as TestInterface<{
query: (filter: string) => Promise<Set<number>>;
}>;
const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"];
const DECIMAL_VALUES = [null, 0, -10, 10];
test.before(async (t) => {
const sql = await initSqlJs();
const db = new sql.Database();
t.context.query = async (filter: string): Promise<Set<number>> => {
const res = db.exec(`SELECT id FROM test WHERE ${filter}`);
if (!res.length) return new Set();
return new Set(res[0].values.flat());
};
db.run(
"CREATE TABLE test (id INTEGER PRIMARY KEY, string STRING, decimal DECIMAL(4,2))"
);
const stmt = db.prepare("INSERT INTO test (string, decimal) VALUES (?, ?)");
const count = STRING_VALUES.length * DECIMAL_VALUES.length;
for (let i = 0; i < count; ++i) {
const str = i % STRING_VALUES.length;
const dec = Math.trunc(i / STRING_VALUES.length) % DECIMAL_VALUES.length;
stmt.run([STRING_VALUES[str], DECIMAL_VALUES[dec]]);
}
stmt.free();
});
function setsEqual(set1: Set<number>, set2: Set<number>): boolean {
if (set1.size !== set2.size) return false;
for (const s of set1) if (!set2.has(s)) return false;
return true;
}
function getPermutations(...arrs: any[][]): any[][] {
const count = arrs.reduce((total, arr) => total * arr.length, 1);
const res = [];
for (let i = 0; i < count; ++i) {
let j = i;
const row = [];
for (const arr of arrs) {
const v = arr[j % arr.length];
j = Math.trunc(j / arr.length);
row.push(v);
}
res.push(row);
}
return res;
}
test("minimize", async (t) => {
const cases: string[] = [];
cases.push("null");
cases.push("false");
cases.push("true");
cases.push("string");
cases.push("(string + decimal) IS NULL");
cases.push("(string + decimal) = NULL");
cases.push("COALESCE(string, decimal) = 0");
for (const [s1, s2, s3, op1, op2] of getPermutations(
STRING_VALUES.filter((s) => s),
STRING_VALUES.filter((s) => s),
STRING_VALUES.filter((s) => s),
[">", "=", "<"],
["<>", ">="]
)) {
cases.push(
`string ${op1} "${s1}" OR string ='${s2}' OR NOT string ${op2} '${s3}'`
);
}
for (const [s1, s2] of getPermutations(
STRING_VALUES.filter((s) => s),
STRING_VALUES.filter((s) => s)
))
cases.push(`string > "${s1}" AND string < '${s2}'`);
for (const c of cases) {
const res1 = await t.context.query(c);
const min = stringify(minimize(parse(c), true));
const res2 = await t.context.query(min);
t.true(setsEqual(res1, res2));
}
});
test("unionDiff", async (t) => {
const cases = [
"true",
"decimal > 0",
"decimal > 10",
"UPPER(string || decimal) LIKE 'AB10'",
"COALESCE(string, decimal) = 0",
];
for (const [c1, c2] of getPermutations(cases, cases)) {
const res1 = await t.context.query(c1);
const res2 = await t.context.query(c2);
const [union, diff] = unionDiff(parse(c1), parse(c2));
const res3 = await t.context.query(stringify(union));
const res4 = await t.context.query(stringify(diff));
const unionSet = new Set([...res1, ...res2]);
const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r)));
t.true(setsEqual(res3, unionSet));
t.true(setsEqual(res4, diffSet));
}
});
+38 -11
View File
@@ -1,7 +1,35 @@
import ava from "ava"; import ava from "ava";
import * as common from "../lib/common"; import * as common from "../lib/common";
ava("generateDeviceId", (t) => { ava("typeOf", t => {
const cases = [
[undefined, common.UNDEFINED_TYPE],
[null, common.NULL_TYPE],
[true, common.BOOLEAN_TYPE],
[false, common.BOOLEAN_TYPE],
[0, common.NUMBER_TYPE],
[1, common.NUMBER_TYPE],
[-1, common.NUMBER_TYPE],
[0.0, common.NUMBER_TYPE],
[1.0, common.NUMBER_TYPE],
[1.1, common.NUMBER_TYPE],
[-1.0, common.NUMBER_TYPE],
[-1.1, common.NUMBER_TYPE],
["", common.STRING_TYPE],
["TEST", common.STRING_TYPE],
[[], common.ARRAY_TYPE],
[[1, 2, 3], common.ARRAY_TYPE],
[{}, common.OBJECT_TYPE],
[{ test: 1 }, common.OBJECT_TYPE],
[/[^A-Za-z0-9_]/g, common.REGEXP_TYPE],
[new RegExp("//"), common.REGEXP_TYPE],
[new Date(), common.DATE_TYPE]
];
t.plan(cases.length);
for (const c of cases) t.is(common.typeOf(c[0]), c[1]);
});
ava("generateDeviceId", t => {
const space = [" ", "%20"]; const space = [" ", "%20"];
const special = [";", "%3B"]; const special = [";", "%3B"];
const cases = [ const cases = [
@@ -9,31 +37,30 @@ ava("generateDeviceId", (t) => {
{ {
ProductClass: "TestProductClass", ProductClass: "TestProductClass",
OUI: "TestOUI", OUI: "TestOUI",
SerialNumber: "TestSerialNumber", SerialNumber: "TestSerialNumber"
}, },
"TestOUI-TestProductClass-TestSerialNumber", "TestOUI-TestProductClass-TestSerialNumber"
], ],
[ [
{ {
OUI: "TestOUI", OUI: "TestOUI",
SerialNumber: "TestSerialNumber", SerialNumber: "TestSerialNumber"
}, },
"TestOUI-TestSerialNumber", "TestOUI-TestSerialNumber"
], ],
[ [
{ {
OUI: `TestOUIWith${space[0]}_${special[0]}2912`, OUI: `TestOUIWith${space[0]}_${special[0]}2912`,
SerialNumber: `TestSerialNumberWith${space[0]}_${special[0]}2912`, SerialNumber: `TestSerialNumberWith${space[0]}_${special[0]}2912`
}, },
`TestOUIWith${space[1]}_${special[1]}2912-TestSerialNumberWith${space[1]}_${special[1]}2912`, `TestOUIWith${space[1]}_${special[1]}2912-TestSerialNumberWith${space[1]}_${special[1]}2912`
], ]
]; ];
t.plan(cases.length); t.plan(cases.length);
for (const c of cases) for (const c of cases) t.is(common.generateDeviceId(c[0]), c[1]);
t.is(common.generateDeviceId(c[0] as Record<string, string>), c[1]);
}); });
ava("escapeRegExp", (t) => { ava("escapeRegExp", t => {
t.is( t.is(
common.escapeRegExp("\\ ^ $ * + ? . ( ) | { } [ ]"), common.escapeRegExp("\\ ^ $ * + ? . ( ) | { } [ ]"),
"\\\\ \\^ \\$ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]" "\\\\ \\^ \\$ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]"
+19 -19
View File
@@ -5,7 +5,7 @@ import PathSet from "../lib/common/path-set";
import VersionedMap from "../lib/versioned-map"; import VersionedMap from "../lib/versioned-map";
import { Attributes } from "../lib/types"; import { Attributes } from "../lib/types";
ava("getAliasDeclarations", (t) => { ava("getAliasDeclarations", t => {
const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b"); const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b");
const decs = device.getAliasDeclarations(path, 99); const decs = device.getAliasDeclarations(path, 99);
@@ -22,64 +22,64 @@ ava("getAliasDeclarations", (t) => {
} }
}); });
ava("unpack", (t) => { ava("unpack", t => {
const now = Date.now(); const now = Date.now();
const deviceData = { const deviceData = {
paths: new PathSet(), paths: new PathSet(),
timestamps: new VersionedMap<Path, number>(), timestamps: new VersionedMap<Path, number>(),
attributes: new VersionedMap<Path, Attributes>(), attributes: new VersionedMap<Path, Attributes>(),
trackers: new Map<Path, { [name: string]: number }>(), trackers: new Map<Path, { [name: string]: number }>(),
changes: new Set<string>(), changes: new Set<string>()
}; };
device.set(deviceData, Path.parse("a.1.b"), now, { device.set(deviceData, Path.parse("a.1.b"), now, {
value: [now, ["b", "xsd:string"]], value: [now, ["b", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.1.c"), now, { device.set(deviceData, Path.parse("a.1.c"), now, {
value: [now, ["c", "xsd:string"]], value: [now, ["c", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.1.a.1.a"), now, { device.set(deviceData, Path.parse("a.1.a.1.a"), now, {
value: [now, ["", "xsd:string"]], value: [now, ["", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.1.a.1.b"), now, { device.set(deviceData, Path.parse("a.1.a.1.b"), now, {
value: [now, ["b1", "xsd:string"]], value: [now, ["b1", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.1.a.1.c"), now, { device.set(deviceData, Path.parse("a.1.a.1.c"), now, {
value: [now, ["c1", "xsd:string"]], value: [now, ["c1", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.1.a.2.a"), now, { device.set(deviceData, Path.parse("a.1.a.2.a"), now, {
value: [now, ["", "xsd:string"]], value: [now, ["", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.1.a.2.b"), now, { device.set(deviceData, Path.parse("a.1.a.2.b"), now, {
value: [now, ["b2", "xsd:string"]], value: [now, ["b2", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.1.a.2.c"), now, { device.set(deviceData, Path.parse("a.1.a.2.c"), now, {
value: [now, ["c2", "xsd:string"]], value: [now, ["c2", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.2.b"), now, { device.set(deviceData, Path.parse("a.2.b"), now, {
value: [now, ["b", "xsd:string"]], value: [now, ["b", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.2.c"), now, { device.set(deviceData, Path.parse("a.2.c"), now, {
value: [now, ["c", "xsd:string"]], value: [now, ["c", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.2.a.1.a"), now, { device.set(deviceData, Path.parse("a.2.a.1.a"), now, {
value: [now, ["", "xsd:string"]], value: [now, ["", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.2.a.1.b"), now, { device.set(deviceData, Path.parse("a.2.a.1.b"), now, {
value: [now, ["b1", "xsd:string"]], value: [now, ["b1", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.2.a.1.c"), now, { device.set(deviceData, Path.parse("a.2.a.1.c"), now, {
value: [now, ["c1", "xsd:string"]], value: [now, ["c1", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.2.a.2.a"), now, { device.set(deviceData, Path.parse("a.2.a.2.a"), now, {
value: [now, ["", "xsd:string"]], value: [now, ["", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.2.a.2.b"), now, { device.set(deviceData, Path.parse("a.2.a.2.b"), now, {
value: [now, ["c1", "xsd:string"]], value: [now, ["c1", "xsd:string"]]
}); });
device.set(deviceData, Path.parse("a.2.a.2.c"), now, { device.set(deviceData, Path.parse("a.2.a.2.c"), now, {
value: [now, ["b1", "xsd:string"]], value: [now, ["b1", "xsd:string"]]
}); });
let unpacked: Path[]; let unpacked: Path[];
+13 -91
View File
@@ -1,13 +1,13 @@
import ava from "ava"; import ava from "ava";
import * as mongodbFunctions from "../lib/mongodb-functions"; import * as mongodbFunctions from "../lib/mongodb-functions";
import { stringify, parse } from "../lib/common/expression-parser"; import { stringify } from "../lib/common/expression-parser";
ava("convertOldPrecondition", (t) => { ava("mongoQueryToFilter", t => {
const tests = [ const tests = [
[{}, "true"], [{}, "true"],
[{ test: "test" }, 'test = "test"'], [{ test: "test" }, 'test = "test"'],
[{ test: { $eq: "test" } }, 'test = "test"'], [{ test: { $eq: "test" } }, 'test = "test"'],
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'], [{ test: { $ne: "test" } }, 'test <> "test"'],
[{ test: { $gte: "test" } }, 'test >= "test"'], [{ test: { $gte: "test" } }, 'test >= "test"'],
[{ "Tags.test": true }, "Tags.test IS NOT NULL"], [{ "Tags.test": true }, "Tags.test IS NOT NULL"],
[{ "Tags.test": false }, "Tags.test IS NULL"], [{ "Tags.test": false }, "Tags.test IS NULL"],
@@ -22,21 +22,21 @@ ava("convertOldPrecondition", (t) => {
[{ _tags: { $eq: "test" } }, "Tags.test IS NOT NULL"], [{ _tags: { $eq: "test" } }, "Tags.test IS NOT NULL"],
[ [
{ $and: [{ test: "test" }, { test: { $ne: "test" } }] }, { $and: [{ test: "test" }, { test: { $ne: "test" } }] },
'test = "test" AND (test <> "test" OR test IS NULL)', 'test = "test" AND test <> "test"'
], ],
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'], [{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
[ [
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] }, { $or: [{ test: "test" }, { test: { $ne: "test" } }] },
'test = "test" OR (test <> "test" OR test IS NULL)', 'test = "test" OR test <> "test"'
], ],
[ [
{ test: { $gte: "test1", $ne: "test2" } }, { test: { $gte: "test1", $ne: "test2" } },
'test >= "test1" AND (test <> "test2" OR test IS NULL)', 'test >= "test1" AND test <> "test2"'
], ],
[ [
{ test: "test", test2: { $ne: "test2" } }, { test: "test", test2: { $ne: "test2" } },
'test = "test" AND (test2 <> "test2" OR test2 IS NULL)', 'test = "test" AND test2 <> "test2"'
], ]
]; ];
const shouldFailTests = [ const shouldFailTests = [
@@ -45,96 +45,18 @@ ava("convertOldPrecondition", (t) => {
[{ "Tags.test": { $gt: true } }, "Invalid tag query"], [{ "Tags.test": { $gt: true } }, "Invalid tag query"],
[{ _tags: [] }, "Invalid type"], [{ _tags: [] }, "Invalid type"],
[{ _tags: { $gt: "test" } }, "Invalid tag query"], [{ _tags: { $gt: "test" } }, "Invalid tag query"],
[{ $nor: [] }, "Operator $nor not supported"], [{ $nor: [] }, "Operator $nor not supported"]
]; ];
t.plan(tests.length + 2 * shouldFailTests.length); t.plan(tests.length + 2 * shouldFailTests.length);
for (const test of tests) { for (const test of tests)
t.is( t.is(stringify(mongodbFunctions.mongoQueryToFilter(test[0])), test[1]);
stringify(
mongodbFunctions.convertOldPrecondition(
test[0] as Record<string, unknown>
)
),
test[1]
);
}
for (const test of shouldFailTests) { for (const test of shouldFailTests) {
const func = (): void => { const func = (): void => {
mongodbFunctions.convertOldPrecondition( mongodbFunctions.mongoQueryToFilter(test[0]);
test[0] as Record<string, unknown>
);
}; };
const error = t.throws(func, { instanceOf: Error }); const error = t.throws(func, Error);
t.is(error.message, test[1]); t.is(error.message, test[1]);
} }
}); });
ava("filterToMongoQuery", async (t) => {
const queries: [string, Record<string, unknown>][] = [
["true", {}],
["Tags.tag1 = true", { _tags: "tag1" }],
["Tags.tag1 <> false", { _tags: "tag1" }],
["Tags.tag1 IS NULL", { _tags: { $ne: "tag1" } }],
["Tags.tag1 = 123", { "Tags.tag1": 123 }],
["Param1 = 'value1'", { "Param1._value": "value1" }],
["Param1 <> 'value1'", { "Param1._value": { $nin: ["value1", null] } }],
["Param1 > 'value'", { "Param1._value": { $gt: "value" } }],
["Param1 IS NOT NULL", { "Param1._value": { $ne: null } }],
["Param1 LIKE 'value'", { "Param1._value": /^value$/ }],
["LOWER(Param1) LIKE 'value'", { "Param1._value": /^value$/i }],
[
"Param1 <> 'value2' OR NOT (Param2 = 'value1' OR Param1 < 'value2')",
{
$or: [
{ "Param1._value": { $nin: ["value2", null] } },
{
$and: [
{ "Param2._value": { $nin: ["value1", null] } },
{ "Param1._value": { $gte: "value2" } },
],
},
],
},
],
[
"Param1 <> 'value2' OR Param1 IS NULL",
{
$or: [
{ "Param1._value": { $nin: ["value2", null] } },
{ "Param1._value": null },
],
},
],
];
for (const [expStr, expect] of queries) {
const exp = parse(expStr);
const query = mongodbFunctions.filterToMongoQuery(
mongodbFunctions.processDeviceFilter(exp)
);
t.deepEqual(query, expect);
}
const failQueries: [any, string][] = [
["Param1 = Param2", "Invalid RHS operand of = clause"],
["Param1 LIKE Param2", "Invalid RHS operand of LIKE clause"],
["1 LIKE '1'", "Invalid LHS operand of LIKE clause"],
["1 = 2", "Invalid LHS operand of = clause"],
["1 IS NULL", "Invalid LHS operand of IS NULL clause"],
["false", "Primitives are not valid queries"],
["UPPER(Param1) LIKE 'value'", "Invalid RHS operand of LIKE clause"],
];
for (const [expStr, err] of failQueries) {
const exp = parse(expStr);
t.throws(
() =>
mongodbFunctions.filterToMongoQuery(
mongodbFunctions.processDeviceFilter(exp)
),
{ message: err }
);
}
});
+10 -10
View File
@@ -2,7 +2,7 @@ import ava from "ava";
import Path from "../lib/common/path"; import Path from "../lib/common/path";
import PathSet from "../lib/common/path-set"; import PathSet from "../lib/common/path-set";
ava("depth", (t) => { ava("depth", t => {
const pathSet = new PathSet(); const pathSet = new PathSet();
t.is(pathSet.depth, 0); t.is(pathSet.depth, 0);
pathSet.add(Path.parse("")); pathSet.add(Path.parse(""));
@@ -11,14 +11,14 @@ ava("depth", (t) => {
t.is(pathSet.depth, 2); t.is(pathSet.depth, 2);
}); });
ava("add", (t) => { ava("add", t => {
const pathSet = new PathSet(); const pathSet = new PathSet();
pathSet.add(Path.parse("a")); pathSet.add(Path.parse("a"));
pathSet.add(Path.parse("a")); pathSet.add(Path.parse("a"));
t.is(pathSet.find(Path.parse("a"), true, true, 99).length, 1); t.is(pathSet.find(Path.parse("a"), true, true, 99).length, 1);
}); });
ava("get", (t) => { ava("get", t => {
const pathSet = new PathSet(); const pathSet = new PathSet();
pathSet.add(Path.parse("a.*")); pathSet.add(Path.parse("a.*"));
pathSet.add(Path.parse("a.a")); pathSet.add(Path.parse("a.a"));
@@ -28,7 +28,7 @@ ava("get", (t) => {
t.is(pathSet.get(Path.parse("*.a")), null); t.is(pathSet.get(Path.parse("*.a")), null);
}); });
ava("find", (t) => { ava("find", t => {
const pathSet = new PathSet(); const pathSet = new PathSet();
pathSet.add(Path.parse("a")); pathSet.add(Path.parse("a"));
pathSet.add(Path.parse("a.*")); pathSet.add(Path.parse("a.*"));
@@ -37,32 +37,32 @@ ava("find", (t) => {
pathSet.add(Path.parse("*.*")); pathSet.add(Path.parse("*.*"));
t.deepEqual( t.deepEqual(
pathSet.find(Path.parse(""), true, true, 1).map((p) => p.toString()), pathSet.find(Path.parse(""), true, true, 1).map(p => p.toString()),
["a"] ["a"]
); );
t.deepEqual( t.deepEqual(
pathSet.find(Path.parse(""), false, false, 2).map((p) => p.toString()), pathSet.find(Path.parse(""), false, false, 2).map(p => p.toString()),
["a", "a.*", "a.a", "*.a", "*.*"] ["a", "a.*", "a.a", "*.a", "*.*"]
); );
t.deepEqual( t.deepEqual(
pathSet.find(Path.parse("a.*"), false, false).map((p) => p.toString()), pathSet.find(Path.parse("a.*"), false, false).map(p => p.toString()),
["a.*"] ["a.*"]
); );
t.deepEqual( t.deepEqual(
pathSet.find(Path.parse("a.*"), false, true).map((p) => p.toString()), pathSet.find(Path.parse("a.*"), false, true).map(p => p.toString()),
["a.*", "a.a"] ["a.*", "a.a"]
); );
t.deepEqual( t.deepEqual(
pathSet.find(Path.parse("a.*"), true, false).map((p) => p.toString()), pathSet.find(Path.parse("a.*"), true, false).map(p => p.toString()),
["a.*", "*.*"] ["a.*", "*.*"]
); );
t.deepEqual( t.deepEqual(
pathSet.find(Path.parse("a.*"), true, true).map((p) => p.toString()), pathSet.find(Path.parse("a.*"), true, true).map(p => p.toString()),
["a.*", "a.a", "*.a", "*.*"] ["a.*", "a.a", "*.a", "*.*"]
); );
}); });
+3 -22
View File
@@ -1,32 +1,13 @@
import ava from "ava"; import ava from "ava";
import Path from "../lib/common/path"; import Path from "../lib/common/path";
ava("parse", (t) => { ava("toString", t => {
t.throws(() => Path.parse("."));
t.throws(() => Path.parse("a "));
t.throws(() => Path.parse(".a"));
t.throws(() => Path.parse("a."));
t.throws(() => Path.parse("a..b"));
t.throws(() => Path.parse("b*"));
t.throws(() => Path.parse("*b"));
t.throws(() => Path.parse("a.b c.d"));
t.throws(() => Path.parse("a["));
t.throws(() => Path.parse("a[b"));
t.throws(() => Path.parse("a[b:"));
t.throws(() => Path.parse('a[b:"waef]'));
t.notThrows(() => Path.parse("[]"));
t.notThrows(() => Path.parse("[a:]"));
t.notThrows(() => Path.parse("*"));
t.notThrows(() => Path.parse(""));
});
ava("toString", (t) => {
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123'); const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123'); const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
t.is(path1.toString(), path2.toString()); t.is(path1.toString(), path2.toString());
}); });
ava("slice", (t) => { ava("slice", t => {
const path = Path.parse("a.*.b.[x:y].c"); const path = Path.parse("a.*.b.[x:y].c");
const sliced = path.slice(1, -1); const sliced = path.slice(1, -1);
t.is(sliced.toString(), '*.b.[x:"y"]'); t.is(sliced.toString(), '*.b.[x:"y"]');
@@ -34,7 +15,7 @@ ava("slice", (t) => {
t.is(sliced.wildcard, 0b1); t.is(sliced.wildcard, 0b1);
}); });
ava("concat", (t) => { ava("concat", t => {
const path1 = Path.parse("a"); const path1 = Path.parse("a");
const path2 = Path.parse("*.[a:b]"); const path2 = Path.parse("*.[a:b]");
const concat = path1.concat(path2); const concat = path1.concat(path2);
+13 -13
View File
@@ -2,10 +2,10 @@ import ava from "ava";
import { import {
parseXmlDeclaration, parseXmlDeclaration,
decodeEntities, decodeEntities,
parseXml, parseXml
} from "../lib/xml-parser"; } from "../lib/xml-parser";
ava("parseXmlDeclaration", (t) => { ava("parseXmlDeclaration", t => {
const buf = Buffer.from( const buf = Buffer.from(
'<?xml version="1.0" encoding="UTF-8"?>\n<soap-env:Envelope />' '<?xml version="1.0" encoding="UTF-8"?>\n<soap-env:Envelope />'
); );
@@ -15,25 +15,25 @@ ava("parseXmlDeclaration", (t) => {
name: "version", name: "version",
namespace: "", namespace: "",
localName: "version", localName: "version",
value: "1.0", value: "1.0"
}, },
{ {
name: "encoding", name: "encoding",
namespace: "", namespace: "",
localName: "encoding", localName: "encoding",
value: "UTF-8", value: "UTF-8"
}, }
]); ]);
}); });
ava("decodeEntities", (t) => { ava("decodeEntities", t => {
t.is( t.is(
decodeEntities("&&amp;&lt;&gt;&quot;&apos;&gt;&#167;&#xd842;&#xDFB7;;"), decodeEntities("&&amp;&lt;&gt;&quot;&apos;&gt;&#167;&#xd842;&#xDFB7;;"),
"&&<>\"'>§𠮷;" "&&<>\"'>§𠮷;"
); );
}); });
ava("parse", (t) => { ava("parse", t => {
const xml = const xml =
'<?xml version="1.0"?>\n<a-b:c><d f="1<g>"/><!-- comment --><h >i</h></a-b:c>'; '<?xml version="1.0"?>\n<a-b:c><d f="1<g>"/><!-- comment --><h >i</h></a-b:c>';
const parsed = parseXml(xml); const parsed = parseXml(xml);
@@ -60,7 +60,7 @@ ava("parse", (t) => {
attrs: 'f="1<g>"', attrs: 'f="1<g>"',
text: "", text: "",
bodyIndex: 42, bodyIndex: 42,
children: [], children: []
}, },
{ {
name: "h", name: "h",
@@ -69,10 +69,10 @@ ava("parse", (t) => {
attrs: "", attrs: "",
text: "i", text: "i",
bodyIndex: 62, bodyIndex: 62,
children: [], children: []
}, }
], ]
}, }
], ]
}); });
}); });
-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));
}
});
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
/**
* 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;
});
+11 -8
View File
@@ -1,9 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["ES2019", "dom"], "lib": [
"module": "ES2020", "esnext", "dom"
],
"module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"target": "ES2019", "target": "esnext",
"resolveJsonModule": true, "resolveJsonModule": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"alwaysStrict": true, "alwaysStrict": true,
@@ -12,10 +14,11 @@
"allowJs": true "allowJs": true
}, },
"include": [ "include": [
"./bin/*.ts", "./bin/.*.ts",
"./lib/**/*", "./lib/**/*",
"./ui/**/*", "./ui/**/*",
"./test/**/*", "./test/**/*",
"./build/**/*" "./build/**/*",
"./tools/.*.ts"
] ]
} }
+2 -2
View File
@@ -22,7 +22,7 @@ import { ClosureComponent, Component } from "mithril";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: vnode => {
const active = { [vnode.attrs["page"]]: "active" }; const active = { [vnode.attrs["page"]]: "active" };
const tabs = []; const tabs = [];
@@ -97,7 +97,7 @@ const component: ClosureComponent = (): Component => {
} }
return m("nav#side-menu", m("ul", tabs)); return m("nav#side-menu", m("ul", tabs));
}, }
}; };
}; };
+28 -16
View File
@@ -37,14 +37,13 @@ import * as usersPage from "./users-page";
import Authorizer from "../lib//common/authorizer"; import Authorizer from "../lib//common/authorizer";
import * as notifications from "./notifications"; import * as notifications from "./notifications";
import { contextifyComponent } from "./components"; import { contextifyComponent } from "./components";
import { PermissionSet, UiConfig } from "../lib/types";
declare global { declare global {
interface Window { interface Window {
authorizer: Authorizer; authorizer: Authorizer;
permissionSets: PermissionSet[]; permissionSets: any;
username: string; username: string;
clientConfig: { ui: UiConfig }; clientConfig: any;
configSnapshot: string; configSnapshot: string;
genieacsVersion: string; genieacsVersion: string;
} }
@@ -59,44 +58,57 @@ const adminPages = [
"files", "files",
"config", "config",
"users", "users",
"permissions", "permissions"
]; ];
let state; let state;
let lastRenderTimestamp = 0;
let pageVisitTimestamp = 0;
let fulfillTimeout = null;
function fulfill(): void {
clearTimeout(fulfillTimeout);
fulfillTimeout = setTimeout(() => {
store.fulfill(lastRenderTimestamp, pageVisitTimestamp).then(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?.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) {
notifications.push("error", err.message); notifications.push("error", err.message);
m.route.set("/login", { continue: requestedPath }); m.route.set("/login", { continue: requestedPath });
@@ -120,7 +132,7 @@ function redirectAdminPage(): RouteResolver {
} }
} }
return null; return null;
}, }
}; };
return component; return component;
} }
@@ -142,5 +154,5 @@ m.route(document.body, "/overview", {
"/admin/files": pagify("files", filesPage), "/admin/files": pagify("files", filesPage),
"/admin/config": pagify("config", configPage), "/admin/config": pagify("config", configPage),
"/admin/users": pagify("users", usersPage), "/admin/users": pagify("users", usersPage),
"/admin/permissions": pagify("permissions", permissionsPage), "/admin/permissions": pagify("permissions", permissionsPage)
}); });
+20 -34
View File
@@ -17,21 +17,19 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
type AutocompleteCallback = (
value: string,
callback: (suggestions: { value: string; tip?: string }[]) => void
) => void;
export default class Autocomplete { export default class Autocomplete {
private declare callback: AutocompleteCallback; private callback: (
private declare element: HTMLInputElement; value: string,
private declare hideTimeout: NodeJS.Timeout; callback: (suggestions: { value: string; tip?: string }[]) => void
private declare visible: boolean; ) => void;
private declare default: string; private element: HTMLInputElement;
private declare selection: number; private hideTimeout: NodeJS.Timeout;
private declare container: HTMLElement; private visible: boolean;
private default: string;
private selection: number;
private container: HTMLElement;
public constructor(className: string, callback: AutocompleteCallback) { public constructor(className, callback) {
this.callback = callback; this.callback = callback;
this.element = null; this.element = null;
this.hideTimeout = null; this.hideTimeout = null;
@@ -46,13 +44,16 @@ export default class Autocomplete {
this.container.className = className; this.container.className = className;
} }
public attach(el: HTMLInputElement): void { public attach(el): void {
el.setAttribute("autocomplete", "off"); el.setAttribute("autocomplete", "off");
el.addEventListener("focus", () => { el.addEventListener("focus", () => {
this.element = el; this.element = el;
const domRect = el.getBoundingClientRect();
this.container.style.left = `${domRect.left + window.pageXOffset}px`;
this.container.style.width = `${domRect.width}px`;
this.container.style.top = `${domRect.bottom + window.pageYOffset}px`;
this.update(); this.update();
this.reposition();
}); });
el.addEventListener("blur", () => { el.addEventListener("blur", () => {
@@ -61,15 +62,14 @@ export default class Autocomplete {
this.hide(); this.hide();
}); });
el.addEventListener("keydown", (e) => { el.addEventListener("keydown", e => {
if (this.element !== el) return; if (this.element !== el) return;
if (e.key === "Escape") { if (e.key === "Escape") {
if (this.visible) this.hide(); if (this.visible) this.hide();
} else if (e.key === "Enter") { } else if (e.key === "Enter") {
if (this.default != null) { if (this.default != null) {
el.value = this.default; el.value = this.default;
e.stopImmediatePropagation(); e.preventDefault();
el.dispatchEvent(new InputEvent("input"));
this.update(); this.update();
} }
} else if (e.key === "ArrowDown") { } else if (e.key === "ArrowDown") {
@@ -91,19 +91,6 @@ export default class Autocomplete {
}); });
} }
public reposition(): void {
if (!this.element) return;
const domRect = this.element.getBoundingClientRect();
if (!domRect.width) {
// Element has been removed
if (this.visible) this.hide();
return;
}
this.container.style.left = `${domRect.left + window.pageXOffset}px`;
this.container.style.width = `${domRect.width}px`;
this.container.style.top = `${domRect.bottom + window.pageYOffset}px`;
}
private hide(): void { private hide(): void {
this.container.style.opacity = "0"; this.container.style.opacity = "0";
this.visible = false; this.visible = false;
@@ -121,7 +108,7 @@ export default class Autocomplete {
private update(): void { private update(): void {
const el = this.element; const el = this.element;
this.callback(el.value, (suggestions) => { this.callback(el.value, suggestions => {
if (this.element !== el) return; if (this.element !== el) return;
this.default = null; this.default = null;
@@ -167,10 +154,9 @@ export default class Autocomplete {
const t = document.createTextNode(suggestion.value); const t = document.createTextNode(suggestion.value);
e.appendChild(t); e.appendChild(t);
e.addEventListener("mousedown", (ev) => { e.addEventListener("mousedown", ev => {
ev.preventDefault(); ev.preventDefault();
el.value = suggestion.value; el.value = suggestion.value;
el.dispatchEvent(new InputEvent("input"));
if (this.element === el) this.update(); if (this.element === el) this.update();
}); });
this.container.appendChild(e); this.container.appendChild(e);
+19 -25
View File
@@ -17,23 +17,17 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { VnodeDOM, ClosureComponent } from "mithril"; import { VnodeDOM, ClosureComponent, Component } from "mithril";
import * as notifications from "./notifications"; import * as notifications from "./notifications";
import { changePassword } from "./store"; import { changePassword } from "./store";
import { m } from "./components"; import { m } from "./components";
interface Attrs { const component: ClosureComponent = (): Component => {
noAuth?: boolean;
username?: string;
onPasswordChange: () => void;
}
const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: vnode => {
const onPasswordChange = vnode.attrs.onPasswordChange; const onPasswordChange = vnode.attrs["onPasswordChange"];
const enforceAuth = !vnode.attrs.noAuth; const enforceAuth = !vnode.attrs["noAuth"];
const username = vnode.attrs.username; const username = vnode.attrs["username"];
if (username) vnode.state["username"] = username; if (username) vnode.state["username"] = username;
@@ -46,19 +40,19 @@ const component: ClosureComponent<Attrs> = () => {
type: "text", type: "text",
value: vnode.state["username"], value: vnode.state["username"],
disabled: !!username, disabled: !!username,
oninput: (e) => { oninput: e => {
vnode.state["username"] = e.target.value; vnode.state["username"] = e.target.value;
}, },
oncreate: (_vnode) => { oncreate: _vnode => {
(_vnode.dom as HTMLSelectElement).focus(); (_vnode.dom as HTMLSelectElement).focus();
}, }
}) })
), )
]; ];
let fields = { let fields = {
newPassword: "New password", newPassword: "New password",
confirmPassword: "Confirm password", confirmPassword: "Confirm password"
}; };
if (enforceAuth) if (enforceAuth)
fields = Object.assign({ authPassword: "Your password" }, fields); fields = Object.assign({ authPassword: "Your password" }, fields);
@@ -72,9 +66,9 @@ const component: ClosureComponent<Attrs> = () => {
name: f, name: f,
type: "password", type: "password",
value: vnode.state[f], value: vnode.state[f],
oninput: (e) => { oninput: e => {
vnode.state[f] = e.target.value; vnode.state[f] = e.target.value;
}, }
}) })
) )
); );
@@ -83,7 +77,7 @@ const component: ClosureComponent<Attrs> = () => {
const submit = m( const submit = m(
"button.primary", "button.primary",
{ {
type: "submit", type: "submit"
}, },
"Change password" "Change password"
) as VnodeDOM; ) as VnodeDOM;
@@ -95,7 +89,7 @@ const component: ClosureComponent<Attrs> = () => {
m( m(
"form", "form",
{ {
onsubmit: (e) => { onsubmit: e => {
e.redraw = false; e.redraw = false;
e.preventDefault(); e.preventDefault();
if ( if (
@@ -126,19 +120,19 @@ const component: ClosureComponent<Attrs> = () => {
if (onPasswordChange) onPasswordChange(); if (onPasswordChange) onPasswordChange();
(submit.dom as HTMLFormElement).disabled = false; (submit.dom as HTMLFormElement).disabled = false;
}) })
.catch((err) => { .catch(err => {
notifications.push("error", err.message); notifications.push("error", err.message);
(submit.dom as HTMLFormElement).disabled = false; (submit.dom as HTMLFormElement).disabled = false;
}); });
} }
}, }
}, },
form form
), )
]; ];
return m("div.put-form", children); return m("div.put-form", children);
}, }
}; };
}; };
+26 -39
View File
@@ -17,57 +17,44 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Editor } from "codemirror"; import { ClosureComponent, Component } from "mithril";
import { ClosureComponent } from "mithril";
import { m } from "./components"; import { m } from "./components";
import { codeMirror } from "./dynamic-loader"; import { codeMirror } from "./dynamic-loader";
interface Attrs { const component: ClosureComponent = (): Component => {
id: string;
value: string;
mode: string;
readOnly?: boolean;
focus?: boolean;
onSubmit?: (dom: Element) => void;
onChange?: (value: string) => void;
onReady?: (editor: Editor) => void;
}
const component: ClosureComponent<Attrs> = () => {
return { return {
view: (vnode) => { view: vnode => {
return m("textarea", { return m("textarea", {
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 => {
vnode.attrs.onChange(e.getValue()); vnode.attrs["onChange"](e.getValue());
}); });
} }
if (vnode.attrs.focus) editor.focus(); if (vnode.attrs["focus"]) editor.focus();
if (vnode.attrs.onReady) vnode.attrs.onReady(editor); if (vnode.attrs["onReady"]) vnode.attrs["onReady"](editor);
}, }
}); });
}, }
}; };
}; };
+13 -26
View File
@@ -22,9 +22,9 @@ import m, {
Attributes, Attributes,
Children, Children,
ComponentTypes, ComponentTypes,
CommonAttributes, Lifecycle,
ClosureComponent, ClosureComponent,
Vnode, Vnode
} from "mithril"; } from "mithril";
import parameter from "./components/parameter"; import parameter from "./components/parameter";
import parameterList from "./components/parameter-list"; import parameterList from "./components/parameter-list";
@@ -39,7 +39,6 @@ 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";
const comps = { const comps = {
parameter, parameter,
@@ -54,8 +53,7 @@ const comps = {
tags, tags,
ping, ping,
"device-link": deviceLink, "device-link": deviceLink,
"long-text": longTextComponent, "long-text": longTextComponent
loading: loading,
}; };
const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>(); const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
@@ -81,7 +79,7 @@ interface MC extends Static {
<Attrs, State>( <Attrs, State>(
ctx: Attributes, ctx: Attributes,
component: ComponentTypes<Attrs, State>, component: ComponentTypes<Attrs, State>,
attributes: Attrs & CommonAttributes<Attrs, State>, attributes: Attrs & Lifecycle<Attrs, State> & { key?: string | number },
...args: Children[] ...args: Children[]
): Vnode<Attrs, State>; ): Vnode<Attrs, State>;
}; };
@@ -98,7 +96,7 @@ const M = new Proxy(m, {
get: (target, prop) => { get: (target, prop) => {
if (prop === "context") return contextFn; if (prop === "context") return contextFn;
else return Reflect.get(target, prop); else return Reflect.get(target, prop);
}, }
}) as MC; }) as MC;
function contextFn(context, ...argumentsList): Vnode { function contextFn(context, ...argumentsList): Vnode {
@@ -116,7 +114,7 @@ function applyContext(vnode, parentContext): void {
vnodeContext.set(vnode, vc); vnodeContext.set(vnode, vc);
vnode.attrs = Object.assign({}, vc, vnode.attrs); vnode.attrs = Object.assign({}, vc, vnode.attrs);
} }
if (vnode.children?.length) if (vnode.children && vnode.children.length)
for (const c of vnode.children) applyContext(c, vc); for (const c of vnode.children) applyContext(c, vc);
} }
} }
@@ -127,32 +125,21 @@ export function contextifyComponent(component: ComponentTypes): ComponentTypes {
if (typeof component !== "function") { if (typeof component !== "function") {
c = Object.assign({}, component); c = Object.assign({}, component);
const view = component.view; const view = component.view;
c.view = function (vnode) { c.view = function(vnode) {
const context = vnodeContext.get(vnode) || {}; const context = vnodeContext.get(vnode) || {};
const res = Reflect.apply(view, this, [vnode]); const res = Reflect.apply(view, this, [vnode]);
applyContext(res, context); applyContext(res, context);
return res; return res;
}; };
} else if (!component.prototype?.view) { } else if (!component.prototype || !component.prototype.view) {
c = (initialNode) => { c = initialNode => {
const state = (component as ClosureComponent)(initialNode); const state = (component as ClosureComponent)(initialNode);
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: "Click to print stack trace to console",
onclick: () => console.error(err),
},
"Error!"
);
}
}; };
return state; return state;
}; };
+91 -138
View File
@@ -23,7 +23,6 @@ import * as taskQueue from "../task-queue";
import { parse } from "../../lib/common/expression-parser"; import { parse } from "../../lib/common/expression-parser";
import memoize from "../../lib/common/memoize"; import memoize from "../../lib/common/memoize";
import { getIcon } from "../icons"; import { getIcon } from "../icons";
import { evaluateExpression } from "../store";
const memoizedParse = memoize(parse); const memoizedParse = memoize(parse);
@@ -31,174 +30,128 @@ function escapeRegExp(str): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
} }
const keysByDepth: WeakMap<Record<string, unknown>, string[][]> = new WeakMap();
function orderKeysByDepth(device: Record<string, unknown>): string[][] {
if (keysByDepth.has(device)) return keysByDepth.get(device);
const res: string[][] = [];
for (const key of Object.keys(device)) {
let count = 0;
for (
let i = key.lastIndexOf(".", key.length - 2);
i >= 0;
i = key.lastIndexOf(".", i - 1)
)
++count;
while (res.length <= count) res.push([]);
res[count].push(key);
}
keysByDepth.set(device, res);
return res;
}
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: vnode => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const limit = evaluateExpression(vnode.attrs["limit"], device) || 100;
const search = m("input", { const search = m("input", {
type: "text", type: "text",
placeholder: "Search parameters", placeholder: "Search parameters",
oninput: (e) => { oninput: e => {
vnode.state["searchString"] = e.target.value; vnode.state["searchString"] = e.target.value;
e.redraw = false; e.redraw = false;
clearTimeout(vnode.state["timeout"]); clearTimeout(vnode.state["timeout"]);
vnode.state["timeout"] = setTimeout(m.redraw, 500); vnode.state["timeout"] = setTimeout(m.redraw, 500);
}, }
}); });
const instanceRegex = /\.[0-9]+$/; const instanceRegex = /\.[0-9]+$/;
let re; let re;
if (vnode.state["searchString"]) { if (vnode.state["searchString"]) {
const keywords = vnode.state["searchString"] const keywords = vnode.state["searchString"].split(" ").filter(s => s);
.split(" ")
.filter((s) => s);
if (keywords.length) if (keywords.length)
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i"); re = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i");
} }
const filteredKeys: string[] = []; const rows = Object.keys(device)
const allKeys = orderKeysByDepth(device); .sort()
let count = 0; .map(k => {
for (const keys of allKeys) {
let c = 0;
for (const k of keys) {
const p = device[k]; const p = device[k];
const str = p.value?.[0] ? `${k} ${p.value[0]}` : k; const val = [];
if (re && !re.test(str)) continue; const attrs = {};
++c; if (re) {
if (count < limit) filteredKeys.push(k); const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k;
} if (!re.test(str)) attrs["style"] = "display: none;";
count += c; }
}
filteredKeys.sort(); if (p.object === false) {
val.push(
m(
"parameter",
Object.assign({ device: device, parameter: memoizedParse(k) })
)
);
} else if (p.object && p.writable) {
if (instanceRegex.test(k)) {
val.push(
m(
"button",
{
title: "Delete this instance",
onclick: () => {
taskQueue.queueTask({
name: "deleteObject",
device: device["DeviceID.ID"].value[0],
objectName: k
});
}
},
getIcon("delete-instance")
)
);
} else {
val.push(
m(
"button",
{
title: "Create a new instance",
onclick: () => {
taskQueue.queueTask({
name: "addObject",
device: device["DeviceID.ID"].value[0],
objectName: k
});
}
},
getIcon("add-instance")
)
);
}
}
const rows = filteredKeys.map((k) => {
const p = device[k];
const val = [];
const attrs = { key: k };
if (p.object === false) {
val.push( val.push(
m( m(
"parameter", "button",
Object.assign({ device: device, parameter: memoizedParse(k) }) {
title: "Refresh tree",
onclick: () => {
taskQueue.queueTask({
name: "getParameterValues",
device: device["DeviceID.ID"].value[0],
parameterNames: [k]
});
}
},
getIcon("refresh")
) )
); );
} else if (p.object && p.writable) {
if (instanceRegex.test(k)) {
val.push(
m(
"button",
{
title: "Delete this instance",
onclick: () => {
taskQueue.queueTask({
name: "deleteObject",
device: device["DeviceID.ID"].value[0],
objectName: k,
});
},
},
getIcon("delete-instance")
)
);
} else {
val.push(
m(
"button",
{
title: "Create a new instance",
onclick: () => {
taskQueue.queueTask({
name: "addObject",
device: device["DeviceID.ID"].value[0],
objectName: k,
});
},
},
getIcon("add-instance")
)
);
}
}
val.push( return m(
m( "tr",
"button", attrs,
{ m("td.left", m("long-text", { text: k })),
title: "Refresh tree", m("td.right", val)
onclick: () => { );
taskQueue.queueTask({ });
name: "getParameterValues",
device: device["DeviceID.ID"].value[0],
parameterNames: [k],
});
},
},
getIcon("refresh")
)
);
return m(
"tr",
attrs,
m("td.left", m("long-text", { text: k })),
m("td.right", val)
);
});
return m( return m(
"loading", ".all-parameters",
{ queries: [vnode.attrs["deviceQuery"]] },
m( m(
".all-parameters", "a.download-csv",
m( {
"a.download-csv", href: `/api/devices/${encodeURIComponent(
{ device["DeviceID.ID"].value[0]
href: `api/devices/${encodeURIComponent( )}.csv`,
device["DeviceID.ID"].value[0] download: "",
)}.csv`, style: "float: right;"
download: "", },
style: "float: right;", "Download"
}, ),
"Download" search,
), m(".parameter-list", m("table", m("tbody", rows)))
search,
m(
".parameter-list",
m("table", m("tbody", rows)),
m(
"m",
`Displaying ${filteredKeys.length} out of ${count} parameters.`
)
)
)
); );
}, }
}; };
}; };
+15 -62
View File
@@ -17,77 +17,30 @@
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>. * along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Attributes, ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { map } from "../../lib/common/expression-parser";
import memoize from "../../lib/common/memoize";
import { Expression } from "../../lib/types";
import { m } from "../components"; import { m } from "../components";
import { evaluateExpression, getTimestamp } from "../store"; import * as store from "../store";
const evaluateAttributes = memoize(
(
attrs: Record<string, Expression>,
obj: Record<string, unknown>,
now: number // eslint-disable-line @typescript-eslint/no-unused-vars
): Attributes => {
const res: Attributes = {};
for (const [k, v] of Object.entries(attrs)) {
const vv = map(v, (e) => {
if (
Array.isArray(e) &&
e[0] === "FUNC" &&
e[1] === "ENCODEURICOMPONENT"
) {
const a = evaluateExpression(e[2], obj);
if (a == null) return null;
return encodeURIComponent(a as string);
}
return e;
});
res[k] = evaluateExpression(vv, obj);
}
return res;
}
);
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: vnode => {
const device = vnode.attrs["device"]; if (vnode.attrs["filter"]) {
if ("filter" in vnode.attrs) { if (
if (!evaluateExpression(vnode.attrs["filter"], device || {})) !store.evaluateExpression(
vnode.attrs["filter"],
vnode.attrs["device"]
)
)
return null; return null;
} }
const children = Object.values(vnode.attrs["components"]).map((c) => { const children = Object.values(vnode.attrs["components"]).map(c => {
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
if (typeof c !== "object") return `${c}`; if (typeof c !== "object") return `${c}`;
const type = evaluateExpression(c["type"], device || {}); return m(c["type"], c);
if (!type) return null;
return m(type as string, c);
}); });
if (vnode.attrs["element"]) return m(vnode.attrs["element"], children);
let el = vnode.attrs["element"]; else return children;
}
if (el == null) return children;
let attrs: Attributes;
if (Array.isArray(el)) {
el = evaluateExpression(el, device || {});
} else if (typeof el === "object") {
if (el["attributes"] != null) {
attrs = evaluateAttributes(
el["attributes"],
device || {},
getTimestamp()
);
}
el = evaluateExpression(el["tag"], device || {});
}
return m(el, attrs, children);
},
}; };
}; };
+10 -10
View File
@@ -25,7 +25,7 @@ import * as store from "../store";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: vnode => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const buttons = []; const buttons = [];
@@ -38,9 +38,9 @@ const component: ClosureComponent = (): Component => {
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "reboot", name: "reboot",
device: device["DeviceID.ID"].value[0], device: device["DeviceID.ID"].value[0]
}); });
}, }
}, },
"Reboot" "Reboot"
) )
@@ -54,9 +54,9 @@ const component: ClosureComponent = (): Component => {
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "factoryReset", name: "factoryReset",
device: device["DeviceID.ID"].value[0], device: device["DeviceID.ID"].value[0]
}); });
}, }
}, },
"Reset" "Reset"
) )
@@ -70,9 +70,9 @@ const component: ClosureComponent = (): Component => {
onclick: () => { onclick: () => {
taskQueue.stageDownload({ taskQueue.stageDownload({
name: "download", name: "download",
devices: [device["DeviceID.ID"].value[0]], device: device["DeviceID.ID"].value[0]
}); });
}, }
}, },
"Push file" "Push file"
) )
@@ -93,17 +93,17 @@ const component: ClosureComponent = (): Component => {
notifications.push("success", `${deviceId}: Device deleted`); notifications.push("success", `${deviceId}: Device deleted`);
m.route.set("/devices"); m.route.set("/devices");
}) })
.catch((err) => { .catch(err => {
notifications.push("error", err.message); notifications.push("error", err.message);
}); });
}, }
}, },
"Delete" "Delete"
) )
); );
return m(".actions-bar", buttons); return m(".actions-bar", buttons);
}, }
}; };
}; };
+14 -25
View File
@@ -22,27 +22,25 @@ 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 {
view: (vnode) => { view: vnode => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const deviceId = device["DeviceID.ID"].value[0]; const deviceId = device["DeviceID.ID"].value[0];
const faults = store.fetch("faults", [ const faults = store.fetch("faults", [
"AND", "AND",
[">", ["PARAM", "_id"], `${deviceId}:`], [">", ["PARAM", "_id"], `${deviceId}:`],
["<", ["PARAM", "_id"], `${deviceId}:zzzz`], ["<", ["PARAM", "_id"], `${deviceId}:zzzz`]
]); ]);
const headers = [ const headers = [
"Channel", "Channel",
"Code", "Code",
"Message", "Message",
"Detail",
"Retries", "Retries",
"Timestamp", "Timestamp"
].map((l) => m("th", l)); ].map(l => m("th", l));
const thead = m("thead", m("tr", headers)); const thead = m("thead", m("tr", headers));
const rows = []; const rows = [];
@@ -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(
@@ -61,33 +57,30 @@ const component: ClosureComponent = (): Component => {
"button", "button",
{ {
title: "Delete fault", title: "Delete fault",
onclick: (e) => { onclick: e => {
e.redraw = false; e.redraw = false;
store store
.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());
}); });
}, }
}, },
getIcon("remove") getIcon("remove")
) )
), )
]); ]);
} }
let tbody; let tbody;
if (rows.length) { if (rows.length) {
tbody = m( tbody = m("tbody", rows.map(r => m("tr", r)));
"tbody",
rows.map((r) => m("tr", r))
);
} else { } else {
tbody = m( tbody = m(
"tbody", "tbody",
@@ -95,12 +88,8 @@ const component: ClosureComponent = (): Component => {
); );
} }
return m( return m("table.table", thead, tbody);
"loading", }
{ queries: [faults] },
m("table.table", thead, tbody)
);
},
}; };
}; };
+6 -10
View File
@@ -19,22 +19,18 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import { evaluateExpression } from "../store";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: vnode => {
let deviceId; let deviceId;
const device = vnode.attrs["device"]; if (vnode.attrs["device"])
if (device) deviceId = device["DeviceID.ID"].value[0]; deviceId = vnode.attrs["device"]["DeviceID.ID"].value[0];
const children = Object.values(vnode.attrs["components"]).map((c) => { const children = Object.values(vnode.attrs["components"]).map(c => {
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
if (typeof c !== "object") return `${c}`; if (typeof c !== "object") return `${c}`;
const type = evaluateExpression(c["type"], device || {});
if (!type) return null;
const attrs = Object.assign({}, vnode.attrs, c); const attrs = Object.assign({}, vnode.attrs, c);
return m(type as string, attrs); return m(attrs["type"], attrs);
}); });
if (deviceId) { if (deviceId) {
return m( return m(
@@ -45,7 +41,7 @@ const component: ClosureComponent = (): Component => {
} else { } else {
return children; return children;
} }
}, }
}; };
}; };
-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;
+7 -8
View File
@@ -20,40 +20,39 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import config from "../config"; import config from "../config";
import { evaluateExpression } from "../store"; import * as store from "../store";
const CHARTS = config.ui.overview.charts; const CHARTS = config.ui.overview.charts;
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: vnode => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const chartName = evaluateExpression(vnode.attrs["chart"], device || {}); const chart = CHARTS[vnode.attrs["chart"]];
const chart = CHARTS[chartName as string] as Record<string, unknown>;
if (!chart) return null; if (!chart) return null;
for (const slice of Object.values(chart.slices)) { for (const slice of Object.values(chart.slices)) {
const filter = slice["filter"]; const filter = slice["filter"];
if (evaluateExpression(filter, device || {})) { if (store.evaluateExpression(filter, device)) {
const dot = m( const dot = m(
"svg", "svg",
{ {
width: "1em", width: "1em",
height: "1em", height: "1em",
xmlns: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink", "xmlns:xlink": "http://www.w3.org/1999/xlink"
}, },
m("circle", { m("circle", {
cx: "0.5em", cx: "0.5em",
cy: "0.5em", cy: "0.5em",
r: "0.4em", r: "0.4em",
fill: slice["color"], fill: slice["color"]
}) })
); );
return m("span.overview-dot", dot, `${slice["label"]}`); return m("span.overview-dot", dot, `${slice["label"]}`);
} }
} }
return null; return null;
}, }
}; };
}; };
+8 -19
View File
@@ -19,49 +19,38 @@
import { ClosureComponent, Component, VnodeDOM } from "mithril"; import { ClosureComponent, Component, VnodeDOM } from "mithril";
import { m } from "../components"; import { m } from "../components";
import { evaluateExpression } from "../store";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
view: (vnode) => { view: vnode => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const rows = Object.values(vnode.attrs["parameters"]).map((parameter) => { const rows = Object.values(vnode.attrs["parameters"]).map(parameter => {
const type = evaluateExpression(parameter["type"], device);
const p = m.context( const p = m.context(
{ {
device: device, device: device,
parameter: parameter["parameter"], parameter: parameter["parameter"]
}, },
(type as string) || "parameter", parameter["type"] || "parameter",
parameter parameter
); );
return m( return m(
"tr", "tr",
{ {
oncreate: (vn) => { onupdate: vn => {
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom (vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
? "" ? ""
: "none"; : "none";
}, }
onupdate: (vn) => {
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
? ""
: "none";
},
}, },
m("th", parameter["label"]), m("th", parameter["label"]),
m("td", p) m("td", p)
); );
}); });
return m( return m("table.parameter-list", rows);
"loading", }
{ queries: [vnode.attrs["deviceQuery"]] },
m("table.parameter-list", rows)
);
},
}; };
}; };
+22 -30
View File
@@ -20,27 +20,27 @@
import { ClosureComponent, Component } from "mithril"; import { ClosureComponent, Component } from "mithril";
import { m } from "../components"; import { m } from "../components";
import * as taskQueue from "../task-queue"; import * as taskQueue from "../task-queue";
import { evaluateExpression } from "../store"; import * as store from "../store";
import * as expressionParser from "../../lib/common/expression-parser"; import * as expressionParser from "../../lib/common/expression-parser";
import { getIcon } from "../icons"; import { getIcon } from "../icons";
const component: ClosureComponent = (): Component => { const component: ClosureComponent = (): Component => {
return { return {
oninit: (vnode) => { oninit: vnode => {
const obj = vnode.attrs["parameter"]; const obj = vnode.attrs["parameter"];
if (!Array.isArray(obj) || obj[0] !== "PARAM") if (!Array.isArray(obj) || obj[0] !== "PARAM")
throw new Error("Object must be a parameter path"); throw new Error("Object must be a parameter path");
vnode.state["object"] = obj[1]; vnode.state["object"] = obj[1];
vnode.state["parameters"] = Object.values(vnode.attrs["childParameters"]); vnode.state["parameters"] = Object.values(vnode.attrs["childParameters"]);
}, },
view: (vnode) => { view: vnode => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const object = evaluateExpression(vnode.state["object"], device); const object = store.evaluateExpression(vnode.state["object"], device);
const parameters = vnode.state["parameters"]; const parameters = vnode.state["parameters"];
if (typeof object !== "string" || !device[object]) return null; if (!device[object]) return null;
const instances: Set<string> = new Set(); const instances = new Set();
const prefix = `${object}.`; const prefix = `${object}.`;
for (const p in device) { for (const p in device) {
if (p.startsWith(prefix)) { if (p.startsWith(prefix)) {
@@ -50,24 +50,25 @@ const component: ClosureComponent = (): Component => {
} }
} }
const headers = Object.values(parameters).map((p) => m("th", p["label"])); const headers = Object.values(parameters).map(p => m("th", p["label"]));
const thead = m("thead", m("tr", headers)); const thead = m("thead", m("tr", headers));
const rows = []; const rows = [];
for (const i of instances) { for (const i of instances) {
let filter = "filter" in vnode.attrs ? vnode.attrs["filter"] : true; let filter =
vnode.attrs["filter"] != null ? vnode.attrs["filter"] : true;
filter = expressionParser.map(filter, (e) => { filter = expressionParser.map(filter, e => {
if (Array.isArray(e) && e[0] === "PARAM") if (Array.isArray(e) && e[0] === "PARAM")
return ["PARAM", ["||", i, ".", e[1]]]; return ["PARAM", ["||", i, ".", e[1]]];
return e; return e;
}); });
if (!evaluateExpression(filter, device)) continue; if (!store.evaluateExpression(filter, device)) continue;
const row = parameters.map((p) => { const row = parameters.map(p => {
const param = expressionParser.map(p.parameter, (e) => { const param = expressionParser.map(p.parameter, e => {
if (Array.isArray(e) && e[0] === "PARAM") if (Array.isArray(e) && e[0] === "PARAM")
return ["PARAM", ["||", i, ".", e[1]]]; return ["PARAM", ["||", i, ".", e[1]]];
return e; return e;
@@ -77,13 +78,13 @@ const component: ClosureComponent = (): Component => {
m.context( m.context(
{ {
device: device, device: device,
parameter: param, parameter: param
}, },
p.type || "parameter", p.type || "parameter",
Object.assign({}, p, { Object.assign({}, p, {
device: device, device: device,
parameter: param, parameter: param,
label: null, label: null
}) })
) )
); );
@@ -101,9 +102,9 @@ const component: ClosureComponent = (): Component => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "deleteObject", name: "deleteObject",
device: device["DeviceID.ID"].value[0], device: device["DeviceID.ID"].value[0],
objectName: i, objectName: i
}); });
}, }
}, },
getIcon("delete-instance") getIcon("delete-instance")
) )
@@ -134,9 +135,9 @@ const component: ClosureComponent = (): Component => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "addObject", name: "addObject",
device: device["DeviceID.ID"].value[0], device: device["DeviceID.ID"].value[0],
objectName: object, objectName: object
}); });
}, }
}, },
getIcon("add-instance") getIcon("add-instance")
) )
@@ -146,19 +147,10 @@ const component: ClosureComponent = (): Component => {
} }
let label; let label;
if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
const l = evaluateExpression(vnode.attrs["label"], device); return [label, m("table.table", thead, m("tbody", rows))];
if (l != null) label = m("h2", l); }
return [
label,
m(
"loading",
{ queries: [vnode.attrs["deviceQuery"]] },
m("table.table", thead, m("tbody", rows))
),
];
},
}; };
}; };
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff. Show more