Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6733cef5dd | ||
|
|
96add7efe2 | ||
|
|
d311dad861 | ||
|
|
4a55f56e48 | ||
|
|
601690e3b5 |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
!/bin/.*.ts
|
||||||
|
!/tools/.*.ts
|
||||||
+9
-18
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
"es2022": true,
|
"es6": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
@@ -8,29 +8,20 @@
|
|||||||
"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"],
|
||||||
"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",
|
||||||
"@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 }
|
"require-atomic-updates": 0,
|
||||||
],
|
|
||||||
"@typescript-eslint/explicit-function-return-type": [
|
|
||||||
"error",
|
|
||||||
{ "allowExpressions": true }
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
|
||||||
"@typescript-eslint/no-misused-promises": "error",
|
|
||||||
"no-prototype-builtins": 0
|
"no-prototype-builtins": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
*~
|
*~
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
bin/.*.ts
|
||||||
|
tools/.*.ts
|
||||||
docs/_build
|
docs/_build
|
||||||
+25
@@ -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/.*
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
dist
|
|
||||||
npm-shrinkwrap.json
|
|
||||||
+52
-260
@@ -1,254 +1,48 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
## 1.2.10 (2024-03-18)
|
|
||||||
|
|
||||||
- Add support for XMPP connection requests. Use the environment variables
|
|
||||||
`XMPP_JID` and `XMPP_PASSWORD` to configure the XMPP connection for the ACS.
|
|
||||||
|
|
||||||
- The environment variables `CWMP_SSL_CERT` and `CWMP_SSL_KEY`, as well as their
|
|
||||||
counterparts for UI, NBI, and FS, now accept PEM-encoded certificates and keys
|
|
||||||
in addition to file paths.
|
|
||||||
|
|
||||||
- The UI no longer requires users to refresh the page after modifying presets,
|
|
||||||
provisions, or virtual parameters. Refreshing is now only necessary for
|
|
||||||
changes to users, permissions, or UI configurations.
|
|
||||||
|
|
||||||
- Improved conversion of GenieACS expressions into MongoDB queries for more
|
|
||||||
optimized queries and better index utilization.
|
|
||||||
|
|
||||||
- Refactor UI pagination and sorting to fix issues from the previous approach,
|
|
||||||
especially with sorting by rapidly changing parameters such as 'last inform'
|
|
||||||
time.
|
|
||||||
|
|
||||||
- The file server now supports HEAD requests, the Range header, and conditional
|
|
||||||
requests.
|
|
||||||
|
|
||||||
- Addressed an issue causing file server disconnections for slow clients over
|
|
||||||
HTTPS.
|
|
||||||
|
|
||||||
- Fix bug preventing users from changing their own passwords.
|
|
||||||
|
|
||||||
- Introduced a new 'locks' collection for database locking, replacing the
|
|
||||||
previous use of the 'cache' collection for this purpose.
|
|
||||||
|
|
||||||
- Various other minor fixes and enhancements.
|
|
||||||
|
|
||||||
## 1.2.9 (2022-08-22)
|
|
||||||
|
|
||||||
- New config option `cwmp.skipRootGpn` to enable a workaround for some
|
|
||||||
problematic CPEs that reject GPN requests on data model root.
|
|
||||||
- Stream query results and CSV downloads as data becomes available instead of
|
|
||||||
buffering the entire response.
|
|
||||||
- Log HTTP/HTTPS client errors in debug log.
|
|
||||||
- Fix occasional lock expired errors after updating presets, etc.
|
|
||||||
- Fix bug where queries containing `<>` operator may return incorrect results.
|
|
||||||
- Fix a performance hit caused by DB calls containing the entire CPE data model
|
|
||||||
rather then just the updated parameters.
|
|
||||||
- Fix bug where tags containing special characters are saved in their encoded
|
|
||||||
form when set via a Provision script.
|
|
||||||
- Fix issue causing invalid `xsd:dateTime` values to be saved in DB as `NaN`
|
|
||||||
rather than maintain the original string value.
|
|
||||||
- Fix bug using `$regex` operator with a numeric or a datetime string.
|
|
||||||
- Fix ping not working on certain platforms.
|
|
||||||
- Ping requests are now authenticated.
|
|
||||||
- Fix error when the `FORWARDED_HEADER` environment variable contains IPv4 CIDR
|
|
||||||
while the listening interface is IPv6 and vice versa.
|
|
||||||
- Fix false warning "unexpected parameter in response" showing in
|
|
||||||
`genieacs-cwmp` logs.
|
|
||||||
- Other minor fixes and stability improvements.
|
|
||||||
|
|
||||||
## 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.
|
||||||
@@ -258,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.
|
||||||
@@ -328,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
@@ -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
|
|
||||||
@@ -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).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
"compileEnhancements": false,
|
||||||
|
"extensions": ["ts"],
|
||||||
|
"require": ["esm", "ts-node/register/transpile-only"]
|
||||||
|
};
|
||||||
|
|
||||||
Executable
+134
@@ -0,0 +1,134 @@
|
|||||||
|
#!/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 config from "../lib/config";
|
||||||
|
import * as logger from "../lib/logger";
|
||||||
|
import * as cluster from "../lib/cluster";
|
||||||
|
import * as server from "../lib/server";
|
||||||
|
import * as cwmp from "../lib/cwmp";
|
||||||
|
import * as db from "../lib/db";
|
||||||
|
import * as extensions from "../lib/extensions";
|
||||||
|
import * as cache from "../lib/cache";
|
||||||
|
import { version as VERSION } from "../package.json";
|
||||||
|
|
||||||
|
logger.init("cwmp", VERSION);
|
||||||
|
|
||||||
|
const SERVICE_ADDRESS = config.get("CWMP_INTERFACE");
|
||||||
|
const SERVICE_PORT = config.get("CWMP_PORT");
|
||||||
|
|
||||||
|
function exitWorkerGracefully(): void {
|
||||||
|
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||||
|
Promise.all([
|
||||||
|
db.disconnect(),
|
||||||
|
cache.disconnect(),
|
||||||
|
extensions.killAll(),
|
||||||
|
cluster.worker.disconnect()
|
||||||
|
]).catch(exitWorkerUngracefully);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWorkerUngracefully(): void {
|
||||||
|
extensions.killAll().then(() => {
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cluster.worker) {
|
||||||
|
const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES");
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
message: `genieacs-cwmp starting`,
|
||||||
|
pid: process.pid,
|
||||||
|
version: VERSION
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGINT, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGTERM, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const ssl = {
|
||||||
|
key: config.get("CWMP_SSL_KEY"),
|
||||||
|
cert: config.get("CWMP_SSL_CERT")
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("uncaughtException", err => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
|
logger.error({
|
||||||
|
message: "Uncaught exception",
|
||||||
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initPromise = Promise.all([db.connect(), cache.connect()])
|
||||||
|
.then(() => {
|
||||||
|
server.start(
|
||||||
|
SERVICE_PORT,
|
||||||
|
SERVICE_ADDRESS,
|
||||||
|
ssl,
|
||||||
|
cwmp.listener,
|
||||||
|
cwmp.onConnection,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
setTimeout(() => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import * as config from "../lib/config.ts";
|
|
||||||
import * as logger from "../lib/logger.ts";
|
|
||||||
import * as cluster from "../lib/cluster.ts";
|
|
||||||
import * as server from "../lib/server.ts";
|
|
||||||
import * as cwmp from "../lib/cwmp.ts";
|
|
||||||
import * as db from "../lib/db/db.ts";
|
|
||||||
import * as extensions from "../lib/extensions.ts";
|
|
||||||
import { version as VERSION } from "../package.json";
|
|
||||||
|
|
||||||
logger.init("cwmp", VERSION);
|
|
||||||
|
|
||||||
const SERVICE_ADDRESS = config.get("CWMP_INTERFACE") as string;
|
|
||||||
const SERVICE_PORT = config.get("CWMP_PORT") as number;
|
|
||||||
|
|
||||||
function exitWorkerGracefully(): void {
|
|
||||||
setTimeout(exitWorkerUngracefully, 5000).unref();
|
|
||||||
Promise.all([
|
|
||||||
db.disconnect(),
|
|
||||||
extensions.killAll(),
|
|
||||||
cluster.worker.disconnect(),
|
|
||||||
])
|
|
||||||
.then(logger.close)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitWorkerUngracefully(): void {
|
|
||||||
void extensions.killAll().finally(() => {
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cluster.worker) {
|
|
||||||
const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES") as number;
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
message: `genieacs-cwmp starting`,
|
|
||||||
pid: process.pid,
|
|
||||||
version: VERSION,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
logger.info({
|
|
||||||
message: "Received signal SIGINT, exiting",
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
logger.info({
|
|
||||||
message: "Received signal SIGTERM, exiting",
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.stop();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const key = config.get("CWMP_SSL_KEY") as string;
|
|
||||||
const cert = config.get("CWMP_SSL_CERT") as string;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
port: SERVICE_PORT,
|
|
||||||
host: SERVICE_ADDRESS,
|
|
||||||
ssl: key && cert ? { key, cert } : null,
|
|
||||||
onConnection: cwmp.onConnection,
|
|
||||||
onClientError: cwmp.onClientError,
|
|
||||||
timeout: 30000,
|
|
||||||
keepAliveTimeout: 0,
|
|
||||||
requestTimeout: cwmp.REQUEST_TIMEOUT,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Need this for Node < 15
|
|
||||||
process.on("unhandledRejection", (err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
|
||||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
|
||||||
logger.error({
|
|
||||||
message: "Uncaught exception",
|
|
||||||
exception: err,
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
server.stop(false).then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
const initPromise = db
|
|
||||||
.connect()
|
|
||||||
.then(() => {
|
|
||||||
server.start(options, cwmp.listener);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
void initPromise.finally(() => {
|
|
||||||
server
|
|
||||||
.stop(false)
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
void initPromise.finally(() => {
|
|
||||||
server
|
|
||||||
.stop(false)
|
|
||||||
.then(exitWorkerGracefully)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,25 @@
|
|||||||
import { Fault } from "../lib/types.ts";
|
#!/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 { Fault } from "../lib/types";
|
||||||
|
|
||||||
const jobs = new Set();
|
const jobs = new Set();
|
||||||
const fileName = process.argv[2];
|
const fileName = process.argv[2];
|
||||||
@@ -14,20 +35,20 @@ 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) {
|
||||||
fault.detail["stack"] = err.stack;
|
fault.detail["stack"] = err.stack;
|
||||||
// Trim the stack trace
|
// Trim the stack trace
|
||||||
const stackTrimIndex = fault.detail["stack"].match(
|
const stackTrimIndex = fault.detail["stack"].match(
|
||||||
/\s+at\s[^\s]+\s\(.*genieacs-ext:.+\)/,
|
/\s+at\s[^\s]+\s\(.*genieacs-ext:.+\)/
|
||||||
);
|
);
|
||||||
if (stackTrimIndex) {
|
if (stackTrimIndex) {
|
||||||
fault.detail["stack"] = fault.detail["stack"].slice(
|
fault.detail["stack"] = fault.detail["stack"].slice(
|
||||||
0,
|
0,
|
||||||
stackTrimIndex.index,
|
stackTrimIndex.index
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,21 +56,16 @@ function errorToFault(err: Error): Fault {
|
|||||||
return fault;
|
return fault;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need this for Node < 15
|
process.on("uncaughtException", err => {
|
||||||
process.on("unhandledRejection", (err) => {
|
|
||||||
throw 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) {
|
||||||
@@ -63,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;
|
||||||
@@ -77,6 +93,4 @@ process.on("message", (message) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Ignore SIGINT
|
// Ignore SIGINT
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {});
|
||||||
// Ignore
|
|
||||||
});
|
|
||||||
Executable
+123
@@ -0,0 +1,123 @@
|
|||||||
|
#!/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 config from "../lib/config";
|
||||||
|
import * as logger from "../lib/logger";
|
||||||
|
import * as cluster from "../lib/cluster";
|
||||||
|
import * as server from "../lib/server";
|
||||||
|
import { listener } from "../lib/fs";
|
||||||
|
import * as db from "../lib/db";
|
||||||
|
import * as cache from "../lib/cache";
|
||||||
|
import { version as VERSION } from "../package.json";
|
||||||
|
|
||||||
|
logger.init("fs", VERSION);
|
||||||
|
|
||||||
|
const SERVICE_ADDRESS = config.get("FS_INTERFACE");
|
||||||
|
const SERVICE_PORT = config.get("FS_PORT");
|
||||||
|
|
||||||
|
function exitWorkerGracefully(): void {
|
||||||
|
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||||
|
Promise.all([
|
||||||
|
db.disconnect(),
|
||||||
|
cache.disconnect(),
|
||||||
|
cluster.worker.disconnect()
|
||||||
|
]).catch(exitWorkerUngracefully);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWorkerUngracefully(): void {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cluster.worker) {
|
||||||
|
const WORKER_COUNT = config.get("FS_WORKER_PROCESSES");
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
message: `genieacs-fs starting`,
|
||||||
|
pid: process.pid,
|
||||||
|
version: VERSION
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGINT, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGTERM, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const ssl = {
|
||||||
|
key: config.get("FS_SSL_KEY"),
|
||||||
|
cert: config.get("FS_SSL_CERT")
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("uncaughtException", err => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
|
logger.error({
|
||||||
|
message: "Uncaught exception",
|
||||||
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initPromise = Promise.all([db.connect(), cache.connect()])
|
||||||
|
.then(() => {
|
||||||
|
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
setTimeout(() => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import * as config from "../lib/config.ts";
|
|
||||||
import * as logger from "../lib/logger.ts";
|
|
||||||
import * as cluster from "../lib/cluster.ts";
|
|
||||||
import * as server from "../lib/server.ts";
|
|
||||||
import { listener } from "../lib/fs.ts";
|
|
||||||
import * as db from "../lib/db/db.ts";
|
|
||||||
import { version as VERSION } from "../package.json";
|
|
||||||
|
|
||||||
logger.init("fs", VERSION);
|
|
||||||
|
|
||||||
const SERVICE_ADDRESS = config.get("FS_INTERFACE") as string;
|
|
||||||
const SERVICE_PORT = config.get("FS_PORT") as number;
|
|
||||||
|
|
||||||
function exitWorkerGracefully(): void {
|
|
||||||
setTimeout(exitWorkerUngracefully, 5000).unref();
|
|
||||||
Promise.all([db.disconnect(), cluster.worker.disconnect()])
|
|
||||||
.then(logger.close)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitWorkerUngracefully(): void {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cluster.worker) {
|
|
||||||
const WORKER_COUNT = config.get("FS_WORKER_PROCESSES") as number;
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
message: `genieacs-fs starting`,
|
|
||||||
pid: process.pid,
|
|
||||||
version: VERSION,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
logger.info({
|
|
||||||
message: "Received signal SIGINT, exiting",
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
logger.info({
|
|
||||||
message: "Received signal SIGTERM, exiting",
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.stop();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const key = config.get("FS_SSL_KEY") as string;
|
|
||||||
const cert = config.get("FS_SSL_CERT") as string;
|
|
||||||
const options = {
|
|
||||||
port: SERVICE_PORT,
|
|
||||||
host: SERVICE_ADDRESS,
|
|
||||||
ssl: key && cert ? { key, cert } : null,
|
|
||||||
timeout: 30000,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Need this for Node < 15
|
|
||||||
process.on("unhandledRejection", (err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
|
||||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
|
||||||
logger.error({
|
|
||||||
message: "Uncaught exception",
|
|
||||||
exception: err,
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
const initPromise = db
|
|
||||||
.connect()
|
|
||||||
.then(() => {
|
|
||||||
server.start(options, listener);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
void initPromise.finally(() => {
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
void initPromise.finally(() => {
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Executable
+127
@@ -0,0 +1,127 @@
|
|||||||
|
#!/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 config from "../lib/config";
|
||||||
|
import * as logger from "../lib/logger";
|
||||||
|
import * as cluster from "../lib/cluster";
|
||||||
|
import * as server from "../lib/server";
|
||||||
|
import { listener } from "../lib/nbi";
|
||||||
|
import * as db from "../lib/db";
|
||||||
|
import * as extensions from "../lib/extensions";
|
||||||
|
import * as cache from "../lib/cache";
|
||||||
|
import { version as VERSION } from "../package.json";
|
||||||
|
|
||||||
|
logger.init("nbi", VERSION);
|
||||||
|
|
||||||
|
const SERVICE_ADDRESS = config.get("NBI_INTERFACE");
|
||||||
|
const SERVICE_PORT = config.get("NBI_PORT");
|
||||||
|
|
||||||
|
function exitWorkerGracefully(): void {
|
||||||
|
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||||
|
Promise.all([
|
||||||
|
db.disconnect(),
|
||||||
|
cache.disconnect(),
|
||||||
|
extensions.killAll(),
|
||||||
|
cluster.worker.disconnect()
|
||||||
|
]).catch(exitWorkerUngracefully);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWorkerUngracefully(): void {
|
||||||
|
extensions.killAll().then(() => {
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cluster.worker) {
|
||||||
|
const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES");
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
message: `genieacs-nbi starting`,
|
||||||
|
pid: process.pid,
|
||||||
|
version: VERSION
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGINT, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGTERM, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const ssl = {
|
||||||
|
key: config.get("NBI_SSL_KEY"),
|
||||||
|
cert: config.get("NBI_SSL_CERT")
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on("uncaughtException", err => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
|
logger.error({
|
||||||
|
message: "Uncaught exception",
|
||||||
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initPromise = Promise.all([db.connect(), cache.connect()])
|
||||||
|
.then(() => {
|
||||||
|
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
setTimeout(() => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import * as config from "../lib/config.ts";
|
|
||||||
import * as logger from "../lib/logger.ts";
|
|
||||||
import * as cluster from "../lib/cluster.ts";
|
|
||||||
import * as server from "../lib/server.ts";
|
|
||||||
import { listener } from "../lib/nbi.ts";
|
|
||||||
import * as db from "../lib/db/db.ts";
|
|
||||||
import * as extensions from "../lib/extensions.ts";
|
|
||||||
import { version as VERSION } from "../package.json";
|
|
||||||
|
|
||||||
logger.init("nbi", VERSION);
|
|
||||||
|
|
||||||
const SERVICE_ADDRESS = config.get("NBI_INTERFACE") as string;
|
|
||||||
const SERVICE_PORT = config.get("NBI_PORT") as number;
|
|
||||||
|
|
||||||
function exitWorkerGracefully(): void {
|
|
||||||
setTimeout(exitWorkerUngracefully, 5000).unref();
|
|
||||||
Promise.all([
|
|
||||||
db.disconnect(),
|
|
||||||
extensions.killAll(),
|
|
||||||
cluster.worker.disconnect(),
|
|
||||||
])
|
|
||||||
.then(logger.close)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitWorkerUngracefully(): void {
|
|
||||||
void extensions.killAll().finally(() => {
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cluster.worker) {
|
|
||||||
const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES") as number;
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
message: `genieacs-nbi starting`,
|
|
||||||
pid: process.pid,
|
|
||||||
version: VERSION,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
logger.info({
|
|
||||||
message: "Received signal SIGINT, exiting",
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
logger.info({
|
|
||||||
message: "Received signal SIGTERM, exiting",
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.stop();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const key = config.get("NBI_SSL_KEY") as string;
|
|
||||||
const cert = config.get("NBI_SSL_CERT") as string;
|
|
||||||
const options = {
|
|
||||||
port: SERVICE_PORT,
|
|
||||||
host: SERVICE_ADDRESS,
|
|
||||||
ssl: key && cert ? { key, cert } : null,
|
|
||||||
timeout: 30000,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Need this for Node < 15
|
|
||||||
process.on("unhandledRejection", (err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
|
||||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
|
||||||
logger.error({
|
|
||||||
message: "Uncaught exception",
|
|
||||||
exception: err,
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
const initPromise = db
|
|
||||||
.connect()
|
|
||||||
.then(() => {
|
|
||||||
server.start(options, listener);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
void initPromise.finally(() => {
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
void initPromise.finally(() => {
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Executable
+139
@@ -0,0 +1,139 @@
|
|||||||
|
#!/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 config from "../lib/config";
|
||||||
|
import * as logger from "../lib/logger";
|
||||||
|
import * as cluster from "../lib/cluster";
|
||||||
|
import * as server from "../lib/server";
|
||||||
|
import { listener } from "../lib/ui";
|
||||||
|
import * as extensions from "../lib/extensions";
|
||||||
|
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";
|
||||||
|
|
||||||
|
logger.init("ui", VERSION);
|
||||||
|
|
||||||
|
const SERVICE_ADDRESS = config.get("UI_INTERFACE");
|
||||||
|
const SERVICE_PORT = config.get("UI_PORT");
|
||||||
|
|
||||||
|
function exitWorkerGracefully(): void {
|
||||||
|
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||||
|
Promise.all([
|
||||||
|
db.disconnect(),
|
||||||
|
db2.disconnect(),
|
||||||
|
cache.disconnect(),
|
||||||
|
extensions.killAll(),
|
||||||
|
cluster.worker.disconnect()
|
||||||
|
]).catch(exitWorkerUngracefully);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWorkerUngracefully(): void {
|
||||||
|
extensions.killAll().then(() => {
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cluster.worker) {
|
||||||
|
const WORKER_COUNT = config.get("UI_WORKER_PROCESSES");
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
message: `genieacs-ui starting`,
|
||||||
|
pid: process.pid,
|
||||||
|
version: VERSION
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGINT, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
logger.info({
|
||||||
|
message: "Received signal SIGTERM, exiting",
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
|
||||||
|
cluster.stop();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const ssl = {
|
||||||
|
key: config.get("UI_SSL_KEY"),
|
||||||
|
cert: config.get("UI_SSL_CERT")
|
||||||
|
};
|
||||||
|
|
||||||
|
let stopping = false;
|
||||||
|
|
||||||
|
process.on("uncaughtException", err => {
|
||||||
|
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||||
|
logger.error({
|
||||||
|
message: "Uncaught exception",
|
||||||
|
exception: err,
|
||||||
|
pid: process.pid
|
||||||
|
});
|
||||||
|
stopping = true;
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
|
||||||
|
const _listener = (req, res): void => {
|
||||||
|
if (stopping) res.setHeader("Connection", "close");
|
||||||
|
listener(req, res);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initPromise = Promise.all([db2.connect(), cache.connect()])
|
||||||
|
.then(() => {
|
||||||
|
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
setTimeout(() => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
stopping = true;
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGTERM", () => {
|
||||||
|
stopping = true;
|
||||||
|
initPromise.finally(() => {
|
||||||
|
server
|
||||||
|
.stop()
|
||||||
|
.then(exitWorkerGracefully)
|
||||||
|
.catch(exitWorkerUngracefully);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import * as config from "../lib/config.ts";
|
|
||||||
import * as logger from "../lib/logger.ts";
|
|
||||||
import * as cluster from "../lib/cluster.ts";
|
|
||||||
import * as server from "../lib/server.ts";
|
|
||||||
import { listener } from "../lib/ui.ts";
|
|
||||||
import * as extensions from "../lib/extensions.ts";
|
|
||||||
import * as db from "../lib/db/db.ts";
|
|
||||||
import { version as VERSION } from "../package.json";
|
|
||||||
|
|
||||||
logger.init("ui", VERSION);
|
|
||||||
|
|
||||||
const SERVICE_ADDRESS = config.get("UI_INTERFACE") as string;
|
|
||||||
const SERVICE_PORT = config.get("UI_PORT") as number;
|
|
||||||
|
|
||||||
function exitWorkerGracefully(): void {
|
|
||||||
setTimeout(exitWorkerUngracefully, 5000).unref();
|
|
||||||
Promise.all([
|
|
||||||
db.disconnect(),
|
|
||||||
extensions.killAll(),
|
|
||||||
cluster.worker.disconnect(),
|
|
||||||
])
|
|
||||||
.then(logger.close)
|
|
||||||
.catch(exitWorkerUngracefully);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitWorkerUngracefully(): void {
|
|
||||||
void extensions.killAll().finally(() => {
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cluster.worker) {
|
|
||||||
const WORKER_COUNT = config.get("UI_WORKER_PROCESSES") as number;
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
message: `genieacs-ui starting`,
|
|
||||||
pid: process.pid,
|
|
||||||
version: VERSION,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
logger.info({
|
|
||||||
message: "Received signal SIGINT, exiting",
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
logger.info({
|
|
||||||
message: "Received signal SIGTERM, exiting",
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
|
|
||||||
cluster.stop();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const key = config.get("UI_SSL_KEY") as string;
|
|
||||||
const cert = config.get("UI_SSL_CERT") as string;
|
|
||||||
const options = {
|
|
||||||
port: SERVICE_PORT,
|
|
||||||
host: SERVICE_ADDRESS,
|
|
||||||
ssl: key && cert ? { key, cert } : null,
|
|
||||||
timeout: 30000,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Need this for Node < 15
|
|
||||||
process.on("unhandledRejection", (err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
|
||||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
|
||||||
logger.error({
|
|
||||||
message: "Uncaught exception",
|
|
||||||
exception: err,
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
|
|
||||||
const initPromise = db
|
|
||||||
.connect()
|
|
||||||
.then(() => {
|
|
||||||
server.start(options, async (req, res) => listener(req, res));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
void initPromise.finally(() => {
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
void initPromise.finally(() => {
|
|
||||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export const APP_JS = "app.js";
|
|
||||||
export const APP_CSS = "app.css";
|
|
||||||
export const ICONS_SVG = "icons.svg";
|
|
||||||
export const LOGO_SVG = "logo.svg";
|
|
||||||
export const FAVICON_PNG = "favicon.png";
|
|
||||||
+296
-264
@@ -1,46 +1,98 @@
|
|||||||
import path from "node:path";
|
/**
|
||||||
import fs from "node:fs";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import { createHash } from "node:crypto";
|
*
|
||||||
import { promisify } from "node:util";
|
* This file is part of GenieACS.
|
||||||
import { exec } from "node:child_process";
|
*
|
||||||
import * as esbuild from "esbuild";
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
import { optimize } from "svgo";
|
* it under the terms of the GNU Affero General Public License as
|
||||||
import * as xmlParser from "../lib/xml-parser.ts";
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
const fsAsync = {
|
import * as path from "path";
|
||||||
readdir: promisify(fs.readdir),
|
import * as fs from "fs";
|
||||||
readFile: promisify(fs.readFile),
|
import { promisify } from "util";
|
||||||
writeFile: promisify(fs.writeFile),
|
import { rollup, WarningHandler } from "rollup";
|
||||||
copyFile: promisify(fs.copyFile),
|
import rollupReplace from "rollup-plugin-replace";
|
||||||
rename: promisify(fs.rename),
|
import rollupJson from "rollup-plugin-json";
|
||||||
chmod: promisify(fs.chmod),
|
import typescript from "rollup-plugin-typescript";
|
||||||
lstat: promisify(fs.lstat),
|
import { terser } from "rollup-plugin-terser";
|
||||||
exists: promisify(fs.exists),
|
import webpack from "webpack";
|
||||||
rmdir: promisify(fs.rmdir),
|
import postcss from "postcss";
|
||||||
unlink: promisify(fs.unlink),
|
import postcssImport from "postcss-import";
|
||||||
mkdir: promisify(fs.mkdir),
|
import postcssPresetEnv from "postcss-preset-env";
|
||||||
};
|
import cssnano from "cssnano";
|
||||||
|
import SVGO from "svgo";
|
||||||
const execAsync = promisify(exec);
|
import * as xmlParser from "../lib/xml-parser";
|
||||||
|
|
||||||
const MODE = process.env["NODE_ENV"] || "production";
|
const MODE = process.env["NODE_ENV"] || "production";
|
||||||
|
|
||||||
const INPUT_DIR = process.cwd();
|
const BUILD_METADATA = new Date()
|
||||||
const OUTPUT_DIR = path.join(INPUT_DIR, "dist");
|
.toISOString()
|
||||||
|
.split(".")[0]
|
||||||
|
.replace(/[^0-9]/g, "");
|
||||||
|
|
||||||
async function rmDir(dirPath: string): Promise<void> {
|
const INPUT_DIR = path.resolve(__dirname, "..");
|
||||||
if (!(await fsAsync.exists(dirPath))) return;
|
const OUTPUT_DIR = path.resolve(__dirname, "../dist");
|
||||||
const files = await fsAsync.readdir(dirPath);
|
|
||||||
|
const externals = [
|
||||||
|
"path",
|
||||||
|
"fs",
|
||||||
|
"cluster",
|
||||||
|
"os",
|
||||||
|
"tls",
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"zlib",
|
||||||
|
"crypto",
|
||||||
|
"util",
|
||||||
|
"mongodb",
|
||||||
|
"libxmljs",
|
||||||
|
"vm",
|
||||||
|
"later",
|
||||||
|
"parsimmon",
|
||||||
|
"seedrandom",
|
||||||
|
"querystring",
|
||||||
|
"child_process",
|
||||||
|
"dgram",
|
||||||
|
"url",
|
||||||
|
"iconv-lite",
|
||||||
|
"koa",
|
||||||
|
"koa-router",
|
||||||
|
"koa-compress",
|
||||||
|
"koa-bodyparser",
|
||||||
|
"koa-jwt",
|
||||||
|
"koa-static",
|
||||||
|
"jsonwebtoken",
|
||||||
|
"stream",
|
||||||
|
"mithril",
|
||||||
|
"parsimmon",
|
||||||
|
"yaml",
|
||||||
|
"codemirror",
|
||||||
|
"codemirror/mode/javascript/javascript",
|
||||||
|
"codemirror/mode/yaml/yaml"
|
||||||
|
];
|
||||||
|
|
||||||
|
function rmDirSync(dirPath): void {
|
||||||
|
if (!fs.existsSync(dirPath)) return;
|
||||||
|
const files = fs.readdirSync(dirPath);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = path.join(dirPath, file);
|
const filePath = `${dirPath}/${file}`;
|
||||||
if ((await fsAsync.lstat(filePath)).isDirectory()) await rmDir(filePath);
|
if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath);
|
||||||
else await fsAsync.unlink(filePath);
|
else rmDirSync(filePath);
|
||||||
}
|
}
|
||||||
await fsAsync.rmdir(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"])) {
|
||||||
@@ -50,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));
|
||||||
@@ -68,59 +111,6 @@ function xmlTostring(xml): string {
|
|||||||
: `<${xml.name} ${xml.attrs}>${children.join("")}</${xml.name}>`;
|
: `<${xml.name} ${xml.attrs}>${children.join("")}</${xml.name}>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function assetHash(buffer: Buffer | string): string {
|
|
||||||
return createHash("md5").update(buffer).digest("hex").slice(0, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ASSETS = {} as {
|
|
||||||
APP_JS?: string;
|
|
||||||
APP_CSS?: string;
|
|
||||||
ICONS_SVG?: string;
|
|
||||||
LOGO_SVG?: string;
|
|
||||||
FAVICON_PNG?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const assetsPlugin = {
|
|
||||||
name: "assets",
|
|
||||||
setup(build) {
|
|
||||||
build.onLoad({ filter: /\/build\/assets.ts$/ }, () => {
|
|
||||||
const lines = Object.entries(ASSETS).map(
|
|
||||||
([k, v]) => `export const ${k} = ${JSON.stringify(v)};`,
|
|
||||||
);
|
|
||||||
return { contents: lines.join("\n") };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
} as esbuild.Plugin;
|
|
||||||
|
|
||||||
const packageDotJsonPlugin = {
|
|
||||||
name: "packageDotJson",
|
|
||||||
setup(build) {
|
|
||||||
const sourcePath = path.join(INPUT_DIR, "package.json");
|
|
||||||
build.onResolve({ filter: /\/package.json$/ }, (args) => {
|
|
||||||
const p = path.join(args.resolveDir, args.path);
|
|
||||||
if (p !== sourcePath) return undefined;
|
|
||||||
return { path: path.join(OUTPUT_DIR, "package.json") };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
} as esbuild.Plugin;
|
|
||||||
|
|
||||||
const inlineDepsPlugin = {
|
|
||||||
name: "inlineDeps",
|
|
||||||
setup(build) {
|
|
||||||
const deps = [
|
|
||||||
"parsimmon",
|
|
||||||
"espresso-iisojs",
|
|
||||||
"codemirror",
|
|
||||||
"mithril",
|
|
||||||
"yaml",
|
|
||||||
];
|
|
||||||
build.onResolve({ filter: /^[^.]/ }, async (args) => {
|
|
||||||
if (deps.some((d) => args.path.startsWith(d))) return undefined;
|
|
||||||
return { sideEffects: false, external: true };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
} as esbuild.Plugin;
|
|
||||||
|
|
||||||
function generateSymbol(id: string, svgStr: string): string {
|
function generateSymbol(id: string, svgStr: string): string {
|
||||||
const xml = xmlParser.parseXml(svgStr);
|
const xml = xmlParser.parseXml(svgStr);
|
||||||
const svg = xml.children[0];
|
const svg = xml.children[0];
|
||||||
@@ -133,65 +123,48 @@ 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 getBuildMetadata(): Promise<string> {
|
|
||||||
const date = new Date().toISOString().slice(2, 10).replaceAll("-", "");
|
|
||||||
|
|
||||||
const [commit, diff, newFiles] = await Promise.all([
|
|
||||||
execAsync("git rev-parse HEAD"),
|
|
||||||
execAsync("git diff HEAD"),
|
|
||||||
execAsync("git ls-files --others --exclude-standard"),
|
|
||||||
]).then((res) => res.map((r) => r.stdout.trim()));
|
|
||||||
|
|
||||||
if (!diff && !newFiles) return date + commit.slice(0, 4);
|
|
||||||
|
|
||||||
const hash = createHash("md5");
|
|
||||||
hash.update(commit).update(diff).update(newFiles);
|
|
||||||
for (const file of newFiles.split("\n").filter((f) => f))
|
|
||||||
hash.update(await fsAsync.readFile(file));
|
|
||||||
return date + hash.digest("hex").slice(0, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
const [buildMetadata, packageJsonFile, npmShrinkwrapFile] = await Promise.all(
|
// Delete any old output directory
|
||||||
[
|
rmDirSync(OUTPUT_DIR);
|
||||||
getBuildMetadata(),
|
|
||||||
fsAsync.readFile(path.join(INPUT_DIR, "package.json")),
|
// Create output directory layout
|
||||||
fsAsync.readFile(path.join(INPUT_DIR, "npm-shrinkwrap.json")),
|
fs.mkdirSync(OUTPUT_DIR);
|
||||||
],
|
fs.mkdirSync(OUTPUT_DIR + "/bin");
|
||||||
|
fs.mkdirSync(OUTPUT_DIR + "/public");
|
||||||
|
fs.mkdirSync(OUTPUT_DIR + "/tools");
|
||||||
|
|
||||||
|
// Create package.json
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString()
|
||||||
|
);
|
||||||
|
delete packageJson["devDependencies"];
|
||||||
|
packageJson["scripts"] = {
|
||||||
|
install: packageJson["scripts"].install,
|
||||||
|
configure: packageJson["scripts"].configure
|
||||||
|
};
|
||||||
|
packageJson["version"] = `${packageJson["version"]}+${BUILD_METADATA}`;
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.resolve(OUTPUT_DIR, "package.json"),
|
||||||
|
JSON.stringify(packageJson, null, 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
const packageJson = JSON.parse(packageJsonFile.toString());
|
// Create npm-shrinkwrap.json
|
||||||
delete packageJson["devDependencies"];
|
const npmShrinkwrapJson = JSON.parse(
|
||||||
delete packageJson["private"];
|
fs.readFileSync(path.resolve(INPUT_DIR, "npm-shrinkwrap.json")).toString()
|
||||||
delete packageJson["scripts"];
|
);
|
||||||
packageJson["version"] = `${packageJson["version"]}+${buildMetadata}`;
|
npmShrinkwrapJson["version"] = packageJson["version"];
|
||||||
|
stripDevDeps(npmShrinkwrapJson);
|
||||||
const npmShrinkwrap = JSON.parse(npmShrinkwrapFile.toString());
|
fs.writeFileSync(
|
||||||
npmShrinkwrap["version"] = packageJson["version"];
|
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
|
||||||
stripDevDeps(npmShrinkwrap);
|
JSON.stringify(npmShrinkwrapJson, null, 2)
|
||||||
stripDevDeps2(npmShrinkwrap);
|
);
|
||||||
|
|
||||||
await rmDir(OUTPUT_DIR);
|
|
||||||
|
|
||||||
await fsAsync.mkdir(OUTPUT_DIR);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
fsAsync.mkdir(path.join(OUTPUT_DIR, "bin")),
|
|
||||||
fsAsync.mkdir(path.join(OUTPUT_DIR, "public")),
|
|
||||||
fsAsync.writeFile(
|
|
||||||
path.join(OUTPUT_DIR, "package.json"),
|
|
||||||
JSON.stringify(packageJson, null, 2),
|
|
||||||
),
|
|
||||||
fsAsync.writeFile(
|
|
||||||
path.join(OUTPUT_DIR, "npm-shrinkwrap.json"),
|
|
||||||
JSON.stringify(npmShrinkwrap, null, 2),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyStatic(): Promise<void> {
|
async function copyStatic(): Promise<void> {
|
||||||
@@ -200,160 +173,219 @@ 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"
|
||||||
];
|
];
|
||||||
|
|
||||||
const [logo, favicon] = await Promise.all([
|
for (const file of files) {
|
||||||
fsAsync.readFile(path.join(INPUT_DIR, "public/logo.svg")),
|
fs.copyFileSync(
|
||||||
fsAsync.readFile(path.join(INPUT_DIR, "public/favicon.png")),
|
path.resolve(INPUT_DIR, file),
|
||||||
]);
|
path.resolve(OUTPUT_DIR, file)
|
||||||
|
);
|
||||||
ASSETS.LOGO_SVG = `logo-${assetHash(logo)}.svg`;
|
}
|
||||||
ASSETS.FAVICON_PNG = `favicon-${assetHash(favicon)}.png`;
|
|
||||||
|
|
||||||
const filenames = {} as Record<string, string>;
|
|
||||||
filenames["public/logo.svg"] = path.join("public", ASSETS.LOGO_SVG);
|
|
||||||
filenames["public/favicon.png"] = path.join("public", ASSETS.FAVICON_PNG);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
files.map((f) =>
|
|
||||||
fsAsync.copyFile(
|
|
||||||
path.join(INPUT_DIR, f),
|
|
||||||
path.join(OUTPUT_DIR, filenames[f] || f),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateCss(): Promise<void> {
|
async function generateCss(): Promise<void> {
|
||||||
const res = await esbuild.build({
|
const cssInPath = path.resolve(INPUT_DIR, "ui/css/app.css");
|
||||||
bundle: true,
|
const cssOutPath = path.resolve(OUTPUT_DIR, "public/app.css");
|
||||||
absWorkingDir: INPUT_DIR,
|
const cssIn = fs.readFileSync(cssInPath);
|
||||||
minify: MODE === "production",
|
const cssOut = await postcss([
|
||||||
sourcemap: "linked",
|
postcssImport,
|
||||||
sourcesContent: false,
|
postcssPresetEnv({
|
||||||
entryPoints: ["ui/css/app.css"],
|
stage: 3,
|
||||||
entryNames: "[dir]/[name]-[hash]",
|
features: {
|
||||||
outfile: path.join(OUTPUT_DIR, "public/app.css"),
|
"nesting-rules": true,
|
||||||
target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"],
|
"color-mod-function": true
|
||||||
metafile: true,
|
}
|
||||||
});
|
}),
|
||||||
|
cssnano
|
||||||
|
]).process(cssIn, { from: cssInPath, to: cssOutPath });
|
||||||
|
fs.writeFileSync(cssOutPath, cssOut.css);
|
||||||
|
}
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(res.metafile.outputs)) {
|
async function generateToolsJs(): Promise<void> {
|
||||||
if (v.entryPoint === "ui/css/app.css") {
|
for (const bin of ["dump-data-model"]) {
|
||||||
ASSETS.APP_CSS = path.relative(
|
const inputFile = path.resolve(INPUT_DIR, `tools/${bin}`);
|
||||||
path.join(OUTPUT_DIR, "public"),
|
const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`);
|
||||||
path.join(INPUT_DIR, k),
|
const bundle = await rollup({
|
||||||
);
|
input: inputFile,
|
||||||
break;
|
external: externals,
|
||||||
}
|
acorn: {
|
||||||
|
allowHashBang: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
rollupReplace({
|
||||||
|
delimiters: ["", ""],
|
||||||
|
"#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": ""
|
||||||
|
}),
|
||||||
|
typescript({
|
||||||
|
tsconfig: "./tsconfig.json",
|
||||||
|
include: [`tools/${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 generateBackendJs(): Promise<void> {
|
async function generateBackendJs(): Promise<void> {
|
||||||
const services = [
|
for (const bin of [
|
||||||
"genieacs-cwmp",
|
"genieacs-cwmp",
|
||||||
"genieacs-ext",
|
"genieacs-ext",
|
||||||
"genieacs-nbi",
|
"genieacs-nbi",
|
||||||
"genieacs-fs",
|
"genieacs-fs",
|
||||||
"genieacs-ui",
|
"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 esbuild.build({
|
await bundle.write({
|
||||||
bundle: true,
|
format: "cjs",
|
||||||
absWorkingDir: INPUT_DIR,
|
preferConst: true,
|
||||||
minify: MODE === "production",
|
sourcemap: "inline",
|
||||||
sourcemap: "inline",
|
sourcemapExcludeSources: true,
|
||||||
sourcesContent: false,
|
banner: "#!/usr/bin/env node",
|
||||||
platform: "node",
|
file: outputFile
|
||||||
target: "node12.13.0",
|
});
|
||||||
packages: "external",
|
|
||||||
banner: { js: "#!/usr/bin/env node" },
|
|
||||||
entryPoints: services.map((s) => `bin/${s}.ts`),
|
|
||||||
outdir: path.join(OUTPUT_DIR, "bin"),
|
|
||||||
plugins: [packageDotJsonPlugin, assetsPlugin],
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const bin of services) {
|
|
||||||
const p = path.join(OUTPUT_DIR, "bin", bin);
|
|
||||||
await fsAsync.rename(`${p}.js`, p);
|
|
||||||
// Mark as executable
|
// Mark as executable
|
||||||
const mode = (await fsAsync.lstat(p)).mode;
|
const mode = fs.statSync(outputFile).mode;
|
||||||
await fsAsync.chmod(p, mode | 73);
|
fs.chmodSync(outputFile, mode | 73);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateFrontendJs(): Promise<void> {
|
async function generateFrontendJs(): Promise<void> {
|
||||||
const res = await esbuild.build({
|
const inputFile = path.resolve(INPUT_DIR, "ui/app.ts");
|
||||||
bundle: true,
|
const outputFile = path.resolve(OUTPUT_DIR, "public/app.js");
|
||||||
absWorkingDir: INPUT_DIR,
|
|
||||||
splitting: true,
|
const bundle = await rollup({
|
||||||
minify: MODE === "production",
|
input: inputFile,
|
||||||
sourcemap: "linked",
|
external: externals,
|
||||||
sourcesContent: false,
|
plugins: [
|
||||||
platform: "browser",
|
rollupJson({ preferConst: true }),
|
||||||
format: "esm",
|
typescript({ tsconfig: "./tsconfig.json" })
|
||||||
target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"],
|
],
|
||||||
entryPoints: ["ui/app.ts"],
|
inlineDynamicImports: true,
|
||||||
entryNames: "[dir]/[name]-[hash]",
|
treeshake: {
|
||||||
outdir: path.join(OUTPUT_DIR, "public"),
|
propertyReadSideEffects: false,
|
||||||
plugins: [packageDotJsonPlugin, inlineDepsPlugin, assetsPlugin],
|
pureExternalModules: true
|
||||||
metafile: true,
|
},
|
||||||
|
onwarn: ((warning, warn) => {
|
||||||
|
// Ignore circular dependency warnings
|
||||||
|
if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning);
|
||||||
|
}) as WarningHandler
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(res.metafile.outputs)) {
|
await bundle.write({
|
||||||
for (const imp of v.imports)
|
preferConst: true,
|
||||||
if (imp.external) throw new Error(`External import found: ${imp.path}`);
|
format: "esm",
|
||||||
|
sourcemap: "inline",
|
||||||
|
sourcemapExcludeSources: true,
|
||||||
|
file: outputFile
|
||||||
|
});
|
||||||
|
|
||||||
if (v.entryPoint === "ui/app.ts") {
|
const webpackConf = {
|
||||||
ASSETS.APP_JS = path.relative(
|
mode: MODE,
|
||||||
path.join(OUTPUT_DIR, "public"),
|
entry: outputFile,
|
||||||
path.join(INPUT_DIR, k),
|
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 symbols = [] as string[];
|
const svgo = new SVGO({ plugins: [{ removeViewBox: false }] });
|
||||||
const iconsDir = path.join(INPUT_DIR, "ui/icons");
|
const symbols = [];
|
||||||
for (const file of await fsAsync.readdir(iconsDir)) {
|
const iconsDir = path.resolve(INPUT_DIR, "ui/icons");
|
||||||
|
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 src = (await fsAsync.readFile(filePath)).toString();
|
const { data } = await svgo.optimize(fs.readFileSync(filePath).toString());
|
||||||
const { data } = await optimize(src, {
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
name: "preset-default",
|
|
||||||
params: {
|
|
||||||
overrides: {
|
|
||||||
removeViewBox: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
symbols.push(generateSymbol(id, data));
|
symbols.push(generateSymbol(id, data));
|
||||||
}
|
}
|
||||||
const data = `<svg xmlns="http://www.w3.org/2000/svg">${symbols.join(
|
fs.writeFileSync(
|
||||||
"",
|
path.resolve(OUTPUT_DIR, "public/icons.svg"),
|
||||||
)}</svg>`;
|
`<svg xmlns="http://www.w3.org/2000/svg">${symbols.join("")}</svg>`
|
||||||
ASSETS.ICONS_SVG = `icons-${assetHash(data)}.svg`;
|
|
||||||
await fsAsync.writeFile(
|
|
||||||
path.join(OUTPUT_DIR, "public", ASSETS.ICONS_SVG),
|
|
||||||
data,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
init()
|
init()
|
||||||
.then(() =>
|
.then(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
Promise.all([generateIconsSprite(), copyStatic()]).then(
|
copyStatic(),
|
||||||
generateFrontendJs,
|
|
||||||
),
|
|
||||||
generateCss(),
|
generateCss(),
|
||||||
]).then(generateBackendJs),
|
generateIconsSprite(),
|
||||||
)
|
generateToolsJs(),
|
||||||
.catch((err) => {
|
generateBackendJs(),
|
||||||
|
generateFrontendJs()
|
||||||
|
])
|
||||||
|
.then(() => {})
|
||||||
|
.catch(err => {
|
||||||
|
process.stderr.write(err.stack + "\n");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
process.stderr.write(err.stack + "\n");
|
process.stderr.write(err.stack + "\n");
|
||||||
});
|
});
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { exec } from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
|
|
||||||
const execPromise = promisify(exec);
|
|
||||||
|
|
||||||
async function runEslint(): Promise<string> {
|
|
||||||
const CMD =
|
|
||||||
"eslint 'bin/*.ts' 'lib/**/*.ts' 'ui/**/*.ts' 'test/**/*.ts' 'build/**/*.ts'";
|
|
||||||
const env = {
|
|
||||||
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
|
|
||||||
...process.env,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const { stdout, stderr } = await execPromise(CMD, { env });
|
|
||||||
if (stderr) throw new Error(stderr);
|
|
||||||
return stdout;
|
|
||||||
} catch (err) {
|
|
||||||
if (err.killed || err.signal || err.stderr || err.code !== 1) throw err;
|
|
||||||
return err.stdout;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runTsc(): Promise<string> {
|
|
||||||
const CMD = "tsc --noEmit";
|
|
||||||
const env = {
|
|
||||||
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
|
|
||||||
...process.env,
|
|
||||||
};
|
|
||||||
const { stdout, stderr } = await execPromise(CMD, { env });
|
|
||||||
if (stderr) throw new Error(stderr);
|
|
||||||
return stdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runPrettier(): Promise<string> {
|
|
||||||
const CMD = "prettier --prose-wrap always --write .";
|
|
||||||
const env = {
|
|
||||||
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
|
|
||||||
...process.env,
|
|
||||||
};
|
|
||||||
const { stdout, stderr } = await execPromise(CMD, { env });
|
|
||||||
if (stderr) throw new Error(stderr);
|
|
||||||
return stdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runAll(): Promise<void> {
|
|
||||||
const prom1 = runPrettier();
|
|
||||||
const prom2 = runEslint();
|
|
||||||
const prom3 = runTsc();
|
|
||||||
|
|
||||||
console.log(await prom1);
|
|
||||||
console.log(await prom2);
|
|
||||||
console.log(await prom3);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAll().catch(console.error);
|
|
||||||
@@ -1,178 +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
|
|
||||||
skipRootGpn
|
|
||||||
GPN
|
|
||||||
NaN
|
|
||||||
CIDR
|
|
||||||
XMPP
|
|
||||||
PEM
|
|
||||||
@@ -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
@@ -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``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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:7567/') 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
@@ -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;
|
||||||
@@ -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,14 +73,8 @@ 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
|
||||||
Generate a secure JWT secret and append to :file:`/opt/genieacs/genieacs.env`:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
node -e "console.log(\"GENIEACS_UI_JWT_SECRET=\" + require('crypto').randomBytes(128).toString('hex'))" >> /opt/genieacs/genieacs.env
|
|
||||||
|
|
||||||
Set file ownership and permissions:
|
Set file ownership and permissions:
|
||||||
|
|
||||||
|
|||||||
@@ -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});
|
||||||
}
|
}
|
||||||
|
|||||||
+147
-352
@@ -1,160 +1,112 @@
|
|||||||
import { ObjectId } from "mongodb";
|
/**
|
||||||
import { collections } from "./db/db.ts";
|
* 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 { parse } from "url";
|
||||||
|
import * as db from "./db";
|
||||||
|
import * as common from "./common";
|
||||||
|
import * as cache from "./cache";
|
||||||
import {
|
import {
|
||||||
deleteConfig,
|
getCurrentSnapshot,
|
||||||
deleteFault as dbDeleteFault,
|
|
||||||
deleteFile,
|
|
||||||
deletePermission,
|
|
||||||
deletePreset,
|
|
||||||
deleteProvision,
|
|
||||||
deleteTask,
|
|
||||||
deleteUser,
|
|
||||||
deleteVirtualParameter,
|
|
||||||
putConfig,
|
|
||||||
putPermission,
|
|
||||||
putPreset,
|
|
||||||
putProvision,
|
|
||||||
putUser,
|
|
||||||
putVirtualParameter,
|
|
||||||
} from "./ui/db.ts";
|
|
||||||
import * as common from "./util.ts";
|
|
||||||
import * as cache from "./cache.ts";
|
|
||||||
import { acquireLock, getToken, releaseLock } from "./lock.ts";
|
|
||||||
import {
|
|
||||||
getRevision,
|
|
||||||
getConfig,
|
getConfig,
|
||||||
getConfigExpression,
|
getConfigExpression
|
||||||
getUsers,
|
} from "./local-cache";
|
||||||
} from "./ui/local-cache.ts";
|
|
||||||
import {
|
import {
|
||||||
httpConnectionRequest,
|
httpConnectionRequest,
|
||||||
udpConnectionRequest,
|
udpConnectionRequest
|
||||||
xmppConnectionRequest,
|
} from "./connection-request";
|
||||||
} from "./connection-request.ts";
|
import { Expression, Task } from "./types";
|
||||||
import { Expression, Task } from "./types.ts";
|
|
||||||
import { evaluate } from "./common/expression/util.ts";
|
|
||||||
import { hashPassword } from "./auth.ts";
|
|
||||||
import { flattenDevice } from "./ui/db.ts";
|
|
||||||
import { ResourceLockedError } from "./common/errors.ts";
|
|
||||||
import * as config from "../lib/config.ts";
|
|
||||||
|
|
||||||
const XMPP_CONFIGURED = !!config.get("XMPP_JID");
|
export async function connectionRequest(deviceId): Promise<void> {
|
||||||
|
const options = {
|
||||||
export async function connectionRequest(
|
projection: {
|
||||||
deviceId: string,
|
_deviceId: 1,
|
||||||
device?: Record<string, { value?: [boolean | number | string, string] }>,
|
"Device.ManagementServer.ConnectionRequestURL._value": 1,
|
||||||
): Promise<string> {
|
"Device.ManagementServer.UDPConnectionRequestAddress._value": 1,
|
||||||
if (!device) {
|
"Device.ManagementServer.ConnectionRequestUsername._value": 1,
|
||||||
const res = await collections.devices.findOne({ _id: deviceId });
|
"Device.ManagementServer.ConnectionRequestPassword._value": 1,
|
||||||
if (!res) throw new Error("No such device");
|
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value": 1,
|
||||||
device = flattenDevice(res);
|
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value": 1,
|
||||||
}
|
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value": 1,
|
||||||
|
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1
|
||||||
let connectionRequestUrl,
|
|
||||||
udpConnectionRequestAddress,
|
|
||||||
stunEnable,
|
|
||||||
connReqJabberId,
|
|
||||||
username,
|
|
||||||
password;
|
|
||||||
|
|
||||||
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
|
|
||||||
connectionRequestUrl = (device[
|
|
||||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL"
|
|
||||||
].value || [""])[0];
|
|
||||||
udpConnectionRequestAddress = ((
|
|
||||||
device[
|
|
||||||
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
|
|
||||||
] || {}
|
|
||||||
).value || [""])[0];
|
|
||||||
stunEnable = ((
|
|
||||||
device["InternetGatewayDevice.ManagementServer.STUNEnable"] || {}
|
|
||||||
).value || [""])[0];
|
|
||||||
connReqJabberId = ((
|
|
||||||
device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || {}
|
|
||||||
).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];
|
|
||||||
stunEnable = ((device["Device.ManagementServer.STUNEnable"] || {})
|
|
||||||
.value || [""])[0];
|
|
||||||
connReqJabberId = ((device["Device.ManagementServer.ConnReqJabberID"] || {})
|
|
||||||
.value || [""])[0];
|
|
||||||
username = ((
|
|
||||||
device["Device.ManagementServer.ConnectionRequestUsername"] || {}
|
|
||||||
).value || [""])[0];
|
|
||||||
password = ((
|
|
||||||
device["Device.ManagementServer.ConnectionRequestPassword"] || {}
|
|
||||||
).value || [""])[0];
|
|
||||||
}
|
|
||||||
let remoteAddress;
|
|
||||||
try {
|
|
||||||
remoteAddress = new URL(connectionRequestUrl).hostname;
|
|
||||||
} catch (err) {
|
|
||||||
return "Invalid connection request URL";
|
|
||||||
}
|
|
||||||
|
|
||||||
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 snapshot = await getRevision();
|
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 now = Date.now();
|
const now = Date.now();
|
||||||
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,
|
||||||
"cwmp.connectionRequestAuth",
|
"cwmp.connectionRequestAuth"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (authExp === undefined) {
|
if (authExp === undefined) {
|
||||||
@@ -162,97 +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;
|
||||||
|
if (udpConnectionRequestAddress) {
|
||||||
let udpProm = Promise.resolve(false);
|
udpProm = udpConnectionRequest(
|
||||||
if (udpConnectionRequestAddress && +stunEnable) {
|
udpConnectionRequestAddress,
|
||||||
try {
|
authExp,
|
||||||
const u = new URL("udp://" + udpConnectionRequestAddress);
|
context,
|
||||||
udpProm = udpConnectionRequest(
|
UDP_CONNECTION_REQUEST_PORT,
|
||||||
u.hostname,
|
debug,
|
||||||
parseInt(u.port || "80"),
|
deviceId
|
||||||
authExp,
|
);
|
||||||
UDP_CONNECTION_REQUEST_PORT,
|
|
||||||
debug,
|
|
||||||
deviceId,
|
|
||||||
).then(
|
|
||||||
() => true,
|
|
||||||
() => false,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
// Ignore invalid address
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let status;
|
try {
|
||||||
|
await httpConnectionRequest(
|
||||||
if (connReqJabberId && XMPP_CONFIGURED) {
|
|
||||||
status = await xmppConnectionRequest(
|
|
||||||
connReqJabberId,
|
|
||||||
authExp,
|
|
||||||
CONNECTION_REQUEST_TIMEOUT,
|
|
||||||
debug,
|
|
||||||
deviceId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
status = await httpConnectionRequest(
|
|
||||||
connectionRequestUrl,
|
connectionRequestUrl,
|
||||||
authExp,
|
authExp,
|
||||||
|
context,
|
||||||
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
|
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
|
||||||
CONNECTION_REQUEST_TIMEOUT,
|
CONNECTION_REQUEST_TIMEOUT,
|
||||||
debug,
|
debug,
|
||||||
deviceId,
|
deviceId
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!udpProm) throw err;
|
||||||
|
await udpProm;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await udpProm) return "";
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
const task = await db.tasksCollection.findOne(
|
||||||
): Promise<boolean> {
|
{ _id: taskId },
|
||||||
const now = Date.now();
|
{ projection: { _id: 1 } }
|
||||||
const device = await collections.devices.findOne(
|
|
||||||
{ _id: deviceId },
|
|
||||||
{ projection: { _lastInform: 1 } },
|
|
||||||
);
|
);
|
||||||
const li = (device["_lastInform"] as Date).getTime();
|
if (!task) return "completed";
|
||||||
if (li > lastInform) return true;
|
|
||||||
const token = await getToken(`cwmp_session_${deviceId}`);
|
|
||||||
if (token?.startsWith("cwmp_session_")) 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 q = { _id: `${deviceId}:task_${taskId}` };
|
||||||
deviceId: string,
|
const fault = await db.faultsCollection.findOne(q, {
|
||||||
timeout: number,
|
projection: { _id: 1 }
|
||||||
): Promise<boolean> {
|
});
|
||||||
const now = Date.now();
|
if (fault) return "fault";
|
||||||
const token = await getToken(`cwmp_session_${deviceId}`);
|
|
||||||
if (!token?.startsWith("cwmp_session_")) return true;
|
if ((timeout -= 500) <= 0) return "timeout";
|
||||||
if (timeout < 500) return false;
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
return watchTask(deviceId, taskId, timeout);
|
||||||
timeout -= Date.now() - now;
|
|
||||||
return awaitSessionEnd(deviceId, 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);
|
||||||
}
|
}
|
||||||
@@ -290,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");
|
||||||
@@ -330,170 +245,50 @@ 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 collections.tasks.deleteOne({
|
await db.tasksCollection.deleteOne({
|
||||||
device: task.device,
|
device: task.device,
|
||||||
uniqueKey: task.uniqueKey,
|
uniqueKey: task.uniqueKey
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await collections.tasks.insertMany(tasks);
|
await db.tasksCollection.insertMany(tasks);
|
||||||
for (const task of tasks) task._id = task._id.toString();
|
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteDevice(deviceId: string): Promise<void> {
|
export async function deleteDevice(deviceId): Promise<void> {
|
||||||
const token = await acquireLock(`cwmp_session_${deviceId}`, 5000);
|
await Promise.all([
|
||||||
if (!token) throw new ResourceLockedError("Device is in session");
|
db.tasksCollection.deleteMany({ device: deviceId }),
|
||||||
try {
|
db.devicesCollection.deleteOne({ _id: deviceId }),
|
||||||
await Promise.all([
|
db.faultsCollection.deleteMany({
|
||||||
collections.tasks.deleteMany({ device: deviceId }),
|
_id: {
|
||||||
collections.devices.deleteOne({ _id: deviceId }),
|
$regex: `^${common.escapeRegExp(deviceId)}\\:`
|
||||||
collections.faults.deleteMany({
|
}
|
||||||
_id: {
|
}),
|
||||||
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
|
db.operationsCollection.deleteMany({
|
||||||
},
|
_id: {
|
||||||
}),
|
$regex: `^${common.escapeRegExp(deviceId)}\\:`
|
||||||
collections.operations.deleteMany({
|
}
|
||||||
_id: {
|
}),
|
||||||
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
|
cache.del(`${deviceId}_tasks_faults_operations`)
|
||||||
},
|
]);
|
||||||
}),
|
|
||||||
]);
|
|
||||||
} finally {
|
|
||||||
await releaseLock(`cwmp_session_${deviceId}`, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteFault(id: string): Promise<void> {
|
|
||||||
const deviceId = id.split(":", 1)[0];
|
|
||||||
const channel = id.slice(deviceId.length + 1);
|
|
||||||
const token = await acquireLock(`cwmp_session_${deviceId}`, 5000);
|
|
||||||
if (!token) throw new ResourceLockedError("Device is in session");
|
|
||||||
try {
|
|
||||||
const proms = [dbDeleteFault(id)];
|
|
||||||
if (channel.startsWith("task_"))
|
|
||||||
proms.push(deleteTask(new ObjectId(channel.slice(5))));
|
|
||||||
await Promise.all(proms);
|
|
||||||
} finally {
|
|
||||||
await releaseLock(`cwmp_session_${deviceId}`, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteResource(
|
|
||||||
resource: string,
|
|
||||||
id: string,
|
|
||||||
): Promise<void> {
|
|
||||||
if (resource === "devices") {
|
|
||||||
await deleteDevice(id);
|
|
||||||
} else if (resource === "files") {
|
|
||||||
await deleteFile(id);
|
|
||||||
await cache.del("cwmp-local-cache-hash");
|
|
||||||
} else if (resource === "faults") {
|
|
||||||
await deleteFault(id);
|
|
||||||
} else if (resource === "provisions") {
|
|
||||||
await deleteProvision(id);
|
|
||||||
await cache.del("cwmp-local-cache-hash");
|
|
||||||
} else if (resource === "presets") {
|
|
||||||
await deletePreset(id);
|
|
||||||
await cache.del("cwmp-local-cache-hash");
|
|
||||||
} else if (resource === "virtualParameters") {
|
|
||||||
await deleteVirtualParameter(id);
|
|
||||||
await cache.del("cwmp-local-cache-hash");
|
|
||||||
} else if (resource === "config") {
|
|
||||||
await deleteConfig(id);
|
|
||||||
await Promise.all([
|
|
||||||
cache.del("ui-local-cache-hash"),
|
|
||||||
cache.del("cwmp-local-cache-hash"),
|
|
||||||
]);
|
|
||||||
} else if (resource === "permissions") {
|
|
||||||
await deletePermission(id);
|
|
||||||
await cache.del("ui-local-cache-hash");
|
|
||||||
} else if (resource === "users") {
|
|
||||||
await deleteUser(id);
|
|
||||||
await cache.del("ui-local-cache-hash");
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown resource ${resource}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Implement validation
|
|
||||||
export async function putResource(
|
|
||||||
resource: string,
|
|
||||||
id: string,
|
|
||||||
data: any,
|
|
||||||
): Promise<void> {
|
|
||||||
if (resource === "presets") {
|
|
||||||
await putPreset(id, data);
|
|
||||||
await cache.del("cwmp-local-cache-hash");
|
|
||||||
} else if (resource === "provisions") {
|
|
||||||
await putProvision(id, data);
|
|
||||||
await cache.del("cwmp-local-cache-hash");
|
|
||||||
} else if (resource === "virtualParameters") {
|
|
||||||
await putVirtualParameter(id, data);
|
|
||||||
await cache.del("cwmp-local-cache-hash");
|
|
||||||
} else if (resource === "config") {
|
|
||||||
await putConfig(id, data);
|
|
||||||
await Promise.all([
|
|
||||||
cache.del("ui-local-cache-hash"),
|
|
||||||
cache.del("cwmp-local-cache-hash"),
|
|
||||||
]);
|
|
||||||
} else if (resource === "permissions") {
|
|
||||||
await putPermission(id, data);
|
|
||||||
await cache.del("ui-local-cache-hash");
|
|
||||||
} else if (resource === "users") {
|
|
||||||
delete data["password"];
|
|
||||||
delete data["salt"];
|
|
||||||
await putUser(id, data);
|
|
||||||
await cache.del("ui-local-cache-hash");
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown resource ${resource}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function authLocal(
|
|
||||||
snapshot: string,
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const users = getUsers(snapshot);
|
|
||||||
const user = users[username];
|
|
||||||
if (!user?.password) return void resolve(null);
|
|
||||||
hashPassword(password, user.salt)
|
|
||||||
.then((hash) => {
|
|
||||||
if (hash === user.password) resolve(true);
|
|
||||||
else resolve(false);
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
+46
-22
@@ -1,16 +1,33 @@
|
|||||||
import { createHash, randomBytes, pbkdf2 } from "node:crypto";
|
/**
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
function parseHeaderFeilds(str: string): Record<string, string> {
|
import { createHash, randomBytes, pbkdf2 } from "crypto";
|
||||||
|
|
||||||
|
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)) {
|
||||||
@@ -33,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 };
|
||||||
@@ -44,7 +59,7 @@ export function parseAuthorizationHeader(authHeader: string): {
|
|||||||
// Inspired by https://github.com/jshttp/basic-auth
|
// Inspired by https://github.com/jshttp/basic-auth
|
||||||
const USER_PASS_REGEX = /^([^:]*):(.*)$/;
|
const USER_PASS_REGEX = /^([^:]*):(.*)$/;
|
||||||
const creds = USER_PASS_REGEX.exec(
|
const creds = USER_PASS_REGEX.exec(
|
||||||
Buffer.from(authHeader.slice(method.length + 1), "base64").toString(),
|
Buffer.from(authHeader.slice(method.length + 1), "base64").toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!creds) throw new Error("Unable to parse auth header");
|
if (!creds) throw new Error("Unable to parse auth header");
|
||||||
@@ -57,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 };
|
||||||
@@ -67,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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,15 +94,23 @@ export function digest(
|
|||||||
qop?: string | Buffer,
|
qop?: string | Buffer,
|
||||||
body?: string | Buffer,
|
body?: string | Buffer,
|
||||||
cnonce?: string | Buffer,
|
cnonce?: string | Buffer,
|
||||||
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")
|
||||||
@@ -101,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(":")
|
||||||
@@ -122,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";
|
||||||
@@ -144,7 +168,7 @@ export function solveDigest(
|
|||||||
qop,
|
qop,
|
||||||
body,
|
body,
|
||||||
cnonce,
|
cnonce,
|
||||||
nc,
|
nc
|
||||||
);
|
);
|
||||||
|
|
||||||
let authString = `Digest username="${username}"`;
|
let authString = `Digest username="${username}"`;
|
||||||
@@ -159,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);
|
||||||
@@ -168,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);
|
||||||
|
|||||||
+125
-20
@@ -1,35 +1,140 @@
|
|||||||
import { collections } from "./db/db.ts";
|
/**
|
||||||
import * as config from "./config.ts";
|
* 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 { MongoClient, Collection } from "mongodb";
|
||||||
|
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");
|
||||||
|
|
||||||
export async function get(key: string): Promise<string> {
|
let clientPromise: Promise<MongoClient>;
|
||||||
const res = await collections.cache.findOne({ _id: key });
|
let mongoCollection: Collection;
|
||||||
return res?.value;
|
let mongoTimeOffset = 0;
|
||||||
|
|
||||||
|
export async function connect(): Promise<void> {
|
||||||
|
const MONGODB_CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
|
||||||
|
clientPromise = MongoClient.connect(MONGODB_CONNECTION_URL, {
|
||||||
|
useNewUrlParser: true
|
||||||
|
});
|
||||||
|
const db = (await clientPromise).db();
|
||||||
|
mongoCollection = db.collection("cache");
|
||||||
|
await mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
|
||||||
|
const now = Date.now();
|
||||||
|
const res = await db.command({ hostInfo: 1 });
|
||||||
|
mongoTimeOffset = res.system.currentTime.getTime() - now;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function del(key: string): Promise<void> {
|
export async function disconnect(): Promise<void> {
|
||||||
await collections.cache.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 collections.cache.replaceOne(
|
|
||||||
{ _id: key },
|
{ _id: key },
|
||||||
{ value, expire, timestamp },
|
{ _id: key, value: value, expire: expire },
|
||||||
{ upsert: true },
|
{ upsert: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pop(key: string): Promise<string> {
|
export async function pop(key): Promise<any> {
|
||||||
const res = await collections.cache.findOneAndDelete({ _id: key });
|
const res = await mongoCollection.findOneAndDelete({ _id: key });
|
||||||
return res.value?.value;
|
|
||||||
|
if (
|
||||||
|
res &&
|
||||||
|
res["value"] &&
|
||||||
|
+res["value"]["expire"] - (Date.now() - mongoTimeOffset)
|
||||||
|
)
|
||||||
|
return res["value"]["value"];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function lock(lockName, ttl): Promise<Function> {
|
||||||
|
const token = Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.slice(2);
|
||||||
|
|
||||||
|
async function unlockOrExtend(extendTtl): Promise<void> {
|
||||||
|
if (!extendTtl) {
|
||||||
|
const res = await mongoCollection.deleteOne({
|
||||||
|
_id: lockName,
|
||||||
|
value: token
|
||||||
|
});
|
||||||
|
if (res["result"]["n"] !== 1) throw new Error("Lock expired");
|
||||||
|
} else {
|
||||||
|
const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000);
|
||||||
|
const res = await mongoCollection.updateOne(
|
||||||
|
{ _id: lockName, value: token },
|
||||||
|
{ expire: expire }
|
||||||
|
);
|
||||||
|
if (res["result"]["n"] !== 1) throw new Error("Lock expired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expireTest = new Date(Date.now() - mongoTimeOffset);
|
||||||
|
const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mongoCollection.updateOne(
|
||||||
|
{ _id: lockName, expire: { $lte: expireTest } },
|
||||||
|
{ $set: { value: token, expire: expireSet } },
|
||||||
|
{ upsert: true }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (err && err.code === 11000) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
return lock(lockName, ttl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlockOrExtend;
|
||||||
}
|
}
|
||||||
+28
-13
@@ -1,11 +1,30 @@
|
|||||||
import cluster, { Worker } from "node:cluster";
|
/**
|
||||||
import { cpus } from "node:os";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import * as logger from "./logger.ts";
|
*
|
||||||
|
* 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 cluster from "cluster";
|
||||||
|
import { cpus } from "os";
|
||||||
|
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
|
||||||
@@ -22,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;
|
||||||
@@ -38,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;
|
||||||
@@ -53,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;
|
||||||
}
|
}
|
||||||
@@ -70,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) &&
|
||||||
@@ -85,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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const UNDEFINED_TYPE = "[object Undefined]";
|
||||||
|
export const NULL_TYPE = "[object Null]";
|
||||||
|
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
|
||||||
|
function esc(str): string {
|
||||||
|
return str.replace(/[^A-Za-z0-9_]/g, chr => {
|
||||||
|
const buf = Buffer.from(chr, "utf8");
|
||||||
|
let rep = "";
|
||||||
|
for (const b of buf) rep += "%" + b.toString(16).toUpperCase();
|
||||||
|
return rep;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guaranteeing globally unique id as defined in TR-069
|
||||||
|
if (deviceIdStruct["ProductClass"]) {
|
||||||
|
return (
|
||||||
|
esc(deviceIdStruct["OUI"]) +
|
||||||
|
"-" +
|
||||||
|
esc(deviceIdStruct["ProductClass"]) +
|
||||||
|
"-" +
|
||||||
|
esc(deviceIdStruct["SerialNumber"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source: http://stackoverflow.com/a/6969486
|
||||||
|
export function escapeRegExp(str): string {
|
||||||
|
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
||||||
|
}
|
||||||
+36
-17
@@ -1,16 +1,35 @@
|
|||||||
import { PermissionSet, Expression } from "../types.ts";
|
/**
|
||||||
import { evaluate, or } from "./expression/util.ts";
|
* 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 { PermissionSet, Expression } from "../types";
|
||||||
|
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();
|
||||||
@@ -58,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);
|
||||||
@@ -80,7 +99,7 @@ export default class Authorizer {
|
|||||||
const validator = (
|
const validator = (
|
||||||
mutationType: string,
|
mutationType: string,
|
||||||
mutation: any,
|
mutation: any,
|
||||||
any: any,
|
any: any
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!validators.length) return false;
|
if (!validators.length) return false;
|
||||||
|
|
||||||
@@ -89,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 => {
|
||||||
@@ -99,13 +118,13 @@ export default class Authorizer {
|
|||||||
if (["mutation", "options"].includes(entry)) {
|
if (["mutation", "options"].includes(entry)) {
|
||||||
value = object[entry];
|
value = object[entry];
|
||||||
for (const seg of paramName.split(".")) {
|
for (const seg of paramName.split(".")) {
|
||||||
if (value == null) break;
|
// typeof null is "object"
|
||||||
if (typeof value !== "object") value = null;
|
if (value != null && typeof value !== "object") value = null;
|
||||||
else value = value[seg];
|
else value = value[seg];
|
||||||
|
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;
|
||||||
@@ -114,7 +133,7 @@ export default class Authorizer {
|
|||||||
const res = evaluate(
|
const res = evaluate(
|
||||||
validators.length > 1 ? ["OR", validators] : validators[0],
|
validators.length > 1 ? ["OR", validators] : validators[0],
|
||||||
valueFunction,
|
valueFunction,
|
||||||
Date.now(),
|
Date.now()
|
||||||
);
|
);
|
||||||
return !Array.isArray(res) && !!res;
|
return !Array.isArray(res) && !!res;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
export default function debounce<T>(
|
|
||||||
func: (args: T[]) => void,
|
|
||||||
timeout: number,
|
|
||||||
): (arg: T) => void {
|
|
||||||
let timer: ReturnType<typeof setTimeout>;
|
|
||||||
let args: T[] = [];
|
|
||||||
return (arg: T) => {
|
|
||||||
args.push(arg);
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
const argscopy = args;
|
|
||||||
args = [];
|
|
||||||
func(argscopy);
|
|
||||||
}, timeout);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export class ResourceLockedError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.name = "ResourceLockedError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -1,5 +1,24 @@
|
|||||||
import parsimmon from "parsimmon";
|
/**
|
||||||
import { Expression } from "../../types.ts";
|
* 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 parsimmon from "parsimmon";
|
||||||
|
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").
|
||||||
function interpretEscapes(str): string {
|
function interpretEscapes(str): string {
|
||||||
@@ -8,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));
|
||||||
@@ -21,7 +40,7 @@ function interpretEscapes(str): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function map<T>(exp: Expression, callback: (e: Expression) => T): T {
|
export function map(exp, callback): Expression {
|
||||||
if (!Array.isArray(exp)) return callback(exp);
|
if (!Array.isArray(exp)) return callback(exp);
|
||||||
|
|
||||||
let clone;
|
let clone;
|
||||||
@@ -36,10 +55,7 @@ export function map<T>(exp: Expression, callback: (e: Expression) => T): T {
|
|||||||
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;
|
||||||
@@ -54,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(),
|
||||||
@@ -65,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(">="),
|
||||||
@@ -79,23 +94,26 @@ const lang = parsimmon.createLanguage({
|
|||||||
parsimmon.string("<="),
|
parsimmon.string("<="),
|
||||||
parsimmon.string("="),
|
parsimmon.string("="),
|
||||||
parsimmon.string(">"),
|
parsimmon.string(">"),
|
||||||
parsimmon.string("<"),
|
parsimmon.string("<")
|
||||||
)
|
)
|
||||||
.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")
|
||||||
.desc("NOT LIKE"),
|
.desc("NOT LIKE")
|
||||||
)
|
)
|
||||||
.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
|
||||||
@@ -105,12 +123,12 @@ const lang = parsimmon.createLanguage({
|
|||||||
parsimmon
|
parsimmon
|
||||||
.regexp(/is\s+not\s+null/i)
|
.regexp(/is\s+not\s+null/i)
|
||||||
.result("IS NOT NULL")
|
.result("IS NOT NULL")
|
||||||
.desc("IS NOT NULL"),
|
.desc("IS NOT NULL")
|
||||||
)
|
)
|
||||||
.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")
|
||||||
@@ -118,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")
|
||||||
@@ -126,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")
|
||||||
@@ -134,53 +152,58 @@ 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_.*-]+/),
|
||||||
r.Expression.wrap(
|
r.Expression.wrap(
|
||||||
parsimmon.string("{").skip(parsimmon.optWhitespace),
|
parsimmon.string("{").skip(parsimmon.optWhitespace),
|
||||||
parsimmon.string("}"),
|
parsimmon.string("}")
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
.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_]/))
|
||||||
@@ -188,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(
|
||||||
@@ -263,32 +245,28 @@ 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,
|
||||||
r.Parameter,
|
r.Parameter,
|
||||||
r.Expression.wrap(
|
r.Expression.wrap(
|
||||||
parsimmon.string("(").skip(parsimmon.optWhitespace),
|
parsimmon.string("(").skip(parsimmon.optWhitespace),
|
||||||
parsimmon.string(")").skip(parsimmon.optWhitespace),
|
parsimmon.string(")").skip(parsimmon.optWhitespace)
|
||||||
),
|
)
|
||||||
),
|
)
|
||||||
),
|
)
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
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(
|
||||||
r.ValueExpression,
|
r.ValueExpression,
|
||||||
r.ComparisonOperator,
|
r.ComparisonOperator,
|
||||||
r.ValueExpression,
|
r.ValueExpression,
|
||||||
(p, o, v) => [o, p, v],
|
(p, o, v) => [o, p, v]
|
||||||
),
|
),
|
||||||
parsimmon.seqMap(
|
parsimmon.seqMap(
|
||||||
r.ValueExpression,
|
r.ValueExpression,
|
||||||
@@ -298,29 +276,21 @@ const lang = parsimmon.createLanguage({
|
|||||||
.regexp(/escape/i)
|
.regexp(/escape/i)
|
||||||
.result("ESCAPE")
|
.result("ESCAPE")
|
||||||
.skip(parsimmon.whitespace)
|
.skip(parsimmon.whitespace)
|
||||||
.desc("ESCAPE"),
|
.desc("ESCAPE")
|
||||||
),
|
),
|
||||||
r.ValueExpression,
|
r.ValueExpression,
|
||||||
(a, b, c, d) => [b, a, c, d],
|
(a, b, c, d) => [b, a, c, d]
|
||||||
),
|
),
|
||||||
parsimmon.seqMap(
|
parsimmon.seqMap(
|
||||||
r.ValueExpression,
|
r.ValueExpression,
|
||||||
r.LikeOperator,
|
r.LikeOperator,
|
||||||
r.ValueExpression,
|
r.ValueExpression,
|
||||||
(a, b, c) => [b, a, c],
|
(a, b, c) => [b, a, c]
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,10 +298,10 @@ const lang = parsimmon.createLanguage({
|
|||||||
r.OrOperator,
|
r.OrOperator,
|
||||||
binaryLeft(
|
binaryLeft(
|
||||||
r.AndOperator,
|
r.AndOperator,
|
||||||
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 {
|
||||||
@@ -339,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 = {
|
||||||
@@ -365,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();
|
||||||
@@ -380,8 +344,8 @@ 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") {
|
||||||
if (typeof exp[1] === "string") {
|
if (typeof exp[1] === "string") {
|
||||||
@@ -390,11 +354,11 @@ 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)}}`;
|
||||||
})
|
})
|
||||||
.join(""),
|
.join("")
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return wrap(`{${stringify(exp[1])}}`);
|
return wrap(`{${stringify(exp[1])}}`);
|
||||||
@@ -406,28 +370,17 @@ export function stringify(exp: Expression, level = 0): string {
|
|||||||
return wrap(
|
return wrap(
|
||||||
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
|
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
|
||||||
exp[2],
|
exp[2],
|
||||||
opLevels[op],
|
opLevels[op]
|
||||||
)} ESCAPE ${stringify(exp[3], opLevels[op])}`,
|
)} ESCAPE ${stringify(exp[3], opLevels[op])}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return wrap(
|
return wrap(
|
||||||
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
|
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
|
||||||
exp[2],
|
exp[2],
|
||||||
opLevels[op],
|
opLevels[op]
|
||||||
)}`,
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} 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));
|
||||||
@@ -440,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) {
|
||||||
@@ -455,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);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
/**
|
||||||
|
* 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, mapAsync, parseLikePattern } from "./expression-parser";
|
||||||
|
import { booleanCnf } from "./expression-cnf";
|
||||||
|
import { naiveDpll } from "./sat-solver";
|
||||||
|
import { Expression } from "../types";
|
||||||
|
|
||||||
|
const isArray = Array.isArray;
|
||||||
|
|
||||||
|
const regExpCache = new WeakMap<object, RegExp>();
|
||||||
|
|
||||||
|
const REDUCE_SKIP = {};
|
||||||
|
function reduce(exp, callback): Expression {
|
||||||
|
let loop = true;
|
||||||
|
while (loop) {
|
||||||
|
loop = false;
|
||||||
|
for (let i = 2; i < exp.length; ++i) {
|
||||||
|
const res = callback(exp[i - 1], exp[i], i - 2);
|
||||||
|
if (res !== REDUCE_SKIP) {
|
||||||
|
loop = true;
|
||||||
|
exp = exp.slice();
|
||||||
|
exp.splice(i - 1, 2, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exp.length === 2) return exp[1];
|
||||||
|
return exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function likePatternToRegExp(pat, esc = "", flags = ""): RegExp {
|
||||||
|
const convChars = {
|
||||||
|
"-": "\\-",
|
||||||
|
"/": "\\/",
|
||||||
|
"\\": "\\/",
|
||||||
|
"^": "\\^",
|
||||||
|
$: "\\$",
|
||||||
|
"*": "\\*",
|
||||||
|
"+": "\\+",
|
||||||
|
"?": "\\?",
|
||||||
|
".": "\\.",
|
||||||
|
"(": "\\(",
|
||||||
|
")": "\\)",
|
||||||
|
"|": "\\|",
|
||||||
|
"[": "\\[",
|
||||||
|
"]": "\\]",
|
||||||
|
"{": "\\{",
|
||||||
|
"}": "\\}",
|
||||||
|
"\\%": ".*",
|
||||||
|
"\\_": "."
|
||||||
|
};
|
||||||
|
let chars = parseLikePattern(pat, esc);
|
||||||
|
if (!chars.length) return new RegExp("^$", flags);
|
||||||
|
chars = chars.map(c => convChars[c] || c);
|
||||||
|
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
|
||||||
|
const l = chars.length - 1;
|
||||||
|
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
|
||||||
|
return new RegExp(chars.join(""), flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
function evalExp(e: Expression): Expression {
|
||||||
|
if (e[0] === "AND") {
|
||||||
|
return reduce(e, (a, b) => {
|
||||||
|
if (!isArray(a)) return a ? b : a;
|
||||||
|
if (!isArray(b)) return b ? a : b;
|
||||||
|
return REDUCE_SKIP;
|
||||||
|
});
|
||||||
|
} else if (e[0] === "OR") {
|
||||||
|
return reduce(e, (a, b) => {
|
||||||
|
if (!isArray(a)) return a ? a : b;
|
||||||
|
if (!isArray(b)) return b ? b : a;
|
||||||
|
return REDUCE_SKIP;
|
||||||
|
});
|
||||||
|
} else if (e[0] === "NOT") {
|
||||||
|
if (!isArray(e[1])) return !e[1];
|
||||||
|
else if (e[1][0] === "NOT") return e[1][1];
|
||||||
|
} else if (e[0] === "IS NULL") {
|
||||||
|
if (isArray(e[1])) return e;
|
||||||
|
else if (e[1] == null) return true;
|
||||||
|
else return null;
|
||||||
|
} else if (e[0] === "IS NOT NULL") {
|
||||||
|
if (isArray(e[1])) return e;
|
||||||
|
else if (e[1] != null) return true;
|
||||||
|
else return null;
|
||||||
|
} else if (e[0] === "LIKE") {
|
||||||
|
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
|
||||||
|
else if (
|
||||||
|
e[1] == null ||
|
||||||
|
e[2] == null ||
|
||||||
|
((e as any[]).length >= 4 && e[3] == null)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
let r = regExpCache.get(e as any[]);
|
||||||
|
if (!r) {
|
||||||
|
r = likePatternToRegExp(e[2], e[3]);
|
||||||
|
regExpCache.set(e as any[], r);
|
||||||
|
}
|
||||||
|
return r.test(e[1]);
|
||||||
|
} else if (e[0] === "NOT LIKE") {
|
||||||
|
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
|
||||||
|
else if (
|
||||||
|
e[1] == null ||
|
||||||
|
e[2] == null ||
|
||||||
|
((e as any[]).length >= 4 && e[3] == null)
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
let r = regExpCache.get(e as any[]);
|
||||||
|
if (!r) {
|
||||||
|
r = likePatternToRegExp(e[2], e[3]);
|
||||||
|
regExpCache.set(e as any[], r);
|
||||||
|
}
|
||||||
|
return !r.test(e[1]);
|
||||||
|
} else if (e[0] === "=") {
|
||||||
|
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||||
|
if (e[1] == null || e[2] == null) return null;
|
||||||
|
return e[1] === e[2];
|
||||||
|
} else if (e[0] === "<>") {
|
||||||
|
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||||
|
if (e[1] == null || e[2] == null) return null;
|
||||||
|
return e[1] !== e[2];
|
||||||
|
} else if (e[0] === ">") {
|
||||||
|
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||||
|
if (e[1] == null || e[2] == null) return null;
|
||||||
|
return e[1] > e[2];
|
||||||
|
} else if (e[0] === ">=") {
|
||||||
|
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||||
|
if (e[1] == null || e[2] == null) return null;
|
||||||
|
return e[1] >= e[2];
|
||||||
|
} else if (e[0] === "<") {
|
||||||
|
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||||
|
if (e[1] == null || e[2] == null) return null;
|
||||||
|
return e[1] < e[2];
|
||||||
|
} else if (e[0] === "<=") {
|
||||||
|
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||||
|
if (e[1] == null || e[2] == null) return null;
|
||||||
|
return e[1] <= e[2];
|
||||||
|
} else if (e[0] === "*") {
|
||||||
|
return reduce(e, (a, b) => {
|
||||||
|
if (!isArray(a) && !isArray(b)) {
|
||||||
|
if (a == null || b == null) return null;
|
||||||
|
return a * b;
|
||||||
|
}
|
||||||
|
return REDUCE_SKIP;
|
||||||
|
});
|
||||||
|
} else if (e[0] === "/") {
|
||||||
|
return reduce(e, (a, b, i) => {
|
||||||
|
if (!isArray(a) && !isArray(b)) {
|
||||||
|
if (a == null || b == null) return null;
|
||||||
|
return i === 0 ? a / b : a * b;
|
||||||
|
}
|
||||||
|
return REDUCE_SKIP;
|
||||||
|
});
|
||||||
|
} else if (e[0] === "+") {
|
||||||
|
return reduce(e, (a, b) => {
|
||||||
|
if (!isArray(a) && !isArray(b)) {
|
||||||
|
if (a == null || b == null) return null;
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
return REDUCE_SKIP;
|
||||||
|
});
|
||||||
|
} else if (e[0] === "-") {
|
||||||
|
return reduce(e, (a, b, i) => {
|
||||||
|
if (!isArray(a) && !isArray(b)) {
|
||||||
|
if (a == null || b == null) return null;
|
||||||
|
return i === 0 ? a - b : a + b;
|
||||||
|
}
|
||||||
|
return REDUCE_SKIP;
|
||||||
|
});
|
||||||
|
} else if (e[0] === "||") {
|
||||||
|
return reduce(e, (a, b) => {
|
||||||
|
if (!isArray(a) && !isArray(b)) {
|
||||||
|
if (a == null || b == null) return null;
|
||||||
|
return `${a}${b}`;
|
||||||
|
}
|
||||||
|
return REDUCE_SKIP;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluate(
|
||||||
|
exp,
|
||||||
|
obj,
|
||||||
|
now: number,
|
||||||
|
cb?: Function
|
||||||
|
): string | number | boolean | null;
|
||||||
|
export function evaluate(exp, obj?, now?: number, cb?: Function): Expression;
|
||||||
|
export function evaluate(exp, obj?, now?: number, cb?: Function): Expression {
|
||||||
|
return map(exp, e => {
|
||||||
|
if (cb) e = cb(e);
|
||||||
|
if (!isArray(e)) return e;
|
||||||
|
|
||||||
|
if (e[0] === "FUNC") {
|
||||||
|
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") {
|
||||||
|
if (e[1] == null) return null;
|
||||||
|
if (obj && !isArray(e[1])) {
|
||||||
|
let v;
|
||||||
|
if (typeof obj === "function") v = obj(e[1]);
|
||||||
|
else v = obj[e[1]];
|
||||||
|
|
||||||
|
if (v == null) return null;
|
||||||
|
if (typeof v === "object") v = v.value ? v.value[0] : null;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return evalExp(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function evaluateAsync(
|
||||||
|
exp,
|
||||||
|
obj,
|
||||||
|
now: number,
|
||||||
|
cb?: Function
|
||||||
|
): Promise<string | number | boolean | null>;
|
||||||
|
export async function evaluateAsync(
|
||||||
|
exp,
|
||||||
|
obj?,
|
||||||
|
now?: number,
|
||||||
|
cb?: Function
|
||||||
|
): Promise<Expression>;
|
||||||
|
export async function evaluateAsync(
|
||||||
|
exp,
|
||||||
|
obj?,
|
||||||
|
now?: number,
|
||||||
|
cb?: Function
|
||||||
|
): Promise<Expression> {
|
||||||
|
return mapAsync(exp, async e => {
|
||||||
|
if (cb) e = await cb(e);
|
||||||
|
if (!isArray(e)) return e;
|
||||||
|
|
||||||
|
if (e[0] === "FUNC") {
|
||||||
|
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") {
|
||||||
|
if (e[1] == null) return null;
|
||||||
|
if (obj && !isArray(e[1])) {
|
||||||
|
let v = obj[e[1]];
|
||||||
|
if (v == null) return null;
|
||||||
|
if (typeof v === "object") v = v.value ? v.value[0] : null;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return evalExp(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function and(exp1: Expression, exp2: Expression): Expression {
|
||||||
|
if (!isArray(exp1)) return exp1 ? exp2 : exp1;
|
||||||
|
if (!isArray(exp2)) return exp2 ? exp1 : exp2;
|
||||||
|
|
||||||
|
let res: Expression = ["AND"];
|
||||||
|
|
||||||
|
if (exp1[0] === "AND") res = res.concat(exp1.slice(1));
|
||||||
|
else res.push(exp1);
|
||||||
|
|
||||||
|
if (exp2[0] === "AND") res = res.concat(exp2.slice(1));
|
||||||
|
else res.push(exp2);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function or(exp1: Expression, exp2: Expression): Expression {
|
||||||
|
if (!isArray(exp1)) return exp1 ? exp1 : exp2;
|
||||||
|
if (!isArray(exp2)) return exp2 ? exp2 : exp1;
|
||||||
|
|
||||||
|
let res: Expression = ["OR"];
|
||||||
|
|
||||||
|
if (exp1[0] === "OR") res = res.concat(exp1.slice(1));
|
||||||
|
else res.push(exp1);
|
||||||
|
|
||||||
|
if (exp2[0] === "OR") res = res.concat(exp2.slice(1));
|
||||||
|
else res.push(exp2);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>();
|
||||||
|
map(exp, e => {
|
||||||
|
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
return Array.from(params);
|
||||||
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
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, type _bigint as bigint };
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
import * as BI from "./bigint.ts";
|
|
||||||
import { Expression } from "../../types.ts";
|
|
||||||
import { map } from "./parser.ts";
|
|
||||||
import { and, evaluateCallback } from "./util.ts";
|
|
||||||
|
|
||||||
const ZERO = BI.BigInt(0);
|
|
||||||
const ONE = BI.BigInt(1);
|
|
||||||
const TWO = BI.BigInt(2);
|
|
||||||
const NEGATIVE_ONE = BI.BigInt(-1);
|
|
||||||
|
|
||||||
class Indeterminates {
|
|
||||||
public declare map: Map<string, number>;
|
|
||||||
public declare sortedKeys: string[];
|
|
||||||
|
|
||||||
public constructor(str?: string) {
|
|
||||||
this.map = new Map();
|
|
||||||
if (str) {
|
|
||||||
this.map.set(str, 1);
|
|
||||||
this.sortedKeys = [str];
|
|
||||||
} else {
|
|
||||||
this.sortedKeys = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public reciprocal(): Indeterminates {
|
|
||||||
const res = new Indeterminates();
|
|
||||||
res.sortedKeys = this.sortedKeys;
|
|
||||||
res.map = new Map();
|
|
||||||
for (const [k, v] of this.map) res.map.set(k, 0 - v);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static multiply(
|
|
||||||
indeterminates1: Indeterminates,
|
|
||||||
indeterminates2: Indeterminates,
|
|
||||||
): Indeterminates {
|
|
||||||
const res = new Indeterminates();
|
|
||||||
res.sortedKeys = indeterminates1.sortedKeys.slice();
|
|
||||||
res.map = new Map(indeterminates1.map);
|
|
||||||
|
|
||||||
for (const [key, val] of indeterminates2.map) {
|
|
||||||
const v = res.map.get(key);
|
|
||||||
if (!v) {
|
|
||||||
res.map.set(key, val);
|
|
||||||
res.sortedKeys.push(key);
|
|
||||||
} else {
|
|
||||||
const v2 = val + v;
|
|
||||||
if (!v2) {
|
|
||||||
res.map.delete(key);
|
|
||||||
res.sortedKeys = res.sortedKeys.filter((s) => s !== key);
|
|
||||||
} else {
|
|
||||||
res.map.set(key, v2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.sortedKeys.sort((a, b) => {
|
|
||||||
if (a.length !== b.length) return b.length - a.length;
|
|
||||||
else if (a > b) return 1;
|
|
||||||
else if (a < b) return -1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static compare(a: Indeterminates, b: Indeterminates): number {
|
|
||||||
if (a.sortedKeys.length !== b.sortedKeys.length)
|
|
||||||
return b.sortedKeys.length - a.sortedKeys.length;
|
|
||||||
for (let i = 0; i < a.sortedKeys.length; ++i) {
|
|
||||||
const k1 = a.sortedKeys[i];
|
|
||||||
const w1 = a.map.get(k1);
|
|
||||||
const k2 = b.sortedKeys[i];
|
|
||||||
const w2 = b.map.get(k2);
|
|
||||||
if (w1 !== w2) return w2 - w1;
|
|
||||||
if (k1.length > k2.length) return -1;
|
|
||||||
else if (k1.length < k2.length) return 1;
|
|
||||||
else if (k1 > k2) return 1;
|
|
||||||
else if (k1 < k2) return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Term {
|
|
||||||
indeterminates: Indeterminates;
|
|
||||||
coefficientNumerator: BI.bigint;
|
|
||||||
coefficientDenominator: BI.bigint;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findGcd(a: BI.bigint, b: BI.bigint): BI.bigint {
|
|
||||||
while (BI.ne(b, ZERO)) {
|
|
||||||
const t = b;
|
|
||||||
b = BI.rem(a, b);
|
|
||||||
a = t;
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Polynomial {
|
|
||||||
public declare terms: Term[];
|
|
||||||
|
|
||||||
public constructor(terms: Term[]) {
|
|
||||||
this.terms = terms;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static simplifyTerms(terms: Term[]): Term[] {
|
|
||||||
const ts = terms
|
|
||||||
.slice()
|
|
||||||
.sort((a: Term, b: Term) =>
|
|
||||||
Indeterminates.compare(a.indeterminates, b.indeterminates),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 1; i < ts.length; ++i) {
|
|
||||||
const t1 = ts[i - 1];
|
|
||||||
const t2 = ts[i];
|
|
||||||
if (Indeterminates.compare(t1.indeterminates, t2.indeterminates) === 0) {
|
|
||||||
const numerator = BI.add(
|
|
||||||
BI.mul(t1.coefficientNumerator, t2.coefficientDenominator),
|
|
||||||
BI.mul(t2.coefficientNumerator, t1.coefficientDenominator),
|
|
||||||
);
|
|
||||||
|
|
||||||
const denominator = BI.mul(
|
|
||||||
t1.coefficientDenominator,
|
|
||||||
t2.coefficientDenominator,
|
|
||||||
);
|
|
||||||
|
|
||||||
const gcd = findGcd(numerator, denominator);
|
|
||||||
|
|
||||||
ts[i] = {
|
|
||||||
indeterminates: t2.indeterminates,
|
|
||||||
coefficientNumerator: BI.div(numerator, gcd),
|
|
||||||
coefficientDenominator: BI.div(denominator, gcd),
|
|
||||||
};
|
|
||||||
ts[i - 1] = {
|
|
||||||
indeterminates: t1.indeterminates,
|
|
||||||
coefficientNumerator: ZERO,
|
|
||||||
coefficientDenominator: t1.coefficientDenominator,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ts.filter((v) => BI.ne(v.coefficientNumerator, ZERO));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromIndeterminate(indeterminate: Expression): Polynomial {
|
|
||||||
const indeterminates = new Indeterminates(JSON.stringify(indeterminate));
|
|
||||||
const terms = [
|
|
||||||
{
|
|
||||||
indeterminates: indeterminates,
|
|
||||||
coefficientNumerator: ONE,
|
|
||||||
coefficientDenominator: ONE,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return new Polynomial(terms);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromConstant(constant: number): Polynomial {
|
|
||||||
const [int, frac] = Math.abs(constant).toString(2).split(".", 2);
|
|
||||||
let numerator = BI.BigInt("0b" + int);
|
|
||||||
if (constant < 0) numerator = BI.mul(numerator, NEGATIVE_ONE);
|
|
||||||
let denominator = ONE;
|
|
||||||
if (frac) {
|
|
||||||
denominator = BI.exp(TWO, BI.BigInt(frac.length));
|
|
||||||
numerator = BI.add(
|
|
||||||
BI.mul(numerator, denominator),
|
|
||||||
BI.BigInt("0b" + frac),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const terms = [
|
|
||||||
{
|
|
||||||
indeterminates: new Indeterminates(),
|
|
||||||
coefficientNumerator: numerator,
|
|
||||||
coefficientDenominator: denominator,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return new Polynomial(terms);
|
|
||||||
}
|
|
||||||
|
|
||||||
public negation(): Polynomial {
|
|
||||||
const terms = this.terms.map((t) => ({
|
|
||||||
indeterminates: t.indeterminates,
|
|
||||||
coefficientNumerator: BI.mul(t.coefficientNumerator, NEGATIVE_ONE),
|
|
||||||
coefficientDenominator: t.coefficientDenominator,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return new Polynomial(terms);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reciprocal(): Polynomial {
|
|
||||||
const terms = this.terms.map((t) => ({
|
|
||||||
indeterminates: t.indeterminates.reciprocal(),
|
|
||||||
coefficientNumerator: t.coefficientDenominator,
|
|
||||||
coefficientDenominator: t.coefficientNumerator,
|
|
||||||
}));
|
|
||||||
return new Polynomial(terms);
|
|
||||||
}
|
|
||||||
|
|
||||||
public constant(): Polynomial {
|
|
||||||
const terms = this.terms.filter((t) => !t.indeterminates.sortedKeys.length);
|
|
||||||
return new Polynomial(terms);
|
|
||||||
}
|
|
||||||
|
|
||||||
public add(rhs: Polynomial): Polynomial {
|
|
||||||
return new Polynomial(
|
|
||||||
Polynomial.simplifyTerms(this.terms.concat(rhs.terms)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public subtract(rhs: Polynomial): Polynomial {
|
|
||||||
return this.add(rhs.negation());
|
|
||||||
}
|
|
||||||
|
|
||||||
public multiply(rhs: Polynomial): Polynomial {
|
|
||||||
const terms: Term[] = [];
|
|
||||||
|
|
||||||
for (const t1 of this.terms) {
|
|
||||||
for (const t2 of rhs.terms) {
|
|
||||||
const numerator = BI.mul(
|
|
||||||
t1.coefficientNumerator,
|
|
||||||
t2.coefficientNumerator,
|
|
||||||
);
|
|
||||||
const denominator = BI.mul(
|
|
||||||
t1.coefficientDenominator,
|
|
||||||
t2.coefficientDenominator,
|
|
||||||
);
|
|
||||||
const gcd = findGcd(numerator, denominator);
|
|
||||||
|
|
||||||
terms.push({
|
|
||||||
indeterminates: Indeterminates.multiply(
|
|
||||||
t1.indeterminates,
|
|
||||||
t2.indeterminates,
|
|
||||||
),
|
|
||||||
coefficientNumerator: BI.div(numerator, gcd),
|
|
||||||
coefficientDenominator: BI.div(denominator, gcd),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Polynomial(Polynomial.simplifyTerms(terms));
|
|
||||||
}
|
|
||||||
|
|
||||||
public divide(rhs: Polynomial): Polynomial {
|
|
||||||
return this.multiply(rhs.reciprocal());
|
|
||||||
}
|
|
||||||
|
|
||||||
public toString(): string {
|
|
||||||
const add: string[] = [];
|
|
||||||
for (const t of this.terms) {
|
|
||||||
const coefficient =
|
|
||||||
BI.toNumber(t.coefficientNumerator) /
|
|
||||||
BI.toNumber(t.coefficientDenominator);
|
|
||||||
|
|
||||||
const mul: string[] = [];
|
|
||||||
if (t.indeterminates.sortedKeys.length) {
|
|
||||||
for (const k of t.indeterminates.sortedKeys) {
|
|
||||||
const w = t.indeterminates.map.get(k);
|
|
||||||
for (let i = Math.abs(w); i > 0; --i) {
|
|
||||||
if (w > 0) mul.push(k);
|
|
||||||
else mul.push(`["/",1,${k}]`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coefficient !== 1) mul.push(coefficient.toString());
|
|
||||||
|
|
||||||
if (mul.length > 1) add.push(`["*",${mul.join(",")}]`);
|
|
||||||
else add.push(mul["0"]);
|
|
||||||
} else {
|
|
||||||
add.push(coefficient.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!add.length) return "0";
|
|
||||||
else if (add.length === 1) return add[0];
|
|
||||||
else return `["+",${add.join(",")}]`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ADDITIVE_IDENTITY = Polynomial.fromConstant(0);
|
|
||||||
const MULTIPLICATIVE_IDENTITY = Polynomial.fromConstant(1);
|
|
||||||
|
|
||||||
const SWAPPED_OPS = {
|
|
||||||
"=": "=",
|
|
||||||
"<>": "<>",
|
|
||||||
">": "<",
|
|
||||||
">=": "<=",
|
|
||||||
"<": ">",
|
|
||||||
"<=": ">=",
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeCallback(exp: Expression): Expression {
|
|
||||||
if (!Array.isArray(exp)) return exp;
|
|
||||||
const op = exp[0];
|
|
||||||
|
|
||||||
if (op === "FUNC" && exp[1] === "COALESCE") {
|
|
||||||
const res: Expression[] = ["CASE"];
|
|
||||||
for (let i = 2; i < exp.length; ++i)
|
|
||||||
res.push(normalizeCallback(["IS NOT NULL", exp[i]]), exp[i]);
|
|
||||||
return normalizeCallback(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op === "CASE") {
|
|
||||||
const res = [] as [Expression, Expression][];
|
|
||||||
for (let i = 1; i < exp.length; i += 2) {
|
|
||||||
let w = exp[i];
|
|
||||||
if (w instanceof Polynomial) w = JSON.parse(w.toString());
|
|
||||||
if (!Array.isArray(w) && !w) continue;
|
|
||||||
const t = exp[i + 1];
|
|
||||||
if (!Array.isArray(t) || t[0] !== "CASE") {
|
|
||||||
res.push([w, t]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (let j = 1; j < t.length; j += 2) res.push([and(w, t[j]), t[j + 1]]);
|
|
||||||
res.push([w, null]);
|
|
||||||
if (!Array.isArray(w) && w) break;
|
|
||||||
}
|
|
||||||
while (res[res.length - 1][1] == null) res.pop();
|
|
||||||
return ["CASE", ...res.flat()];
|
|
||||||
}
|
|
||||||
|
|
||||||
const permutations: Map<number, [Expression, Expression][]> = new Map();
|
|
||||||
for (const [i, e] of exp.entries()) {
|
|
||||||
if (!Array.isArray(e)) continue;
|
|
||||||
if (e[0] !== "CASE") continue;
|
|
||||||
const perms: [Expression, Expression][] = [];
|
|
||||||
for (let j = 1; j < e.length; j += 2) perms.push([e[j], e[j + 1]]);
|
|
||||||
perms.push([true, null]);
|
|
||||||
permutations.set(i, perms);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (permutations.size) {
|
|
||||||
let res: [Expression, Expression][] = [[true, exp]];
|
|
||||||
for (const [i, perms] of permutations) {
|
|
||||||
const res2: [Expression, Expression][] = [];
|
|
||||||
for (const [w, t] of perms) {
|
|
||||||
res2.push(
|
|
||||||
...res.map((r) => {
|
|
||||||
const e = (r[1] as Expression[]).slice();
|
|
||||||
e[i] = t;
|
|
||||||
return [and(w, r[0]), e] as [Expression, Expression];
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (!Array.isArray(w) && w) break;
|
|
||||||
}
|
|
||||||
res = res2;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const r of res) r[1] = normalizeCallback(r[1]);
|
|
||||||
|
|
||||||
if (res[0][0] === true) return res[0][1];
|
|
||||||
while (res[res.length - 1][1] == null) res.pop();
|
|
||||||
|
|
||||||
return ["CASE", ...res.flat()];
|
|
||||||
}
|
|
||||||
|
|
||||||
function toPolynomial(e: Expression): Polynomial {
|
|
||||||
if (e == null) return null;
|
|
||||||
if (e instanceof Polynomial) return e;
|
|
||||||
if (typeof e === "number") return Polynomial.fromConstant(e);
|
|
||||||
if (typeof e === "string")
|
|
||||||
return Polynomial.fromConstant(parseFloat(e) || 0);
|
|
||||||
if (typeof e === "boolean") return Polynomial.fromConstant(+e);
|
|
||||||
return Polynomial.fromIndeterminate(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op === "+") {
|
|
||||||
const args: Polynomial[] = [];
|
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
|
||||||
const p = toPolynomial(exp[i]);
|
|
||||||
if (p == null) return null;
|
|
||||||
args.push(p);
|
|
||||||
}
|
|
||||||
return args.reduce(
|
|
||||||
(previousValue, currentValue) => previousValue.add(currentValue),
|
|
||||||
ADDITIVE_IDENTITY,
|
|
||||||
) as unknown as Expression;
|
|
||||||
} else if (op === "*") {
|
|
||||||
const args: Polynomial[] = [];
|
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
|
||||||
const p = toPolynomial(exp[i]);
|
|
||||||
if (p == null) return null;
|
|
||||||
args.push(p);
|
|
||||||
}
|
|
||||||
return args.reduce(
|
|
||||||
(previousValue, currentValue) => previousValue.multiply(currentValue),
|
|
||||||
MULTIPLICATIVE_IDENTITY,
|
|
||||||
) as unknown as Expression;
|
|
||||||
} else if (op === "-") {
|
|
||||||
const args: Polynomial[] = [];
|
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
|
||||||
const p = toPolynomial(exp[i]);
|
|
||||||
if (p == null) return null;
|
|
||||||
args.push(p);
|
|
||||||
}
|
|
||||||
return args.reduce((previousValue, currentValue) =>
|
|
||||||
previousValue.subtract(currentValue),
|
|
||||||
) as unknown as Expression;
|
|
||||||
} else if (op === "/") {
|
|
||||||
const args: Polynomial[] = [];
|
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
|
||||||
const p = toPolynomial(exp[i]);
|
|
||||||
if (p == null) return null;
|
|
||||||
args.push(p);
|
|
||||||
}
|
|
||||||
return args.reduce((previousValue, currentValue) =>
|
|
||||||
previousValue.divide(currentValue),
|
|
||||||
) as unknown as Expression;
|
|
||||||
} else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
let lhs: Polynomial, rhs: Polynomial;
|
|
||||||
if (exp[1] instanceof Polynomial) lhs = exp[1];
|
|
||||||
else if (typeof exp[1] === "number") lhs = Polynomial.fromConstant(exp[1]);
|
|
||||||
|
|
||||||
if (exp[2] instanceof Polynomial) rhs = exp[2];
|
|
||||||
else if (typeof exp[2] === "number") rhs = Polynomial.fromConstant(exp[2]);
|
|
||||||
|
|
||||||
if (lhs || rhs) {
|
|
||||||
if (!lhs) lhs = Polynomial.fromIndeterminate(exp[1]);
|
|
||||||
if (!rhs) rhs = Polynomial.fromIndeterminate(exp[2]);
|
|
||||||
|
|
||||||
lhs = lhs.subtract(rhs);
|
|
||||||
rhs = lhs.constant().negation();
|
|
||||||
lhs = lhs.add(rhs);
|
|
||||||
|
|
||||||
if (!lhs.terms.length) {
|
|
||||||
exp = [op, JSON.parse(lhs.toString()), JSON.parse(rhs.toString())];
|
|
||||||
|
|
||||||
if (op === "=") return exp[1] === exp[2];
|
|
||||||
else if (op === "<>") return exp[1] !== exp[2];
|
|
||||||
else if (op === ">") return exp[1] > exp[2];
|
|
||||||
else if (op === ">=") return exp[1] >= exp[2];
|
|
||||||
else if (op === "<") return exp[1] < exp[2];
|
|
||||||
else if (op === "<=") return exp[1] <= exp[2];
|
|
||||||
} else {
|
|
||||||
let flipOp = 1;
|
|
||||||
|
|
||||||
const n = lhs.terms[0].coefficientNumerator;
|
|
||||||
const d = lhs.terms[0].coefficientDenominator;
|
|
||||||
|
|
||||||
if (BI.lt(n, ZERO) || BI.lt(d, ZERO)) flipOp *= -1;
|
|
||||||
|
|
||||||
const reciprocal = new Polynomial([
|
|
||||||
{
|
|
||||||
indeterminates: new Indeterminates(),
|
|
||||||
coefficientNumerator: d,
|
|
||||||
coefficientDenominator: n,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
lhs = lhs.multiply(reciprocal);
|
|
||||||
rhs = rhs.multiply(reciprocal);
|
|
||||||
|
|
||||||
const keys = lhs.terms[0].indeterminates.sortedKeys;
|
|
||||||
let invert = lhs.terms[0].indeterminates.map.get(keys[0]) < 0 ? -1 : 0;
|
|
||||||
|
|
||||||
for (const t of lhs.terms)
|
|
||||||
for (const v of t.indeterminates.map.values()) invert += v;
|
|
||||||
|
|
||||||
if (invert < 0) {
|
|
||||||
flipOp *= -1;
|
|
||||||
lhs = lhs.reciprocal();
|
|
||||||
rhs = rhs.reciprocal();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flipOp < 0) exp = [SWAPPED_OPS[op], lhs, rhs];
|
|
||||||
else exp = [op, lhs, rhs];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore polynomial expressions
|
|
||||||
exp = exp.map((e) =>
|
|
||||||
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
|
|
||||||
);
|
|
||||||
|
|
||||||
exp = evaluateCallback(exp);
|
|
||||||
|
|
||||||
return exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function normalize(expr: Expression): Expression {
|
|
||||||
expr = map(expr, normalizeCallback);
|
|
||||||
if (expr instanceof Polynomial) {
|
|
||||||
expr = JSON.parse(expr.toString());
|
|
||||||
} else if (Array.isArray(expr) && expr[0] === "CASE") {
|
|
||||||
expr = expr.map((e) =>
|
|
||||||
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return expr;
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { complement } from "espresso-iisojs";
|
|
||||||
import { Expression } from "../../types";
|
|
||||||
import normalize from "./normalize";
|
|
||||||
import { Clause, SynthContext } from "./synth";
|
|
||||||
import { and, or } from "./util";
|
|
||||||
|
|
||||||
type Bookmark = Record<string, null | boolean | number | string>;
|
|
||||||
|
|
||||||
type Minterm = number[];
|
|
||||||
|
|
||||||
export function toBookmark(
|
|
||||||
sort: Record<string, number>,
|
|
||||||
row: unknown,
|
|
||||||
): Bookmark {
|
|
||||||
const bookmark: Bookmark = {};
|
|
||||||
for (const param of Object.keys(sort)) {
|
|
||||||
let v = row[param];
|
|
||||||
if (v != null && typeof v === "object") v = v.value?.[0];
|
|
||||||
bookmark[param] = v;
|
|
||||||
}
|
|
||||||
return bookmark;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bookmarkToExpression(
|
|
||||||
bookmark: Bookmark,
|
|
||||||
sort: Record<string, number>,
|
|
||||||
): Expression {
|
|
||||||
return Object.entries(sort)
|
|
||||||
.reverse()
|
|
||||||
.reduce((cur, kv) => {
|
|
||||||
const [param, asc] = kv;
|
|
||||||
if (asc < 0) {
|
|
||||||
if (bookmark[param] == null) {
|
|
||||||
return or(
|
|
||||||
["IS NOT NULL", ["PARAM", param]],
|
|
||||||
and(["IS NULL", ["PARAM", param]], cur),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let f = null;
|
|
||||||
f = or(f, [">", ["PARAM", param], bookmark[param]]);
|
|
||||||
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
|
|
||||||
} else {
|
|
||||||
let f: Expression = ["IS NULL", ["PARAM", param]];
|
|
||||||
if (bookmark[param] == null) return and(f, cur);
|
|
||||||
f = or(f, ["<", ["PARAM", param], bookmark[param]]);
|
|
||||||
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
|
|
||||||
}
|
|
||||||
}, true as Expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCover(
|
|
||||||
context: SynthContext,
|
|
||||||
minterm: Minterm,
|
|
||||||
allSort: [string, number][],
|
|
||||||
): Minterm[] {
|
|
||||||
if (!allSort.length) return [[]];
|
|
||||||
const [param, sort] = allSort[0];
|
|
||||||
|
|
||||||
const cov: Minterm = [];
|
|
||||||
const nextCov: Minterm = [];
|
|
||||||
|
|
||||||
if (sort > 0) {
|
|
||||||
const lhs = new Clause.Exp(["PARAM", param]);
|
|
||||||
const isNull = context.getVar(new Clause.IsNull(lhs));
|
|
||||||
cov.push((isNull << 2) ^ 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const m of minterm) {
|
|
||||||
const clause = context.getClause(m >>> 2);
|
|
||||||
if (clause instanceof Clause.IsNull) {
|
|
||||||
const lhs = clause.operand.expression();
|
|
||||||
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
|
|
||||||
if (lhs[1] !== param) continue;
|
|
||||||
nextCov.push(m);
|
|
||||||
if (sort < 0 && m & 1) cov.push(m, m ^ 1);
|
|
||||||
} else if (clause instanceof Clause.Compare) {
|
|
||||||
const lhs = clause.lhs.expression();
|
|
||||||
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
|
|
||||||
if (lhs[1] !== param) continue;
|
|
||||||
if (!(m & 1) && sort > 0) continue;
|
|
||||||
nextCov.push(m);
|
|
||||||
|
|
||||||
const negate = (m ^ (m >> 1)) & 1;
|
|
||||||
|
|
||||||
if (sort > 0) {
|
|
||||||
if (
|
|
||||||
(clause.op === "=" && !negate) ||
|
|
||||||
(clause.op === ">" && !negate) ||
|
|
||||||
(clause.op === "<" && negate)
|
|
||||||
) {
|
|
||||||
const c = new Clause.Compare(clause.lhs, ">", clause.rhs);
|
|
||||||
const v = context.getVar(c);
|
|
||||||
cov.push((v << 2) ^ 3);
|
|
||||||
}
|
|
||||||
} else if (sort < 0) {
|
|
||||||
if (
|
|
||||||
(clause.op === "=" && !negate) ||
|
|
||||||
(clause.op === ">" && negate) ||
|
|
||||||
(clause.op === "<" && !negate)
|
|
||||||
) {
|
|
||||||
const c = new Clause.Compare(clause.lhs, "<", clause.rhs);
|
|
||||||
const v = context.getVar(c);
|
|
||||||
cov.push((v << 2) ^ 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = getCover(context, minterm, allSort.slice(1));
|
|
||||||
|
|
||||||
return [cov, ...next.map((n) => [...nextCov, ...n])];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function paginate(
|
|
||||||
fetched: Expression,
|
|
||||||
toFetch: Expression,
|
|
||||||
sort: Record<string, number>,
|
|
||||||
): [Expression, Expression] {
|
|
||||||
fetched = normalize(fetched);
|
|
||||||
if (!fetched) return [false, toFetch];
|
|
||||||
|
|
||||||
toFetch = normalize(toFetch);
|
|
||||||
if (!toFetch) return [false, toFetch];
|
|
||||||
|
|
||||||
const synth1 = Clause.fromExpression(fetched);
|
|
||||||
const synth2 = Clause.fromExpression(toFetch);
|
|
||||||
|
|
||||||
const context = new SynthContext();
|
|
||||||
|
|
||||||
const expr1Minterms = synth1.true(context);
|
|
||||||
const expr2MintermsC = complement(synth2.true(context));
|
|
||||||
|
|
||||||
const gaps = context.sanitizeMinterms(
|
|
||||||
complement([
|
|
||||||
...expr1Minterms,
|
|
||||||
...expr2MintermsC,
|
|
||||||
...context.getDcSet([...expr1Minterms, ...expr2MintermsC]),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const cover = gaps.flatMap((m) => getCover(context, m, Object.entries(sort)));
|
|
||||||
const minterms1 = context.minimize(complement([...cover, ...expr2MintermsC]));
|
|
||||||
const minterms2 = context.minimize(
|
|
||||||
complement([...minterms1, ...expr2MintermsC]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [context.toExpression(minterms1), context.toExpression(minterms2)];
|
|
||||||
}
|
|
||||||
@@ -1,972 +0,0 @@
|
|||||||
import { espresso, complement, tautology } from "espresso-iisojs";
|
|
||||||
import { Expression } from "../../types.ts";
|
|
||||||
import normalize from "./normalize.ts";
|
|
||||||
import { map, parseLikePattern } from "./parser.ts";
|
|
||||||
|
|
||||||
type Minterm = number[];
|
|
||||||
|
|
||||||
export abstract class SynthContextBase<
|
|
||||||
T extends { toString: () => string } = unknown,
|
|
||||||
U = unknown,
|
|
||||||
> {
|
|
||||||
public variables = new Map<string, number>();
|
|
||||||
protected clauses = new Map<number, U>();
|
|
||||||
|
|
||||||
public getVar(c: U): number {
|
|
||||||
const str = c.toString();
|
|
||||||
let idx = this.variables.get(str);
|
|
||||||
if (idx == null) {
|
|
||||||
idx = this.variables.size;
|
|
||||||
this.variables.set(str, idx);
|
|
||||||
this.clauses.set(idx, c);
|
|
||||||
}
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getClause(v: number): U {
|
|
||||||
return this.clauses.get(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract getMinterms(exp: T, res: true | false | null): Minterm[];
|
|
||||||
abstract getDcSet(minterms: Minterm[]): Minterm[];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
canRaise(idx: number, set: Set<number>): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
canLower(idx: number, set: Set<number>): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bias(a: number, b: number): number {
|
|
||||||
// Bias towards 1 then lower index
|
|
||||||
return (b ^ 1) - (a ^ 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
sanitizeMinterms(minterms: Minterm[]): Minterm[] {
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
|
|
||||||
minimize(minterms: Minterm[], dcSet: Minterm[] = []): Minterm[] {
|
|
||||||
minterms = this.sanitizeMinterms(minterms);
|
|
||||||
const canRaise = this.canRaise.bind(this);
|
|
||||||
const canLower = this.canLower.bind(this);
|
|
||||||
const bias = this.bias.bind(this);
|
|
||||||
|
|
||||||
return espresso(
|
|
||||||
minterms,
|
|
||||||
[...this.getDcSet([...minterms, ...dcSet]), ...dcSet],
|
|
||||||
{
|
|
||||||
canRaise,
|
|
||||||
canLower,
|
|
||||||
bias,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function* findIsNullDeps(exp: Expression): IterableIterator<Expression> {
|
|
||||||
if (!Array.isArray(exp)) return;
|
|
||||||
const op = exp[0];
|
|
||||||
if (op === "IS NULL" || op === "IS NOT NULL") return;
|
|
||||||
|
|
||||||
if (op === "FUNC") {
|
|
||||||
if (exp[1] === "NOW") {
|
|
||||||
return;
|
|
||||||
} else if (exp[1] === "LOWER" || exp[1] === "UPPER") {
|
|
||||||
yield* findIsNullDeps(exp[2]);
|
|
||||||
} else if (exp[1] === "ROUND") {
|
|
||||||
for (const e of exp.slice(2, 4)) yield* findIsNullDeps(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (op !== "PARAM") {
|
|
||||||
for (const e of exp.slice(1)) yield* findIsNullDeps(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
yield exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class Clause {
|
|
||||||
private _isNullable: Set<string>;
|
|
||||||
protected _expression: Expression;
|
|
||||||
abstract true(context: SynthContextBase<Clause>): Minterm[];
|
|
||||||
abstract false(context: SynthContextBase<Clause>): Minterm[];
|
|
||||||
abstract null(context: SynthContextBase<Clause>): Minterm[];
|
|
||||||
expression(): Expression {
|
|
||||||
if (this._expression !== undefined) return this._expression;
|
|
||||||
const context = createSynthContext();
|
|
||||||
const minterms = this.true(context);
|
|
||||||
const minimized = context.minimize(minterms);
|
|
||||||
this._expression = context.toExpression(minimized) as Expression;
|
|
||||||
return this._expression;
|
|
||||||
}
|
|
||||||
isBoolean(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
isNullable(c: Clause.IsNull): boolean {
|
|
||||||
if (!this._isNullable) {
|
|
||||||
this._isNullable = new Set(
|
|
||||||
[...this.getNullables()].map((n) => n.toString()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this._isNullable.has(c.toString());
|
|
||||||
}
|
|
||||||
*getNullables(): IterableIterator<Clause.IsNull> {
|
|
||||||
const exp = this.expression();
|
|
||||||
for (const e of findIsNullDeps(exp)) {
|
|
||||||
if (e === exp) yield new Clause.IsNull(this);
|
|
||||||
else yield new Clause.IsNull(new Clause.Exp(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toString(): string {
|
|
||||||
return JSON.stringify(this.expression());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
||||||
export namespace Clause {
|
|
||||||
export class Not extends Clause {
|
|
||||||
constructor(public operand: Clause) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return this.operand.false(context);
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return this.operand.true(context);
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return this.operand.null(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Or extends Clause {
|
|
||||||
constructor(public operands: Clause[]) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
|
||||||
for (const s of this.operands) {
|
|
||||||
const m = s.true(context);
|
|
||||||
if (m.length === 1 && !m[0].length) return [[]];
|
|
||||||
minterms.push(...m);
|
|
||||||
}
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[][] = [];
|
|
||||||
const operands = [...this.operands];
|
|
||||||
for (const e of operands) {
|
|
||||||
if (e instanceof Or) {
|
|
||||||
operands.push(...e.operands);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const m = e.false(context);
|
|
||||||
if (!m.length) return [];
|
|
||||||
minterms.push(m);
|
|
||||||
}
|
|
||||||
if (!minterms.length) return [[]];
|
|
||||||
if (minterms.length === 1) return minterms[0];
|
|
||||||
return complement(minterms.map((m) => complement(m)).flat());
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const trueMinterms: Minterm[] = [];
|
|
||||||
const operands = [...this.operands];
|
|
||||||
for (const s of operands) {
|
|
||||||
if (s instanceof Or) {
|
|
||||||
operands.push(...s.operands);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const t = s.true(context);
|
|
||||||
if (t.length === 1 && !t[0].length) return [];
|
|
||||||
trueMinterms.push(...t);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nullMinterms: Minterm[] = [];
|
|
||||||
for (const s of operands) {
|
|
||||||
const n = s.null(context);
|
|
||||||
if (n.length === 1 && !n[0].length && !trueMinterms.length) return [[]];
|
|
||||||
nullMinterms.push(...n);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!trueMinterms.length) return nullMinterms;
|
|
||||||
return complement([...complement(nullMinterms), ...trueMinterms]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class And extends Clause {
|
|
||||||
constructor(public operands: Clause[]) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[][] = [];
|
|
||||||
const operands = [...this.operands];
|
|
||||||
for (const s of operands) {
|
|
||||||
if (s instanceof And) {
|
|
||||||
operands.push(...s.operands);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const m = s.true(context);
|
|
||||||
if (!m.length) return [];
|
|
||||||
if (m.length === 1 && !m[0].length) continue;
|
|
||||||
minterms.push(m);
|
|
||||||
}
|
|
||||||
if (!minterms.length) return [[]];
|
|
||||||
if (minterms.length === 1) return minterms[0];
|
|
||||||
return complement(minterms.map((m) => complement(m)).flat());
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
|
||||||
for (const s of this.operands) {
|
|
||||||
const m = s.false(context);
|
|
||||||
if (m.length === 1 && !m[0].length) return [[]];
|
|
||||||
minterms.push(...m);
|
|
||||||
}
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const falseMinterms: Minterm[] = [];
|
|
||||||
const operands = [...this.operands];
|
|
||||||
for (const s of operands) {
|
|
||||||
if (s instanceof And) {
|
|
||||||
operands.push(...s.operands);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const f = s.false(context);
|
|
||||||
if (f.length === 1 && !f[0].length) return [];
|
|
||||||
falseMinterms.push(...f);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nullMinterms: Minterm[] = [];
|
|
||||||
for (const s of operands) {
|
|
||||||
const n = s.null(context);
|
|
||||||
if (n.length === 1 && !n[0].length && !falseMinterms.length)
|
|
||||||
return [[]];
|
|
||||||
nullMinterms.push(...n);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!falseMinterms.length) return nullMinterms;
|
|
||||||
return complement([...complement(nullMinterms), ...falseMinterms]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Case extends Clause {
|
|
||||||
constructor(public clauses: Clause[]) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
|
||||||
const cumulative: Minterm[] = [];
|
|
||||||
for (let i = 0; i < this.clauses.length; i += 2) {
|
|
||||||
const w = this.clauses[i].true(context);
|
|
||||||
const t = this.clauses[i + 1].true(context);
|
|
||||||
minterms.push(
|
|
||||||
...complement([...cumulative, ...complement(w), ...complement(t)]),
|
|
||||||
);
|
|
||||||
if (i < this.clauses.length - 2) {
|
|
||||||
cumulative.push(
|
|
||||||
...complement([
|
|
||||||
...this.clauses[i].false(context),
|
|
||||||
...this.clauses[i].null(context),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
|
||||||
const cumulative: Minterm[] = [];
|
|
||||||
for (let i = 0; i < this.clauses.length; i += 2) {
|
|
||||||
const w = this.clauses[i].true(context);
|
|
||||||
const t = this.clauses[i + 1].false(context);
|
|
||||||
minterms.push(
|
|
||||||
...complement([...cumulative, ...complement(w), ...complement(t)]),
|
|
||||||
);
|
|
||||||
if (i < this.clauses.length - 2) {
|
|
||||||
cumulative.push(
|
|
||||||
...complement([
|
|
||||||
...this.clauses[i].false(context),
|
|
||||||
...this.clauses[i].null(context),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
const minterms: Minterm[] = [];
|
|
||||||
const cumulative: Minterm[] = [];
|
|
||||||
for (let i = 0; i < this.clauses.length; i += 2) {
|
|
||||||
const w = this.clauses[i].true(context);
|
|
||||||
const t = this.clauses[i + 1].null(context);
|
|
||||||
minterms.push(
|
|
||||||
...complement([...cumulative, ...complement(w), ...complement(t)]),
|
|
||||||
);
|
|
||||||
cumulative.push(
|
|
||||||
...complement([
|
|
||||||
...this.clauses[i].false(context),
|
|
||||||
...this.clauses[i].null(context),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
minterms.push(...complement([...cumulative]));
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
expression(): Expression {
|
|
||||||
if (this._expression != null) return this._expression;
|
|
||||||
if (this.isBoolean()) {
|
|
||||||
this._expression = new Clause.Not(new Clause.Not(this)).expression();
|
|
||||||
return this._expression;
|
|
||||||
}
|
|
||||||
const context = createSynthContext();
|
|
||||||
const cases: { when: Minterm[]; then: Expression }[] = [];
|
|
||||||
for (let i = 0; i < this.clauses.length; i += 2) {
|
|
||||||
let minterms = this.clauses[i].true(context);
|
|
||||||
const then = this.clauses[i + 1].expression();
|
|
||||||
if (
|
|
||||||
cases.length &&
|
|
||||||
JSON.stringify(then) === JSON.stringify(cases[cases.length - 1].then)
|
|
||||||
)
|
|
||||||
minterms.push(...cases.pop().when);
|
|
||||||
minterms = context.minimize(
|
|
||||||
minterms,
|
|
||||||
cases.flatMap((c) => c.when),
|
|
||||||
);
|
|
||||||
if (!minterms.length) continue;
|
|
||||||
cases.push({ when: minterms, then });
|
|
||||||
if (minterms.length === 1 && !minterms[0].length) break;
|
|
||||||
}
|
|
||||||
while (cases[cases.length - 1].then == null) cases.pop();
|
|
||||||
if (!cases.length) return null;
|
|
||||||
this._expression = [
|
|
||||||
"CASE",
|
|
||||||
...cases.flatMap((c) => [context.toExpression(c.when), c.then]),
|
|
||||||
];
|
|
||||||
return this._expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
isBoolean(): boolean {
|
|
||||||
for (let i = 1; i < this.clauses.length; i += 2)
|
|
||||||
if (!this.clauses[i].isBoolean()) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class IsNull extends Clause {
|
|
||||||
private _boolean: Clause;
|
|
||||||
constructor(public operand: Clause) {
|
|
||||||
super();
|
|
||||||
this._boolean = new Not(new Not(this));
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return this.operand.null(context);
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return complement(this.true(context));
|
|
||||||
}
|
|
||||||
null(): Minterm[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
isBoolean(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
*getNullables(): IterableIterator<IsNull> {
|
|
||||||
// Never returns null
|
|
||||||
}
|
|
||||||
expression(): Expression {
|
|
||||||
const nullables = [...this.operand.getNullables()];
|
|
||||||
if (nullables.length === 1 && nullables[0].operand === this.operand)
|
|
||||||
return ["IS NULL", this.operand.expression()];
|
|
||||||
return super.expression();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Exp extends Clause {
|
|
||||||
constructor(public exp: Expression) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
if (Array.isArray(this.exp)) return context.getMinterms(this, true);
|
|
||||||
if (this.exp) return [[]];
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
if (Array.isArray(this.exp)) return context.getMinterms(this, false);
|
|
||||||
if (!this.exp && this.exp != null) return [[]];
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
if (Array.isArray(this.exp)) return context.getMinterms(this, null);
|
|
||||||
if (this.exp == null) return [[]];
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
expression(): Expression {
|
|
||||||
return this.exp;
|
|
||||||
}
|
|
||||||
isBoolean(): boolean {
|
|
||||||
return this.exp === true || this.exp === false || this.exp == null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Compare extends Clause {
|
|
||||||
constructor(
|
|
||||||
public lhs: Clause,
|
|
||||||
public op: ">" | "<" | "=",
|
|
||||||
public rhs: boolean | number | string,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return context.getMinterms(this, true);
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return context.getMinterms(this, false);
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return this.lhs.null(context);
|
|
||||||
}
|
|
||||||
*getNullables(): IterableIterator<IsNull> {
|
|
||||||
yield* this.lhs.getNullables();
|
|
||||||
}
|
|
||||||
isBoolean(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
expression(): Expression {
|
|
||||||
return [this.op, this.lhs.expression(), this.rhs];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Like extends Clause {
|
|
||||||
public readonly caseSensitive: boolean;
|
|
||||||
public readonly contradiction: boolean;
|
|
||||||
public readonly pattern: string[];
|
|
||||||
public readonly lhs: Clause;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
lhs: Clause,
|
|
||||||
public rhs: string,
|
|
||||||
public esc?: string,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
const exp = lhs.expression();
|
|
||||||
let caseSensitive = true;
|
|
||||||
let contradiction = false;
|
|
||||||
if (Array.isArray(exp)) {
|
|
||||||
if (exp[0] === "FUNC") {
|
|
||||||
const func = exp[1];
|
|
||||||
if (func === "UPPER" || func === "LOWER") {
|
|
||||||
const p = func === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase();
|
|
||||||
if (p === rhs) caseSensitive = false;
|
|
||||||
else contradiction = true;
|
|
||||||
lhs = new Exp(exp[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.lhs = lhs;
|
|
||||||
this.pattern = parseLikePattern(rhs, esc);
|
|
||||||
this.caseSensitive = caseSensitive;
|
|
||||||
this.contradiction = contradiction;
|
|
||||||
}
|
|
||||||
true(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return context.getMinterms(this, true);
|
|
||||||
}
|
|
||||||
false(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return context.getMinterms(this, false);
|
|
||||||
}
|
|
||||||
null(context: SynthContextBase<Clause>): Minterm[] {
|
|
||||||
return this.lhs.null(context);
|
|
||||||
}
|
|
||||||
isBoolean(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
isNullable(c: IsNull): boolean {
|
|
||||||
return this.lhs.isNullable(c);
|
|
||||||
}
|
|
||||||
getNullables(): IterableIterator<IsNull> {
|
|
||||||
return this.lhs.getNullables();
|
|
||||||
}
|
|
||||||
expression(): Expression {
|
|
||||||
let lhs = this.lhs.expression();
|
|
||||||
if (this.contradiction) {
|
|
||||||
if (this.rhs === this.rhs.toLocaleUpperCase())
|
|
||||||
lhs = ["FUNC", "LOWER", lhs];
|
|
||||||
else lhs = ["FUNC", "UPPER", lhs];
|
|
||||||
} else if (!this.caseSensitive) {
|
|
||||||
if (this.rhs === this.rhs.toLocaleUpperCase())
|
|
||||||
lhs = ["FUNC", "UPPER", lhs];
|
|
||||||
else lhs = ["FUNC", "LOWER", lhs];
|
|
||||||
}
|
|
||||||
return ["LIKE", lhs, this.rhs, ...(this.esc ? [this.esc] : [])];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromExpression(exp: Expression): Clause {
|
|
||||||
const res = map(exp, (e) => {
|
|
||||||
if (!Array.isArray(e)) return new Clause.Exp(e);
|
|
||||||
let op = e[0];
|
|
||||||
let negate = true;
|
|
||||||
if (op === "NOT LIKE") op = "LIKE";
|
|
||||||
else if (op === "IS NOT NULL") op = "IS NULL";
|
|
||||||
else if (op === "<>") op = "=";
|
|
||||||
else if (op === ">=") op = "<";
|
|
||||||
else if (op === "<=") op = ">";
|
|
||||||
else negate = false;
|
|
||||||
|
|
||||||
let clause: Clause;
|
|
||||||
if (op === "IS NULL") {
|
|
||||||
clause = new Clause.IsNull(e[1]);
|
|
||||||
} else if (op === "NOT") {
|
|
||||||
clause = new Clause.Not(e[1]);
|
|
||||||
} else if (op === "OR") {
|
|
||||||
clause = new Clause.Or(e.slice(1));
|
|
||||||
} else if (op === "AND") {
|
|
||||||
clause = new Clause.And(e.slice(1));
|
|
||||||
} else if (op === "CASE") {
|
|
||||||
clause = new Clause.Case(e.slice(1));
|
|
||||||
} else if (op === "LIKE") {
|
|
||||||
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
|
|
||||||
const esc = e[3] instanceof Exp ? e[3].expression() : null;
|
|
||||||
if (typeof rhs === "string" && typeof esc === "string")
|
|
||||||
clause = new Clause.Like(e[1], rhs, esc);
|
|
||||||
else if (typeof rhs === "string" && esc == null)
|
|
||||||
clause = new Clause.Like(e[1], rhs);
|
|
||||||
} else if (op === ">" || op === "<" || op === "=") {
|
|
||||||
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
|
|
||||||
if (["boolean", "number", "string"].includes(typeof rhs)) {
|
|
||||||
clause = new Clause.Compare(
|
|
||||||
e[1],
|
|
||||||
op,
|
|
||||||
rhs as boolean | number | string,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!clause) {
|
|
||||||
const args = e.slice(1).map((a) => (a as Clause).expression());
|
|
||||||
clause = new Clause.Exp([op, ...args]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (negate) clause = new Clause.Not(clause);
|
|
||||||
return clause;
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupBy<T, K>(
|
|
||||||
input: T[],
|
|
||||||
callback: (item: T) => K,
|
|
||||||
): Iterable<[K, T[]]> {
|
|
||||||
const groups = new Map<K, T[]>();
|
|
||||||
for (const item of input) {
|
|
||||||
const key = callback(item);
|
|
||||||
let arr = groups.get(key);
|
|
||||||
if (!arr) groups.set(key, (arr = []));
|
|
||||||
arr.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups.entries();
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SynthContext extends SynthContextBase<Clause, Clause> {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMinterms(clause: Clause, res: true | false | null): number[][] {
|
|
||||||
const v = this.getVar(clause);
|
|
||||||
if (res === true) return [[(v << 2) ^ 3]];
|
|
||||||
else if (res === false) return [[(v << 2) ^ 1]];
|
|
||||||
else return [[v << 2, (v << 2) ^ 2]];
|
|
||||||
}
|
|
||||||
|
|
||||||
getDcSet(minterms: Minterm[]): number[][] {
|
|
||||||
const dcSet: number[][] = [];
|
|
||||||
|
|
||||||
const whitelist = new Set([...minterms.flat()].map((v) => v >> 2));
|
|
||||||
|
|
||||||
const allClauses = [...whitelist].map((v) => this.getClause(v));
|
|
||||||
const comparisons = allClauses.filter(
|
|
||||||
(c) => c instanceof Clause.Compare,
|
|
||||||
) as Clause.Compare[];
|
|
||||||
|
|
||||||
// Comparisons
|
|
||||||
for (const [, clauses] of groupBy(comparisons, (c) =>
|
|
||||||
JSON.stringify(c.lhs),
|
|
||||||
)) {
|
|
||||||
const lhs = clauses[0].lhs;
|
|
||||||
const values = new Set(clauses.map((c) => c.rhs));
|
|
||||||
const valuesSorted = [...values].sort((a, b) => {
|
|
||||||
const ta = typeof a;
|
|
||||||
const tb = typeof b;
|
|
||||||
if (ta === tb) return a > b ? 1 : -1;
|
|
||||||
else if (ta === "string") return 1;
|
|
||||||
else if (tb === "string") return -1;
|
|
||||||
return +a - +b;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [i, v] of valuesSorted.entries()) {
|
|
||||||
const eq = this.getVar(new Clause.Compare(lhs, "=", v));
|
|
||||||
const gt = this.getVar(new Clause.Compare(lhs, ">", v));
|
|
||||||
const lt = this.getVar(new Clause.Compare(lhs, "<", v));
|
|
||||||
|
|
||||||
dcSet.push([(eq << 2) ^ 3, (gt << 2) ^ 3]);
|
|
||||||
dcSet.push([(lt << 2) ^ 3, (gt << 2) ^ 3]);
|
|
||||||
dcSet.push([(lt << 2) ^ 3, (eq << 2) ^ 3]);
|
|
||||||
dcSet.push([(lt << 2) ^ 1, (eq << 2) ^ 1, (gt << 2) ^ 1]);
|
|
||||||
|
|
||||||
const negEquivOp = [lt, eq, gt].filter((o) => !whitelist.has(o));
|
|
||||||
if (negEquivOp.length === 1) whitelist.add(negEquivOp[0]);
|
|
||||||
|
|
||||||
for (let j = 0; j < i; j++) {
|
|
||||||
const eq2 = this.getVar(
|
|
||||||
new Clause.Compare(lhs, "=", valuesSorted[j]),
|
|
||||||
);
|
|
||||||
const gt2 = this.getVar(
|
|
||||||
new Clause.Compare(lhs, ">", valuesSorted[j]),
|
|
||||||
);
|
|
||||||
const lt2 = this.getVar(
|
|
||||||
new Clause.Compare(lhs, "<", valuesSorted[j]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// This is the minimum clauses required if all relavent vars
|
|
||||||
// were included in the DC set.
|
|
||||||
// dcSet.push([(eq << 2) ^ 3, (eq2 << 2) ^ 3]);
|
|
||||||
// dcSet.push([(lt << 2) ^ 1, (gt2 << 2) ^ 1]);
|
|
||||||
|
|
||||||
// But we use non-minimal set because intermediate vars
|
|
||||||
// between any two may not be present in the DC set.
|
|
||||||
dcSet.push([(gt2 << 2) ^ 1, (lt << 2) ^ 1]);
|
|
||||||
dcSet.push([(eq2 << 2) ^ 3, (lt << 2) ^ 1]);
|
|
||||||
dcSet.push([(lt2 << 2) ^ 3, (lt << 2) ^ 1]);
|
|
||||||
dcSet.push([(gt2 << 2) ^ 1, (gt << 2) ^ 3]);
|
|
||||||
dcSet.push([(gt2 << 2) ^ 1, (eq << 2) ^ 3]);
|
|
||||||
dcSet.push([(eq2 << 2) ^ 3, (gt << 2) ^ 3]);
|
|
||||||
dcSet.push([(eq2 << 2) ^ 3, (eq << 2) ^ 3]);
|
|
||||||
dcSet.push([(lt2 << 2) ^ 3, (gt << 2) ^ 3]);
|
|
||||||
dcSet.push([(lt2 << 2) ^ 3, (eq << 2) ^ 3]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LIKE
|
|
||||||
const likes = allClauses.filter(
|
|
||||||
(c) => c instanceof Clause.Like,
|
|
||||||
) as Clause.Like[];
|
|
||||||
|
|
||||||
for (const [, clauses] of groupBy(likes, (c) => JSON.stringify(c.lhs))) {
|
|
||||||
for (let i1 = 0; i1 < clauses.length; ++i1) {
|
|
||||||
const l1 = clauses[i1];
|
|
||||||
if (l1.contradiction) {
|
|
||||||
dcSet.push([(this.getVar(l1) << 2) ^ 3]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (let i2 = i1 + 1; i2 < clauses.length; ++i2) {
|
|
||||||
const l2 = clauses[i2];
|
|
||||||
if (l2.contradiction) continue;
|
|
||||||
let p1 = l1.pattern;
|
|
||||||
let p2 = l2.pattern;
|
|
||||||
if (!l1.caseSensitive || !l2.caseSensitive) {
|
|
||||||
p1 = p1.map((c) => c.toLowerCase());
|
|
||||||
p2 = p2.map((c) => c.toLowerCase());
|
|
||||||
}
|
|
||||||
if (likeDisjoint(p1, p2)) {
|
|
||||||
dcSet.push([
|
|
||||||
(this.getVar(l1) << 2) ^ 3,
|
|
||||||
(this.getVar(l2) << 2) ^ 3,
|
|
||||||
]);
|
|
||||||
} else if (
|
|
||||||
(!l1.caseSensitive || l2.caseSensitive) &&
|
|
||||||
likeImplies(p1, p2)
|
|
||||||
) {
|
|
||||||
dcSet.push([
|
|
||||||
(this.getVar(l1) << 2) ^ 2,
|
|
||||||
(this.getVar(l2) << 2) ^ 3,
|
|
||||||
]);
|
|
||||||
dcSet.push([
|
|
||||||
(this.getVar(l1) << 2) ^ 1,
|
|
||||||
(this.getVar(l2) << 2) ^ 0,
|
|
||||||
]);
|
|
||||||
} else if (
|
|
||||||
(!l2.caseSensitive || l1.caseSensitive) &&
|
|
||||||
likeImplies(p2, p1)
|
|
||||||
) {
|
|
||||||
dcSet.push([
|
|
||||||
(this.getVar(l1) << 2) ^ 3,
|
|
||||||
(this.getVar(l2) << 2) ^ 2,
|
|
||||||
]);
|
|
||||||
dcSet.push([
|
|
||||||
(this.getVar(l1) << 2) ^ 0,
|
|
||||||
(this.getVar(l2) << 2) ^ 1,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const v of whitelist) {
|
|
||||||
const clause = this.getClause(v);
|
|
||||||
const nullables = [...clause.getNullables()].map((c) => this.getVar(c));
|
|
||||||
if (nullables.length) {
|
|
||||||
dcSet.push([
|
|
||||||
...nullables.map((n) => (n << 2) ^ 2),
|
|
||||||
(v << 2) ^ 0,
|
|
||||||
(v << 2) ^ 2,
|
|
||||||
]);
|
|
||||||
for (const n of nullables) {
|
|
||||||
dcSet.push([(n << 2) ^ 3, (v << 2) ^ 1]);
|
|
||||||
dcSet.push([(n << 2) ^ 3, (v << 2) ^ 3]);
|
|
||||||
whitelist.add(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clause instanceof Clause.IsNull) {
|
|
||||||
const str = clause.operand.toString();
|
|
||||||
if (str === '["PARAM","DeviceID.ID"]' || str === '["PARAM","_id"]')
|
|
||||||
dcSet.push([(v << 2) ^ 3]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dcSet.filter((m) => m.every((v) => whitelist.has(v >> 2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
canRaise(idx: number, set: Set<number>): boolean {
|
|
||||||
const clause = this.getClause(idx >> 2);
|
|
||||||
if (clause instanceof Clause.IsNull) {
|
|
||||||
for (const i of set) {
|
|
||||||
if (i === idx || i & 1) continue;
|
|
||||||
const c = this.getClause(i >> 2);
|
|
||||||
if (c.isNullable(clause)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !(idx & 1) || !set.has(idx ^ 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
canLower(idx: number, set: Set<number>): boolean {
|
|
||||||
if (idx & 1) return true;
|
|
||||||
const clause = this.getClause(idx >> 2);
|
|
||||||
if (clause instanceof Clause.IsNull) return true;
|
|
||||||
return set.has(idx ^ 3) || set.has(idx ^ 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
bias(a: number, b: number): number {
|
|
||||||
// Bias towards 1 then true
|
|
||||||
return ((b & 3) ^ 3) - ((a & 3) ^ 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
sanitizeMinterms(minterms: Minterm[]): Minterm[] {
|
|
||||||
const res = [] as number[][];
|
|
||||||
|
|
||||||
loop: for (const m of minterms) {
|
|
||||||
const merged: Map<number, number> = new Map();
|
|
||||||
for (const i of m)
|
|
||||||
merged.set(i >> 2, (merged.get(i >> 2) || 0) | (1 << (i & 3)));
|
|
||||||
const minterm: number[] = [];
|
|
||||||
const perms: number[][] = [];
|
|
||||||
for (const [k, v] of merged) {
|
|
||||||
if ((v & 0b0011) === 0b0011) continue loop;
|
|
||||||
if ((v & 0b1100) === 0b1100) continue loop;
|
|
||||||
const clause = this.clauses.get(k);
|
|
||||||
if (!clause) throw new Error("Invalid literal");
|
|
||||||
if (clause instanceof Clause.IsNull) {
|
|
||||||
if (v === 0b0100) minterm.push((k << 2) ^ 2);
|
|
||||||
else if (v === 0b1000) minterm.push((k << 2) ^ 3);
|
|
||||||
else throw new Error("Invalid literal");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ((v & 0b1010) === 0b1010) continue loop;
|
|
||||||
const isNullVars = [...clause.getNullables()].map((c) =>
|
|
||||||
this.getVar(c),
|
|
||||||
);
|
|
||||||
const t = k << 2;
|
|
||||||
if (v === 0b0101) {
|
|
||||||
if (isNullVars.length === 1) minterm.push((isNullVars[0] << 2) ^ 3);
|
|
||||||
else perms.push(isNullVars.map((n) => (n << 2) ^ 3));
|
|
||||||
} else if (v === 0b0001) {
|
|
||||||
perms.push([...isNullVars.map((n) => (n << 2) ^ 3), t ^ 3]);
|
|
||||||
} else if (v === 0b0100) {
|
|
||||||
perms.push([...isNullVars.map((n) => (n << 2) ^ 3), t ^ 1]);
|
|
||||||
} else if (v & 0b1000) {
|
|
||||||
minterm.push(t ^ 3);
|
|
||||||
} else if (v & 0b0010) {
|
|
||||||
minterm.push(t ^ 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let ms = [minterm];
|
|
||||||
while (perms.length) {
|
|
||||||
const newMs: number[][] = [];
|
|
||||||
const perm = perms.pop();
|
|
||||||
for (const p of perm) newMs.push(...ms.map((mm) => [...mm, p]));
|
|
||||||
ms = newMs;
|
|
||||||
}
|
|
||||||
res.push(...ms);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
toExpression(sop: number[][]): Expression {
|
|
||||||
if (!sop.length) return false;
|
|
||||||
const res: Expression[] = [];
|
|
||||||
for (const s of sop) {
|
|
||||||
if (!s.length) return true;
|
|
||||||
const conjs: Expression[] = [];
|
|
||||||
for (const i of s) {
|
|
||||||
const clause = this.getClause(i >>> 2);
|
|
||||||
if (!clause) throw new Error("Invalid literal");
|
|
||||||
if (clause instanceof Clause.IsNull) {
|
|
||||||
if (!(i & 2)) throw new Error("Invalid literal");
|
|
||||||
} else if (!(i & 1)) {
|
|
||||||
// Should never be reached if minimized correctly
|
|
||||||
const isNullVars = [...clause.getNullables()].map((c) =>
|
|
||||||
this.getVar(c),
|
|
||||||
);
|
|
||||||
conjs.push(
|
|
||||||
this.toExpression([
|
|
||||||
[i ^ 3],
|
|
||||||
...isNullVars.map((n) => [(n << 2) ^ 3]),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let expr = clause.expression() as Expression;
|
|
||||||
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
|
|
||||||
if (
|
|
||||||
Array.isArray(expr) &&
|
|
||||||
expr[0] === "NOT" &&
|
|
||||||
Array.isArray(expr[1])
|
|
||||||
) {
|
|
||||||
const e: Expression[] = expr[1];
|
|
||||||
if (e[0] === "IS NULL") expr = ["IS NOT NULL", ...e.slice(1)];
|
|
||||||
else if (e[0] === "LIKE") expr = ["NOT LIKE", ...e.slice(1)];
|
|
||||||
else if (e[0] === "=") expr = ["<>", ...e.slice(1)];
|
|
||||||
else if (e[0] === "<>") expr = ["=", ...e.slice(1)];
|
|
||||||
else if (e[0] === ">") expr = ["<=", ...e.slice(1)];
|
|
||||||
else if (e[0] === ">=") expr = ["<", ...e.slice(1)];
|
|
||||||
else if (e[0] === "<") expr = [">=", ...e.slice(1)];
|
|
||||||
else if (e[0] === "<=") expr = [">", ...e.slice(1)];
|
|
||||||
else if (e[0] === "NOT") expr = e[1];
|
|
||||||
}
|
|
||||||
conjs.push(expr);
|
|
||||||
}
|
|
||||||
if (conjs.length > 1) res.push(["AND", ...conjs]);
|
|
||||||
else res.push(conjs[0]);
|
|
||||||
}
|
|
||||||
if (res.length > 1) return ["OR", ...res];
|
|
||||||
return res[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classes aren't hoisted in JS but functions are. This function is used to
|
|
||||||
// create a new SynthContext instance from inside the Clause class.
|
|
||||||
export function createSynthContext(): SynthContext {
|
|
||||||
return new SynthContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function likeImplies(pat1: string[], pat2: string[]): boolean {
|
|
||||||
let backtrack: [number, number] = null;
|
|
||||||
|
|
||||||
for (let i1 = 0, i2 = 0; ; ++i1, ++i2) {
|
|
||||||
while (i1 < pat1.length && pat1[i1] === "\\%") backtrack = [i1++, i2];
|
|
||||||
|
|
||||||
if (i2 >= pat2.length) return i1 >= pat1.length;
|
|
||||||
|
|
||||||
const c = i1 < pat1.length ? pat1[i1] : null;
|
|
||||||
|
|
||||||
if (c !== pat2[i2] && c !== "\\_") {
|
|
||||||
if (!backtrack) return false;
|
|
||||||
[i1, i2] = backtrack;
|
|
||||||
++backtrack[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function likeDisjoint(pat1: string[], pat2: string[]): boolean {
|
|
||||||
const left1Idx = pat1.indexOf("\\%");
|
|
||||||
const left2Idx = pat2.indexOf("\\%");
|
|
||||||
const right1Idx = pat1.lastIndexOf("\\%");
|
|
||||||
const right2Idx = pat2.lastIndexOf("\\%");
|
|
||||||
|
|
||||||
const left1 = pat1.slice(0, left1Idx !== -1 ? left1Idx : pat1.length);
|
|
||||||
const left2 = pat2.slice(0, left2Idx !== -1 ? left2Idx : pat2.length);
|
|
||||||
const right1 = pat1.slice(right1Idx !== -1 ? right1Idx + 1 : 0).reverse();
|
|
||||||
const right2 = pat2.slice(right2Idx !== -1 ? right2Idx + 1 : 0).reverse();
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(left1.length, left2.length); ++i) {
|
|
||||||
if (left1[i] !== left2[i] && left1[i] !== "\\_" && left2[i] !== "\\_")
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(right1.length, right2.length); ++i) {
|
|
||||||
if (right1[i] !== right2[i] && right1[i] !== "\\_" && right2[i] !== "\\_")
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pat1.length === left1.length)
|
|
||||||
return pat2.filter((c) => c !== "\\%").length > pat1.length;
|
|
||||||
else if (pat2.length === left2.length)
|
|
||||||
return pat1.filter((c) => c !== "\\%").length > pat2.length;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function minimize(expr: Expression, boolean = false): Expression {
|
|
||||||
let synth = Clause.fromExpression(normalize(expr));
|
|
||||||
if (boolean) synth = new Clause.Not(new Clause.Not(synth));
|
|
||||||
return synth.expression();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unionDiff(
|
|
||||||
expr1: Expression,
|
|
||||||
expr2: Expression,
|
|
||||||
): [Expression, Expression] {
|
|
||||||
expr2 = normalize(expr2);
|
|
||||||
|
|
||||||
if (!expr2) return [expr1, false];
|
|
||||||
|
|
||||||
const synth2 = Clause.fromExpression(expr2);
|
|
||||||
|
|
||||||
if (!expr1) {
|
|
||||||
const e = synth2.expression();
|
|
||||||
return [e, e];
|
|
||||||
}
|
|
||||||
|
|
||||||
expr1 = normalize(expr1);
|
|
||||||
const synth1 = Clause.fromExpression(expr1);
|
|
||||||
|
|
||||||
const context = new SynthContext();
|
|
||||||
|
|
||||||
const expr2Minterms = synth2.true(context);
|
|
||||||
const expr1Minterms = synth1.true(context);
|
|
||||||
|
|
||||||
const union = context.minimize([...expr1Minterms, ...expr2Minterms]);
|
|
||||||
|
|
||||||
const diff = context.minimize(
|
|
||||||
complement([...expr1Minterms, ...complement(expr2Minterms)]),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [context.toExpression(union), context.toExpression(diff)];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function covers(expr1: Expression, expr2: Expression): boolean {
|
|
||||||
expr2 = normalize(expr2);
|
|
||||||
if (!expr2) return true;
|
|
||||||
expr1 = normalize(expr1);
|
|
||||||
if (!Array.isArray(expr1)) return !!expr1;
|
|
||||||
|
|
||||||
const synt1 = Clause.fromExpression(expr1);
|
|
||||||
const synt2 = Clause.fromExpression(expr2);
|
|
||||||
|
|
||||||
const context = new SynthContext();
|
|
||||||
const expr1Minterms = synt1.true(context);
|
|
||||||
const expr2Minterms = synt2.true(context);
|
|
||||||
|
|
||||||
return tautology([
|
|
||||||
...context.sanitizeMinterms(complement(expr2Minterms)),
|
|
||||||
...context.getDcSet([...expr2Minterms, ...expr1Minterms]),
|
|
||||||
...context.sanitizeMinterms(expr1Minterms),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
import { map, mapAsync, parseLikePattern } from "./parser.ts";
|
|
||||||
import { Expression } from "../../types.ts";
|
|
||||||
|
|
||||||
const isArray = Array.isArray;
|
|
||||||
|
|
||||||
const regExpCache = new WeakMap<any, RegExp>();
|
|
||||||
|
|
||||||
const REDUCE_SKIP = {};
|
|
||||||
function reduce(exp, callback): Expression {
|
|
||||||
let loop = true;
|
|
||||||
while (loop) {
|
|
||||||
loop = false;
|
|
||||||
for (let i = 2; i < exp.length; ++i) {
|
|
||||||
const res = callback(exp[i - 1], exp[i], i - 2);
|
|
||||||
if (res !== REDUCE_SKIP) {
|
|
||||||
loop = true;
|
|
||||||
exp = exp.slice();
|
|
||||||
exp.splice(i - 1, 2, res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (exp.length === 2) return exp[1];
|
|
||||||
return exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
|
|
||||||
const convChars = {
|
|
||||||
"-": "\\-",
|
|
||||||
"/": "\\/",
|
|
||||||
"\\": "\\/",
|
|
||||||
"^": "\\^",
|
|
||||||
$: "\\$",
|
|
||||||
"*": "\\*",
|
|
||||||
"+": "\\+",
|
|
||||||
"?": "\\?",
|
|
||||||
".": "\\.",
|
|
||||||
"(": "\\(",
|
|
||||||
")": "\\)",
|
|
||||||
"|": "\\|",
|
|
||||||
"[": "\\[",
|
|
||||||
"]": "\\]",
|
|
||||||
"{": "\\{",
|
|
||||||
"}": "\\}",
|
|
||||||
"\\%": ".*",
|
|
||||||
"\\_": ".",
|
|
||||||
};
|
|
||||||
let chars = parseLikePattern(pat, esc);
|
|
||||||
if (!chars.length) return new RegExp("^$", flags);
|
|
||||||
chars = chars.map((c) => convChars[c] || c);
|
|
||||||
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
|
|
||||||
const l = chars.length - 1;
|
|
||||||
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
|
|
||||||
return new RegExp(chars.join(""), flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
function compare(
|
|
||||||
a: boolean | number | string,
|
|
||||||
b: boolean | number | string,
|
|
||||||
): number {
|
|
||||||
if (typeof a === "boolean") a = +a;
|
|
||||||
if (typeof b === "boolean") b = +b;
|
|
||||||
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
|
|
||||||
return a > b ? 1 : a < b ? -1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toNumber(a: boolean | number | string): number {
|
|
||||||
switch (typeof a) {
|
|
||||||
case "number":
|
|
||||||
return a;
|
|
||||||
case "boolean":
|
|
||||||
return +a;
|
|
||||||
case "string":
|
|
||||||
return parseFloat(a) || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toString(a: boolean | number | string): string {
|
|
||||||
switch (typeof a) {
|
|
||||||
case "string":
|
|
||||||
return a;
|
|
||||||
case "number":
|
|
||||||
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 (
|
|
||||||
exp[1] == null ||
|
|
||||||
exp[2] == null ||
|
|
||||||
((exp as any[]).length >= 4 && exp[3] == null)
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
let r = regExpCache.get(exp as any[]);
|
|
||||||
if (!r) {
|
|
||||||
r = likePatternToRegExp(exp[2], exp[3]);
|
|
||||||
regExpCache.set(exp as any[], r);
|
|
||||||
}
|
|
||||||
return r.test(exp[1]);
|
|
||||||
} else if (exp[0] === "NOT LIKE") {
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
|
|
||||||
else if (
|
|
||||||
exp[1] == null ||
|
|
||||||
exp[2] == null ||
|
|
||||||
((exp as any[]).length >= 4 && exp[3] == null)
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
let r = regExpCache.get(exp as any[]);
|
|
||||||
if (!r) {
|
|
||||||
r = likePatternToRegExp(exp[2], exp[3]);
|
|
||||||
regExpCache.set(exp as any[], r);
|
|
||||||
}
|
|
||||||
return !r.test(exp[1]);
|
|
||||||
} else if (exp[0] === "=") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) === 0;
|
|
||||||
} else if (exp[0] === "<>") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) !== 0;
|
|
||||||
} else if (exp[0] === ">") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) > 0;
|
|
||||||
} else if (exp[0] === ">=") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) >= 0;
|
|
||||||
} else if (exp[0] === "<") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) < 0;
|
|
||||||
} else if (exp[0] === "<=") {
|
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
|
||||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
|
||||||
return compare(exp[1], exp[2]) <= 0;
|
|
||||||
} 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;
|
|
||||||
});
|
|
||||||
} else if (exp[0] === "/") {
|
|
||||||
return reduce(exp, (a, b, i) => {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
if (isArray(a) || isArray(b)) return REDUCE_SKIP;
|
|
||||||
const n1 = toNumber(a);
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
} else if (exp[0] === "-") {
|
|
||||||
return reduce(exp, (a, b, i) => {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
if (!isArray(a) && !isArray(b))
|
|
||||||
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
|
|
||||||
return REDUCE_SKIP;
|
|
||||||
});
|
|
||||||
} else if (exp[0] === "%") {
|
|
||||||
return reduce(exp, (a, b, i) => {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
if (isArray(a) || isArray(b) || i !== 0) return REDUCE_SKIP;
|
|
||||||
const n1 = toNumber(a);
|
|
||||||
const n2 = Math.trunc(toNumber(b));
|
|
||||||
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 toString(a) + toString(b);
|
|
||||||
return REDUCE_SKIP;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function evaluate(
|
|
||||||
exp: Expression,
|
|
||||||
obj: Record<string, unknown> | ((e: string) => Expression),
|
|
||||||
now: number,
|
|
||||||
cb?: (e: Expression) => Expression,
|
|
||||||
): string | number | boolean | null;
|
|
||||||
export function evaluate(
|
|
||||||
exp: Expression,
|
|
||||||
obj?: Record<string, unknown> | ((e: string) => Expression),
|
|
||||||
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 (!isArray(e)) return e;
|
|
||||||
|
|
||||||
if (e[0] === "FUNC" && e[1] === "NOW") {
|
|
||||||
if (now) return now;
|
|
||||||
} else if (e[0] === "PARAM") {
|
|
||||||
if (e[1] == null) return null;
|
|
||||||
if (obj && !isArray(e[1])) {
|
|
||||||
let v;
|
|
||||||
if (typeof obj === "function") v = obj(e[1]);
|
|
||||||
else v = obj[e[1]];
|
|
||||||
|
|
||||||
if (v == null) return null;
|
|
||||||
if (typeof v === "object") v = v.value ? v.value[0] : null;
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return evaluateCallback(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function evaluateAsync(
|
|
||||||
exp: Expression,
|
|
||||||
obj: Record<string, unknown>,
|
|
||||||
now: number,
|
|
||||||
cb?: (e: Expression) => Promise<Expression>,
|
|
||||||
): Promise<string | number | boolean | null>;
|
|
||||||
export async function evaluateAsync(
|
|
||||||
exp: Expression,
|
|
||||||
obj?: Record<string, unknown>,
|
|
||||||
now?: number,
|
|
||||||
cb?: (e: Expression) => Promise<Expression>,
|
|
||||||
): Promise<Expression>;
|
|
||||||
export async function evaluateAsync(
|
|
||||||
exp: Expression,
|
|
||||||
obj?: Record<string, unknown>,
|
|
||||||
now?: number,
|
|
||||||
cb?: (e: Expression) => Promise<Expression>,
|
|
||||||
): Promise<Expression> {
|
|
||||||
return mapAsync(exp, async (e) => {
|
|
||||||
if (cb) e = await cb(e);
|
|
||||||
if (!isArray(e)) return e;
|
|
||||||
|
|
||||||
if (e[0] === "FUNC") {
|
|
||||||
if (e[1] === "NOW") if (now) return now;
|
|
||||||
} else if (e[0] === "PARAM") {
|
|
||||||
if (e[1] == null) return null;
|
|
||||||
if (obj && !isArray(e[1])) {
|
|
||||||
let v = obj[e[1]];
|
|
||||||
if (v == null) return null;
|
|
||||||
if (typeof v === "object") v = v["value"] ? v["value"][0] : null;
|
|
||||||
return v as Expression;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return evaluateCallback(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function and(exp1: Expression, exp2: Expression): Expression {
|
|
||||||
if (exp1 != null && !exp1) return false;
|
|
||||||
if (exp2 != null && !exp2) return false;
|
|
||||||
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"];
|
|
||||||
|
|
||||||
if (Array.isArray(exp1) && exp1[0] === "AND") res.push(...exp1.slice(1));
|
|
||||||
else res.push(exp1);
|
|
||||||
|
|
||||||
if (Array.isArray(exp2) && exp2[0] === "AND") res.push(...exp2.slice(1));
|
|
||||||
else res.push(exp2);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function or(exp1: Expression, exp2: Expression): Expression {
|
|
||||||
if (!Array.isArray(exp1) && exp1) return true;
|
|
||||||
if (!Array.isArray(exp2) && exp2) return true;
|
|
||||||
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"];
|
|
||||||
|
|
||||||
if (Array.isArray(exp1) && exp1[0] === "OR") res.push(...exp1.slice(1));
|
|
||||||
else res.push(exp1);
|
|
||||||
|
|
||||||
if (Array.isArray(exp2) && exp2[0] === "OR") res.push(...exp2.slice(1));
|
|
||||||
else res.push(exp2);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractParams(exp: Expression): Expression[] {
|
|
||||||
const params = new Set<Expression>();
|
|
||||||
map(exp, (e) => {
|
|
||||||
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
return Array.from(params);
|
|
||||||
}
|
|
||||||
+23
-13
@@ -1,3 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
let cache1 = new Map();
|
let cache1 = new Map();
|
||||||
let cache2 = new Map();
|
let cache2 = new Map();
|
||||||
const keys = new WeakMap();
|
const keys = new WeakMap();
|
||||||
@@ -20,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;
|
||||||
@@ -28,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;
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-12
@@ -1,9 +1,28 @@
|
|||||||
import Path from "./path.ts";
|
/**
|
||||||
|
* 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 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 = [];
|
||||||
@@ -54,9 +73,9 @@ 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");
|
||||||
|
|
||||||
@@ -87,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;
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-114
@@ -1,105 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
type Segments = (string | Alias)[];
|
type Segments = (string | Alias)[];
|
||||||
type Alias = [Path, string][];
|
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
|
||||||
@@ -117,29 +78,32 @@ export default class Path {
|
|||||||
|
|
||||||
protected static parsePath(
|
protected static parsePath(
|
||||||
pattern: string,
|
pattern: string,
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,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 === "*") {
|
||||||
@@ -178,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);
|
||||||
}
|
}
|
||||||
@@ -196,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);
|
||||||
|
|
||||||
@@ -248,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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
+78
-37
@@ -1,5 +1,24 @@
|
|||||||
import { resolve } from "node:path";
|
/**
|
||||||
import { readFileSync, existsSync } from "node:fs";
|
* 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 { resolve } from "path";
|
||||||
|
import { readFileSync, existsSync } from "fs";
|
||||||
|
|
||||||
// Find project root directory
|
// Find project root directory
|
||||||
export let ROOT_DIR = resolve(__dirname, "..");
|
export let ROOT_DIR = resolve(__dirname, "..");
|
||||||
@@ -13,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: "" },
|
||||||
@@ -32,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: "" },
|
||||||
@@ -40,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: "" },
|
||||||
@@ -57,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 },
|
||||||
@@ -80,11 +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 },
|
||||||
|
|
||||||
// Should probably never be changed
|
// XML configuration
|
||||||
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 },
|
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 },
|
||||||
|
|
||||||
XMPP_JID: { type: "string", default: "" },
|
// Should probably never be changed
|
||||||
XMPP_PASSWORD: { type: "string", default: "" },
|
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }
|
||||||
};
|
};
|
||||||
|
|
||||||
const allConfig: { [name: string]: string | number } = {};
|
const allConfig: { [name: string]: string | number } = {};
|
||||||
@@ -96,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")
|
||||||
@@ -127,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 {
|
||||||
@@ -134,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":
|
||||||
@@ -224,22 +274,13 @@ 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"]);
|
||||||
|
|
||||||
export function get(
|
export function get(
|
||||||
optionName: string,
|
optionName: string,
|
||||||
deviceId?: string,
|
deviceId?: string
|
||||||
): string | number | boolean {
|
): string | number | boolean {
|
||||||
if (!deviceId) return allConfig[optionName];
|
if (!deviceId) return allConfig[optionName];
|
||||||
|
|
||||||
@@ -268,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;
|
||||||
|
|
||||||
|
|||||||
+113
-173
@@ -1,25 +1,43 @@
|
|||||||
import * as crypto from "node:crypto";
|
/**
|
||||||
import * as dgram from "node:dgram";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import * as http from "node:http";
|
*
|
||||||
import { evaluateAsync } from "./common/expression/util.ts";
|
* This file is part of GenieACS.
|
||||||
import { Expression } from "./types.ts";
|
*
|
||||||
import * as auth from "./auth.ts";
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
import * as extensions from "./extensions.ts";
|
* it under the terms of the GNU Affero General Public License as
|
||||||
import * as debug from "./debug.ts";
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
import XmppClient from "./xmpp-client.ts";
|
* License, or (at your option) any later version.
|
||||||
import * as config from "../lib/config.ts";
|
*
|
||||||
import { encodeEntities, parseAttrs, Element } from "./xml-parser.ts";
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
import * as logger from "../lib/logger.ts";
|
* 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 crypto from "crypto";
|
||||||
|
import * as dgram from "dgram";
|
||||||
|
import { parse } from "url";
|
||||||
|
import * as http from "http";
|
||||||
|
import { evaluateAsync } from "./common/expression";
|
||||||
|
import { Expression } from "./types";
|
||||||
|
import * as auth from "./auth";
|
||||||
|
import * as extensions from "./extensions";
|
||||||
|
import * as debug from "./debug";
|
||||||
|
|
||||||
async function extractAuth(
|
async function extractAuth(
|
||||||
exp: Expression,
|
exp: Expression,
|
||||||
dflt: any,
|
context: {},
|
||||||
|
now: number,
|
||||||
|
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") {
|
||||||
@@ -32,42 +50,44 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
return [username, password, _exp];
|
return [username, password, _exp];
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -75,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;
|
||||||
|
|
||||||
@@ -96,15 +119,16 @@ 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
|
||||||
);
|
);
|
||||||
} else if (authHeader["method"] === "Digest") {
|
} else if (authHeader["method"] === "Digest") {
|
||||||
opts = Object.assign(
|
opts = Object.assign(
|
||||||
@@ -113,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 });
|
||||||
@@ -191,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 = "";
|
||||||
@@ -203,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();
|
||||||
@@ -217,88 +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();
|
||||||
}
|
}
|
||||||
|
|
||||||
const XMPP_JID = config.get("XMPP_JID") as string;
|
|
||||||
const XMPP_PASSWORD = config.get("XMPP_PASSWORD") as string;
|
|
||||||
const XMPP_RESOURCE = crypto.randomBytes(8).toString("hex");
|
|
||||||
|
|
||||||
let xmppClient: XmppClient;
|
|
||||||
|
|
||||||
function xmppClientOnError(err: Error): void {
|
|
||||||
xmppClient = null;
|
|
||||||
logger.error({
|
|
||||||
message: "XMPP exception",
|
|
||||||
exception: err,
|
|
||||||
pid: process.pid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function xmppClientOnClose(): void {
|
|
||||||
xmppClient = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function xmppConnectionRequest(
|
|
||||||
jid: string,
|
|
||||||
authExp: Expression,
|
|
||||||
timeout: number,
|
|
||||||
_debug: boolean,
|
|
||||||
deviceId: string,
|
|
||||||
): Promise<string> {
|
|
||||||
if (!xmppClient) {
|
|
||||||
const [host, username] = XMPP_JID.split("@").reverse();
|
|
||||||
xmppClient = await XmppClient.connect({
|
|
||||||
host,
|
|
||||||
username,
|
|
||||||
resource: XMPP_RESOURCE,
|
|
||||||
password: XMPP_PASSWORD,
|
|
||||||
timeout: 120000,
|
|
||||||
});
|
|
||||||
xmppClient.on("error", xmppClientOnError);
|
|
||||||
xmppClient.on("close", xmppClientOnClose);
|
|
||||||
xmppClient.unref();
|
|
||||||
}
|
|
||||||
|
|
||||||
let username: string;
|
|
||||||
let password: string;
|
|
||||||
|
|
||||||
[username, password, authExp] = await extractAuth(authExp, null);
|
|
||||||
while (username != null && password != null) {
|
|
||||||
const msg = `<connectionRequest xmlns="urn:broadband-forum-org:cwmp:xmppConnReq-1-0"><username>${encodeEntities(
|
|
||||||
username,
|
|
||||||
)}</username><password>${encodeEntities(
|
|
||||||
password,
|
|
||||||
)}</password></connectionRequest>`;
|
|
||||||
let res: Element, rawRes: string, rawReq: string;
|
|
||||||
try {
|
|
||||||
({ res, rawRes, rawReq } = await xmppClient.sendIqStanza(
|
|
||||||
XMPP_JID,
|
|
||||||
jid,
|
|
||||||
"get",
|
|
||||||
msg,
|
|
||||||
timeout,
|
|
||||||
));
|
|
||||||
} catch (err) {
|
|
||||||
return err.message;
|
|
||||||
}
|
|
||||||
if (_debug) {
|
|
||||||
debug.outgoingXmppStanza(deviceId, rawReq);
|
|
||||||
debug.incomingXmppStanza(deviceId, rawRes);
|
|
||||||
}
|
|
||||||
const attrs = parseAttrs(res.attrs);
|
|
||||||
const type = attrs.find((a) => a.name === "type");
|
|
||||||
if (type && type.value === "result") return "";
|
|
||||||
const error = res.children.find((c) => c.name === "error");
|
|
||||||
if (!error || !error.children[0])
|
|
||||||
return "Unexpected XMPP connection request response";
|
|
||||||
if (error.children[0].name === "service-unavailable")
|
|
||||||
return "Device is offline";
|
|
||||||
if (error.children[0].name !== "not-authorized")
|
|
||||||
return "Unexpected XMPP connection request response";
|
|
||||||
[username, password, authExp] = await extractAuth(authExp, null);
|
|
||||||
}
|
|
||||||
return "Incorrect connection request credentials";
|
|
||||||
}
|
|
||||||
+578
-716
File diff suppressed because it is too large.
Load diff
@@ -1,357 +0,0 @@
|
|||||||
import * as vm from "node:vm";
|
|
||||||
import * as crypto from "node:crypto";
|
|
||||||
import * as config from "../config.ts";
|
|
||||||
import { collections } from "../db/db.ts";
|
|
||||||
import { convertOldPrecondition } from "../db/util.ts";
|
|
||||||
import * as logger from "../logger.ts";
|
|
||||||
import * as scheduling from "../scheduling.ts";
|
|
||||||
import * as expression from "../common/expression/util.ts";
|
|
||||||
import { parse } from "../common/expression/parser.ts";
|
|
||||||
import {
|
|
||||||
Preset,
|
|
||||||
Expression,
|
|
||||||
Provisions,
|
|
||||||
VirtualParameters,
|
|
||||||
Files,
|
|
||||||
Config,
|
|
||||||
} from "../types.ts";
|
|
||||||
import { LocalCache } from "../local-cache.ts";
|
|
||||||
|
|
||||||
interface Snapshot {
|
|
||||||
presets: Preset[];
|
|
||||||
provisions: Provisions;
|
|
||||||
virtualParameters: VirtualParameters;
|
|
||||||
files: Files;
|
|
||||||
config: Config;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenObject<T extends Record<keyof T, unknown>>(
|
|
||||||
src: T,
|
|
||||||
prefix = "",
|
|
||||||
dst = {} as T,
|
|
||||||
): T {
|
|
||||||
for (const k of Object.keys(src)) {
|
|
||||||
const v = src[k];
|
|
||||||
if (typeof v === "object" && !Array.isArray(v))
|
|
||||||
flattenObject(v as T, `${prefix}${k}.`, dst);
|
|
||||||
else dst[`${prefix}${k}`] = v;
|
|
||||||
}
|
|
||||||
return dst;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchPresets(): Promise<[string, Preset[]]> {
|
|
||||||
const res = await collections.presets.find().toArray();
|
|
||||||
let objects = await collections.objects.find().toArray();
|
|
||||||
res.sort((a, b) => (a._id > b._id ? 1 : -1));
|
|
||||||
objects.sort((a, b) => (a._id > b._id ? 1 : -1));
|
|
||||||
const h = crypto
|
|
||||||
.createHash("md5")
|
|
||||||
.update(JSON.stringify(res))
|
|
||||||
.update(JSON.stringify(objects))
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
objects = objects.map((obj) => {
|
|
||||||
// Flatten object
|
|
||||||
obj = flattenObject(obj);
|
|
||||||
|
|
||||||
// If no keys are defined, consider all parameters as keys to keep the
|
|
||||||
// same behavior from v1.0
|
|
||||||
if (!obj["_keys"]?.length)
|
|
||||||
obj["_keys"] = Object.keys(obj).filter((k) => !k.startsWith("_"));
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.sort((a, b) => {
|
|
||||||
if (a["weight"] === b["weight"])
|
|
||||||
return a["_id"] > b["_id"] ? 1 : a["_id"] < b["_id"] ? -1 : 0;
|
|
||||||
else return a["weight"] - b["weight"];
|
|
||||||
});
|
|
||||||
|
|
||||||
const presets = [] as Preset[];
|
|
||||||
for (const preset of res) {
|
|
||||||
let schedule: { md5: string; duration: number; schedule: any } = null;
|
|
||||||
if (preset["schedule"]) {
|
|
||||||
const parts = preset["schedule"].trim().split(/\s+/);
|
|
||||||
schedule = {
|
|
||||||
md5: crypto.createHash("md5").update(preset["schedule"]).digest("hex"),
|
|
||||||
duration: null,
|
|
||||||
schedule: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
schedule.duration = +parts.shift() * 1000;
|
|
||||||
schedule.schedule = scheduling.parseCron(parts.join(" "));
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn({
|
|
||||||
message: "Invalid preset schedule",
|
|
||||||
preset: preset["_id"],
|
|
||||||
schedule: preset["schedule"],
|
|
||||||
});
|
|
||||||
schedule.schedule = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = preset["events"] || {};
|
|
||||||
let precondition = true as Expression;
|
|
||||||
if (preset["precondition"]) {
|
|
||||||
try {
|
|
||||||
precondition = parse(preset["precondition"]);
|
|
||||||
} catch (error) {
|
|
||||||
precondition = convertOldPrecondition(
|
|
||||||
JSON.parse(preset["precondition"]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simplify expression
|
|
||||||
precondition = expression.evaluate(precondition);
|
|
||||||
}
|
|
||||||
|
|
||||||
const _provisions: Preset["provisions"] = [];
|
|
||||||
|
|
||||||
// Generate provisions from the old configuration format
|
|
||||||
for (const c of preset.configurations) {
|
|
||||||
switch (c.type) {
|
|
||||||
case "age":
|
|
||||||
_provisions.push(["refresh", c.name, +c.age]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "value":
|
|
||||||
_provisions.push(["value", c.name, c.value]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "add_tag":
|
|
||||||
_provisions.push(["tag", c.tag, true]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "delete_tag":
|
|
||||||
_provisions.push(["tag", c.tag, false]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "provision":
|
|
||||||
_provisions.push([c.name, ...(c.args || [])]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "add_object":
|
|
||||||
for (const obj of objects) {
|
|
||||||
if (obj["_id"] === c.object) {
|
|
||||||
const alias = obj["_keys"]
|
|
||||||
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
|
||||||
.join(",");
|
|
||||||
const p = `${c.name}.[${alias}]`;
|
|
||||||
_provisions.push(["instances", p, 1]);
|
|
||||||
|
|
||||||
for (const k in obj) {
|
|
||||||
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
|
|
||||||
_provisions.push(["value", `${p}.${k}`, obj[k]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "delete_object":
|
|
||||||
for (const obj of objects) {
|
|
||||||
if (obj["_id"] === c.object) {
|
|
||||||
const alias = obj["_keys"]
|
|
||||||
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
|
||||||
.join(",");
|
|
||||||
const p = `${c.name}.[${alias}]`;
|
|
||||||
_provisions.push(["instances", p, 0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: {
|
|
||||||
const exhaustiveCheck: never = c;
|
|
||||||
throw new Error(
|
|
||||||
`Unknown configuration type ${exhaustiveCheck["type"]}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
presets.push({
|
|
||||||
name: preset["_id"],
|
|
||||||
channel: (preset["channel"] as string) || "default",
|
|
||||||
schedule: schedule,
|
|
||||||
events: events,
|
|
||||||
precondition: precondition,
|
|
||||||
provisions: _provisions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return [h, presets];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchProvisions(): Promise<[string, Provisions]> {
|
|
||||||
const res = await collections.provisions.find().toArray();
|
|
||||||
res.sort((a, b) => (a._id > b._id ? 1 : -1));
|
|
||||||
const h = crypto.createHash("md5").update(JSON.stringify(res)).digest("hex");
|
|
||||||
|
|
||||||
const provisions = {};
|
|
||||||
for (const r of res) {
|
|
||||||
provisions[r._id] = {};
|
|
||||||
provisions[r._id].md5 = crypto
|
|
||||||
.createHash("md5")
|
|
||||||
.update(r.script)
|
|
||||||
.digest("hex");
|
|
||||||
provisions[r._id].script = new vm.Script(
|
|
||||||
`"use strict";(function(){\n${r.script}\n})();`,
|
|
||||||
{ filename: r._id, lineOffset: -1 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [h, provisions];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchVirtualParameters(): Promise<[string, VirtualParameters]> {
|
|
||||||
const res = await collections.virtualParameters.find().toArray();
|
|
||||||
res.sort((a, b) => (a._id > b._id ? 1 : -1));
|
|
||||||
const h = crypto.createHash("md5").update(JSON.stringify(res)).digest("hex");
|
|
||||||
|
|
||||||
const virtualParameters = {};
|
|
||||||
for (const r of res) {
|
|
||||||
virtualParameters[r._id] = {};
|
|
||||||
virtualParameters[r._id].md5 = crypto
|
|
||||||
.createHash("md5")
|
|
||||||
.update(r.script)
|
|
||||||
.digest("hex");
|
|
||||||
virtualParameters[r._id].script = new vm.Script(
|
|
||||||
`"use strict";(function(){\n${r.script}\n})();`,
|
|
||||||
{ filename: r._id, lineOffset: -1 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [h, virtualParameters];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchFiles(): Promise<[string, Files]> {
|
|
||||||
const res = await collections.files.find().toArray();
|
|
||||||
res.sort((a, b) => (a._id > b._id ? 1 : -1));
|
|
||||||
const h = crypto.createHash("md5").update(JSON.stringify(res)).digest("hex");
|
|
||||||
const files = {};
|
|
||||||
|
|
||||||
for (const r of res) {
|
|
||||||
const id = r.filename || r._id.toString();
|
|
||||||
files[id] = {};
|
|
||||||
files[id].length = r.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [h, files];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchConfig(): Promise<[string, Config]> {
|
|
||||||
const conf = await collections.config.find().toArray();
|
|
||||||
conf.sort((a, b) => (a._id > b._id ? 1 : -1));
|
|
||||||
const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex");
|
|
||||||
|
|
||||||
const _config = {};
|
|
||||||
|
|
||||||
for (const c of conf) {
|
|
||||||
// Evaluate expressions to simplify them
|
|
||||||
_config[c._id] = expression.evaluate(parse(c.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [h, _config];
|
|
||||||
}
|
|
||||||
|
|
||||||
const localCache = new LocalCache<Snapshot>("cwmp-local-cache-hash", refresh);
|
|
||||||
|
|
||||||
async function refresh(): Promise<[string, Snapshot]> {
|
|
||||||
const res = await Promise.all([
|
|
||||||
fetchPresets(),
|
|
||||||
fetchProvisions(),
|
|
||||||
fetchVirtualParameters(),
|
|
||||||
fetchFiles(),
|
|
||||||
fetchConfig(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const h = crypto.createHash("md5");
|
|
||||||
for (const r of res) h.update(r[0]);
|
|
||||||
|
|
||||||
const snapshot = {
|
|
||||||
presets: res[0][1],
|
|
||||||
provisions: res[1][1],
|
|
||||||
virtualParameters: res[2][1],
|
|
||||||
files: res[3][1],
|
|
||||||
config: res[4][1],
|
|
||||||
};
|
|
||||||
|
|
||||||
return [h.digest("hex"), snapshot];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRevision(): Promise<string> {
|
|
||||||
return await localCache.getRevision();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPresets(revision: string): Preset[] {
|
|
||||||
return localCache.get(revision).presets;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getProvisions(revision: string): Provisions {
|
|
||||||
return localCache.get(revision).provisions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getVirtualParameters(revision: string): VirtualParameters {
|
|
||||||
return localCache.get(revision).virtualParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFiles(revision: string): Files {
|
|
||||||
return localCache.get(revision).files;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConfig(
|
|
||||||
snapshotKey: string,
|
|
||||||
key: string,
|
|
||||||
context: Record<string, unknown>,
|
|
||||||
now: number,
|
|
||||||
cb?: (e: Expression) => Expression,
|
|
||||||
): string | number | boolean | null {
|
|
||||||
const snapshot = localCache.get(snapshotKey);
|
|
||||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
|
||||||
|
|
||||||
const oldOpts = {
|
|
||||||
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
|
|
||||||
"cwmp.debug": "DEBUG",
|
|
||||||
"cwmp.retryDelay": "RETRY_DELAY",
|
|
||||||
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
|
|
||||||
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
|
|
||||||
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
|
|
||||||
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
|
|
||||||
"cwmp.cookiesPath": "COOKIES_PATH",
|
|
||||||
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
|
|
||||||
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
|
|
||||||
"cwmp.connectionRequestAllowBasicAuth":
|
|
||||||
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
|
|
||||||
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
|
|
||||||
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
|
|
||||||
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!(key in snapshot.config)) {
|
|
||||||
if (key in oldOpts) {
|
|
||||||
let id;
|
|
||||||
if (context?.["id"]) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = expression.evaluate(snapshot.config[key], context, now, cb);
|
|
||||||
return Array.isArray(v) ? null : v;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConfigExpression(
|
|
||||||
snapshotKey: string,
|
|
||||||
key: string,
|
|
||||||
): Expression {
|
|
||||||
const snapshot = localCache.get(snapshotKey);
|
|
||||||
return snapshot.config[key];
|
|
||||||
}
|
|
||||||
+257
-225
@@ -1,18 +1,51 @@
|
|||||||
import { ObjectId } from "mongodb";
|
/**
|
||||||
import { decodeTag, encodeTag, escapeRegExp } from "../util.ts";
|
* 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 { MongoClient, ObjectID, Collection } from "mongodb";
|
||||||
|
import { get } from "./config";
|
||||||
|
import { escapeRegExp } from "./common";
|
||||||
|
import { parse } from "./common/expression-parser";
|
||||||
import {
|
import {
|
||||||
DeviceData,
|
DeviceData,
|
||||||
Attributes,
|
Attributes,
|
||||||
SessionFault,
|
SessionFault,
|
||||||
Task,
|
Task,
|
||||||
Operation,
|
Operation,
|
||||||
} from "../types.ts";
|
Expression
|
||||||
import Path from "../common/path.ts";
|
} from "./types";
|
||||||
import { collections } from "../db/db.ts";
|
import Path from "./common/path";
|
||||||
import { optimizeProjection } from "../db/util.ts";
|
|
||||||
import * as MongoTypes from "../db/types.ts";
|
|
||||||
|
|
||||||
const INVALID_PATH_SUFFIX = "__invalid";
|
export let tasksCollection: Collection,
|
||||||
|
devicesCollection: Collection,
|
||||||
|
presetsCollection: Collection,
|
||||||
|
objectsCollection: Collection,
|
||||||
|
provisionsCollection: Collection,
|
||||||
|
virtualParametersCollection: Collection,
|
||||||
|
faultsCollection: Collection,
|
||||||
|
filesCollection: Collection,
|
||||||
|
operationsCollection: Collection,
|
||||||
|
permissionsCollection: Collection,
|
||||||
|
usersCollection: Collection,
|
||||||
|
configCollection: Collection;
|
||||||
|
|
||||||
|
let clientPromise: Promise<MongoClient>;
|
||||||
|
export let client: MongoClient;
|
||||||
|
|
||||||
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;
|
||||||
@@ -20,32 +53,76 @@ function compareAccessLists(list1: string[], list2: string[]): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function connect(): Promise<void> {
|
||||||
|
clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), {
|
||||||
|
useNewUrlParser: true
|
||||||
|
});
|
||||||
|
|
||||||
|
client = await clientPromise;
|
||||||
|
const db = client.db();
|
||||||
|
|
||||||
|
tasksCollection = db.collection("tasks");
|
||||||
|
await tasksCollection.createIndex({ device: 1, timestamp: 1 });
|
||||||
|
|
||||||
|
devicesCollection = db.collection("devices");
|
||||||
|
presetsCollection = db.collection("presets");
|
||||||
|
objectsCollection = db.collection("objects");
|
||||||
|
filesCollection = db.collection("fs.files");
|
||||||
|
provisionsCollection = db.collection("provisions");
|
||||||
|
virtualParametersCollection = db.collection("virtualParameters");
|
||||||
|
faultsCollection = db.collection("faults");
|
||||||
|
operationsCollection = db.collection("operations");
|
||||||
|
permissionsCollection = db.collection("permissions");
|
||||||
|
usersCollection = db.collection("users");
|
||||||
|
configCollection = db.collection("config");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disconnect(): Promise<void> {
|
||||||
|
if (clientPromise) await (await clientPromise).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize projection by removing overlaps
|
||||||
|
// This can modify the object
|
||||||
|
function optimizeProjection(obj: {}): {} {
|
||||||
|
if (obj[""]) return { "": obj[""] };
|
||||||
|
|
||||||
|
const keys = Object.keys(obj).sort();
|
||||||
|
if (keys.length <= 1) return obj;
|
||||||
|
|
||||||
|
for (let i = 1; i < keys.length; ++i) {
|
||||||
|
const a = keys[i - 1];
|
||||||
|
const b = keys[i];
|
||||||
|
if (b.startsWith(a)) {
|
||||||
|
if (b.charAt(a.length) === "." || b.charAt(a.length - 1) === ".") {
|
||||||
|
delete obj[b];
|
||||||
|
keys.splice(i--, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchDevice(
|
export async function fetchDevice(
|
||||||
id: string,
|
id: string,
|
||||||
timestamp: number,
|
timestamp: number
|
||||||
): Promise<[Path, number, Attributes?][]> {
|
): Promise<[Path, number, Attributes?][]> {
|
||||||
const res: [Path, number, Attributes?][] = [
|
const res: [Path, number, Attributes?][] = [
|
||||||
[
|
[
|
||||||
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 collections.devices.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"];
|
||||||
@@ -56,7 +133,7 @@ export async function fetchDevice(
|
|||||||
|
|
||||||
if (obj["_value"] != null) {
|
if (obj["_value"] != null) {
|
||||||
attrs.value = [obj["_timestamp"] || 1, [obj["_value"], obj["_type"]]];
|
attrs.value = [obj["_timestamp"] || 1, [obj["_value"], obj["_type"]]];
|
||||||
if (obj["_type"] === "xsd:dateTime" && obj["_value"] instanceof Date)
|
if (obj["_type"] === "xsd:dateTime")
|
||||||
attrs.value[1][0] = +attrs.value[1][0];
|
attrs.value[1][0] = +attrs.value[1][0];
|
||||||
|
|
||||||
obj["_object"] = false;
|
obj["_object"] = false;
|
||||||
@@ -69,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":
|
||||||
@@ -113,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":
|
||||||
@@ -124,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":
|
||||||
@@ -135,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":
|
||||||
@@ -147,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":
|
||||||
@@ -158,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":
|
||||||
@@ -167,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;
|
||||||
@@ -191,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"]]
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,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"]]
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,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"]]
|
||||||
},
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +305,7 @@ export async function saveDevice(
|
|||||||
deviceId: string,
|
deviceId: string,
|
||||||
deviceData: DeviceData,
|
deviceData: DeviceData,
|
||||||
isNew: boolean,
|
isNew: boolean,
|
||||||
sessionTimestamp: number,
|
sessionTimestamp: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const update = { $set: {}, $unset: {}, $addToSet: {}, $pull: {} };
|
const update = { $set: {}, $unset: {}, $addToSet: {}, $pull: {} };
|
||||||
|
|
||||||
@@ -356,37 +418,21 @@ export async function saveDevice(
|
|||||||
if (value2 != null) {
|
if (value2 != null) {
|
||||||
if (!update["$addToSet"]["_tags"])
|
if (!update["$addToSet"]["_tags"])
|
||||||
update["$addToSet"]["_tags"] = { $each: [] };
|
update["$addToSet"]["_tags"] = { $each: [] };
|
||||||
update["$addToSet"]["_tags"]["$each"].push(
|
update["$addToSet"]["_tags"]["$each"].push(path.segments[1]);
|
||||||
decodeTag(path.segments[1] as string),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
if (!update["$pull"]["_tags"]) {
|
if (!update["$pull"]["_tags"]) {
|
||||||
update["$pull"]["_tags"] = {
|
update["$pull"]["_tags"] = {
|
||||||
$in: [],
|
$in: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
update["$pull"]["_tags"]["$in"].push(
|
update["$pull"]["_tags"]["$in"].push(path.segments[1]);
|
||||||
decodeTag(path.segments[1] as string),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +450,7 @@ export async function saveDevice(
|
|||||||
Number.isInteger(value2 as number)
|
Number.isInteger(value2 as number)
|
||||||
) {
|
) {
|
||||||
update["$set"][path.toString() + "._value"] = new Date(
|
update["$set"][path.toString() + "._value"] = new Date(
|
||||||
value2 as number,
|
value2 as number
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
update["$set"][path.toString() + "._value"] = value2;
|
update["$set"][path.toString() + "._value"] = value2;
|
||||||
@@ -416,13 +462,13 @@ export async function saveDevice(
|
|||||||
|
|
||||||
if (valueTimestamp2 !== valueTimestamp1) {
|
if (valueTimestamp2 !== valueTimestamp1) {
|
||||||
update["$set"][path.toString() + "._timestamp"] = new Date(
|
update["$set"][path.toString() + "._timestamp"] = new Date(
|
||||||
valueTimestamp2,
|
valueTimestamp2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -430,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;
|
||||||
@@ -451,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;
|
||||||
@@ -470,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,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;
|
||||||
@@ -531,128 +579,78 @@ export async function saveDevice(
|
|||||||
delete update["$pull"];
|
delete update["$pull"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await collections.devices.updateOne(
|
const result = await devicesCollection.updateOne({ _id: deviceId }, update, {
|
||||||
{ _id: deviceId },
|
upsert: isNew
|
||||||
update,
|
});
|
||||||
{
|
|
||||||
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) {
|
||||||
await collections.devices.updateOne({ _id: deviceId }, update2);
|
await devicesCollection.updateOne({ _id: deviceId }, update2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFaults(
|
export async function getFaults(
|
||||||
deviceId: string,
|
deviceId
|
||||||
): Promise<{ [channel: string]: SessionFault }> {
|
): Promise<{ [channel: string]: SessionFault }> {
|
||||||
const res = await collections.faults
|
const res = await faultsCollection
|
||||||
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
|
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
const faults: { [channel: string]: SessionFault } = {};
|
const faults: { [channel: string]: SessionFault } = {};
|
||||||
for (const r of res) {
|
for (const r of res) {
|
||||||
const channel = r._id.slice(deviceId.length + 1);
|
const channel = r._id.slice(deviceId.length + 1);
|
||||||
const fault: SessionFault = {
|
delete r._id;
|
||||||
code: r.code,
|
delete r.channel;
|
||||||
message: r.message,
|
delete r.device;
|
||||||
...(r.detail && { detail: r.detail }),
|
r.timestamp = +r.timestamp;
|
||||||
timestamp: +r.timestamp,
|
r.provisions = JSON.parse(r.provisions);
|
||||||
provisions: JSON.parse(r.provisions),
|
faults[channel] = r;
|
||||||
retries: r.retries,
|
|
||||||
...(r.expiry && { expiry: +r.expiry }),
|
|
||||||
};
|
|
||||||
faults[channel] = fault;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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: MongoTypes.Fault = {
|
fault = Object.assign({}, fault);
|
||||||
_id: id,
|
fault._id = id;
|
||||||
device: deviceId,
|
fault.device = deviceId;
|
||||||
channel: channel,
|
fault.channel = channel;
|
||||||
timestamp: new Date(fault.timestamp),
|
fault.timestamp = new Date(fault.timestamp);
|
||||||
code: fault.code,
|
fault.provisions = JSON.stringify(fault.provisions);
|
||||||
message: fault.message,
|
await faultsCollection.replaceOne({ _id: id }, fault, { upsert: true });
|
||||||
...(fault.detail && { detail: fault.detail }),
|
|
||||||
retries: fault.retries,
|
|
||||||
...(fault.expiry && { expiry: new Date(fault.expiry) }),
|
|
||||||
provisions: JSON.stringify(fault.provisions),
|
|
||||||
};
|
|
||||||
await collections.faults.replaceOne({ _id: id }, f, { upsert: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteFault(
|
export async function deleteFault(deviceId, channel): Promise<void> {
|
||||||
deviceId: string,
|
await faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` });
|
||||||
channel: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await collections.faults.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 = collections.tasks
|
const cur = tasksCollection.find({ device: deviceId }).sort(["timestamp"]);
|
||||||
.find({ device: deviceId })
|
|
||||||
.sort({ timestamp: 1 });
|
|
||||||
const tasks = [] as Task[];
|
const tasks = [] as Task[];
|
||||||
|
|
||||||
for await (const t of cur) {
|
for await (const task of cur) {
|
||||||
if (+t.timestamp >= timestamp) return [tasks, +t.timestamp];
|
if (task.timestamp) task.timestamp = +task.timestamp;
|
||||||
const task: Task = {
|
if (task.expiry) task.expiry = +task.expiry;
|
||||||
_id: t._id.toString(),
|
if (task.timestamp >= timestamp) return [tasks, +task.timestamp];
|
||||||
name: t.name,
|
task._id = String(task._id);
|
||||||
...(t.timestamp && { timestamp: +t.timestamp }),
|
|
||||||
...(t.expiry && { expiry: +t.expiry }),
|
|
||||||
...(t.name === "getParameterValues" && {
|
|
||||||
parameterNames: t.parameterNames,
|
|
||||||
}),
|
|
||||||
...(t.name === "setParameterValues" && {
|
|
||||||
parameterValues: t.parameterValues,
|
|
||||||
}),
|
|
||||||
...(t.name === "refreshObject" && {
|
|
||||||
objectName: t.objectName,
|
|
||||||
}),
|
|
||||||
...(t.name === "download" && {
|
|
||||||
fileType: t.fileType,
|
|
||||||
fileName: t.fileName,
|
|
||||||
targetFileName: t.targetFileName,
|
|
||||||
}),
|
|
||||||
...(t.name === "addObject" && {
|
|
||||||
objectName: t.objectName,
|
|
||||||
parameterValues: t.parameterValues,
|
|
||||||
}),
|
|
||||||
...(t.name === "deleteObject" && {
|
|
||||||
objectName: t.objectName,
|
|
||||||
}),
|
|
||||||
...(t.name === "provisions" && {
|
|
||||||
provisions: t.provisions,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
tasks.push(task);
|
tasks.push(task);
|
||||||
|
|
||||||
// For API compatibility
|
// For API compatibility
|
||||||
if (task.name === "download" && t["file"]) {
|
if (task.name === "download" && task.file) {
|
||||||
let q;
|
let q;
|
||||||
if (ObjectId.isValid(t["file"]))
|
if (ObjectID.isValid(task.file))
|
||||||
q = { _id: { $in: [t["file"], new ObjectId(t["file"])] } };
|
q = { _id: { $in: [task.file, new ObjectID(task.file)] } };
|
||||||
else q = { _id: t["file"] };
|
else q = { _id: task.file };
|
||||||
|
|
||||||
const res = await collections.files.find(q).toArray();
|
const res = await filesCollection.find(q).toArray();
|
||||||
|
|
||||||
if (res[0]) {
|
if (res[0]) {
|
||||||
if (!task.fileType) task.fileType = res[0].metadata.fileType;
|
if (!task.fileType) task.fileType = res[0].metadata.fileType;
|
||||||
@@ -665,68 +663,102 @@ export async function getDueTasks(
|
|||||||
return [tasks, null];
|
return [tasks, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearTasks(
|
export async function clearTasks(deviceId, taskIds): Promise<void> {
|
||||||
deviceId: string,
|
await tasksCollection.deleteMany({
|
||||||
taskIds: string[],
|
_id: { $in: taskIds.map(id => new ObjectID(id)) }
|
||||||
): Promise<void> {
|
|
||||||
await collections.tasks.deleteMany({
|
|
||||||
_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 collections.operations
|
const res = await operationsCollection
|
||||||
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
|
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
const operations: { [commandKey: string]: Operation } = {};
|
const operations: { [commandKey: string]: Operation } = {};
|
||||||
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);
|
||||||
// Workaround for a bug in v1.2.1 where operation object is saved without deserialization
|
delete r._id;
|
||||||
if (typeof r.provisions !== "string") {
|
r.timestamp = +r.timestamp;
|
||||||
delete r._id;
|
if (r.args) r.args = JSON.parse(r.args);
|
||||||
operations[commandKey] = r as unknown as Operation;
|
r.provisions = JSON.parse(r.provisions);
|
||||||
continue;
|
r.retries = JSON.parse(r.retries);
|
||||||
}
|
operations[commandKey] = r;
|
||||||
const operation: Operation = {
|
|
||||||
name: r.name,
|
|
||||||
timestamp: +r.timestamp,
|
|
||||||
channels:
|
|
||||||
typeof r.channels === "string" ? JSON.parse(r.channels) : r.channels,
|
|
||||||
retries: JSON.parse(r.retries),
|
|
||||||
provisions: JSON.parse(r.provisions),
|
|
||||||
...(r.args && { args: JSON.parse(r.args) }),
|
|
||||||
};
|
|
||||||
operations[commandKey] = operation;
|
|
||||||
}
|
}
|
||||||
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: MongoTypes.Operation = {
|
operation = Object.assign({}, operation);
|
||||||
_id: id,
|
operation._id = id;
|
||||||
name: operation.name,
|
operation.timestamp = new Date(operation.timestamp);
|
||||||
timestamp: new Date(operation.timestamp),
|
operation.provisions = JSON.stringify(operation.provisions);
|
||||||
channels: JSON.stringify(operation.channels),
|
operation.retries = JSON.stringify(operation.retries);
|
||||||
provisions: JSON.stringify(operation.provisions),
|
operation.args = JSON.stringify(operation.args);
|
||||||
retries: JSON.stringify(operation.retries),
|
await operationsCollection.replaceOne({ _id: id }, operation, {
|
||||||
args: JSON.stringify(operation.args),
|
upsert: true
|
||||||
};
|
|
||||||
await collections.operations.replaceOne({ _id: id }, o, {
|
|
||||||
upsert: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteOperation(
|
export async function deleteOperation(deviceId, commandKey): Promise<void> {
|
||||||
deviceId: string,
|
await operationsCollection.deleteOne({ _id: `${deviceId}:${commandKey}` });
|
||||||
commandKey: string,
|
}
|
||||||
): Promise<void> {
|
|
||||||
await collections.operations.deleteOne({ _id: `${deviceId}:${commandKey}` });
|
export async function getPresets(): Promise<{}[]> {
|
||||||
|
return presetsCollection.find().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getObjects(): Promise<{}[]> {
|
||||||
|
return objectsCollection.find().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProvisions(): Promise<{}[]> {
|
||||||
|
return provisionsCollection.find().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getVirtualParameters(): Promise<{}[]> {
|
||||||
|
return virtualParametersCollection.find().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFiles(): Promise<{}[]> {
|
||||||
|
return filesCollection.find().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConfig(): Promise<
|
||||||
|
{ id: string; value: Expression }[]
|
||||||
|
> {
|
||||||
|
const res = await configCollection.find().toArray();
|
||||||
|
return res.map(c => ({
|
||||||
|
id: c["_id"],
|
||||||
|
value: parse(c["value"])
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Permission {
|
||||||
|
role: string;
|
||||||
|
resource: string;
|
||||||
|
access: number;
|
||||||
|
filter: string;
|
||||||
|
validate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPermissions(): Promise<Permission[]> {
|
||||||
|
return permissionsCollection.find().toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
_id: string;
|
||||||
|
password: string;
|
||||||
|
salt: string;
|
||||||
|
roles: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUsers(): Promise<User[]> {
|
||||||
|
return usersCollection.find().toArray();
|
||||||
}
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { MongoClient, Collection, GridFSBucket } from "mongodb";
|
|
||||||
import { get } from "../config.ts";
|
|
||||||
import * as MongoTypes from "./types.ts";
|
|
||||||
|
|
||||||
export let filesBucket: GridFSBucket;
|
|
||||||
|
|
||||||
export const collections = {
|
|
||||||
devices: null as Collection<MongoTypes.Device>,
|
|
||||||
presets: null as Collection<MongoTypes.Preset>,
|
|
||||||
objects: null as Collection<MongoTypes.Object>,
|
|
||||||
provisions: null as Collection<MongoTypes.Provision>,
|
|
||||||
virtualParameters: null as Collection<MongoTypes.VirtualParameter>,
|
|
||||||
faults: null as Collection<MongoTypes.Fault>,
|
|
||||||
tasks: null as Collection<MongoTypes.Task>,
|
|
||||||
files: null as Collection<MongoTypes.File>,
|
|
||||||
operations: null as Collection<MongoTypes.Operation>,
|
|
||||||
permissions: null as Collection<MongoTypes.Permission>,
|
|
||||||
users: null as Collection<MongoTypes.User>,
|
|
||||||
config: null as Collection<MongoTypes.Config>,
|
|
||||||
cache: null as Collection<MongoTypes.Cache>,
|
|
||||||
locks: null as Collection<MongoTypes.Lock>,
|
|
||||||
};
|
|
||||||
|
|
||||||
let clientPromise: Promise<MongoClient>;
|
|
||||||
|
|
||||||
export async function connect(): Promise<void> {
|
|
||||||
clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"));
|
|
||||||
|
|
||||||
const client = await clientPromise;
|
|
||||||
const db = client.db();
|
|
||||||
|
|
||||||
collections.tasks = db.collection("tasks");
|
|
||||||
collections.devices = db.collection("devices");
|
|
||||||
collections.presets = db.collection("presets");
|
|
||||||
collections.objects = db.collection("objects");
|
|
||||||
collections.files = db.collection("fs.files");
|
|
||||||
collections.provisions = db.collection("provisions");
|
|
||||||
collections.virtualParameters = db.collection("virtualParameters");
|
|
||||||
collections.faults = db.collection("faults");
|
|
||||||
collections.operations = db.collection("operations");
|
|
||||||
collections.permissions = db.collection("permissions");
|
|
||||||
collections.users = db.collection("users");
|
|
||||||
collections.config = db.collection("config");
|
|
||||||
collections.cache = db.collection("cache");
|
|
||||||
collections.locks = db.collection("locks");
|
|
||||||
filesBucket = new GridFSBucket(db);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
collections.tasks.createIndex({ device: 1, timestamp: 1 }),
|
|
||||||
collections.cache.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }),
|
|
||||||
collections.locks.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function disconnect(): Promise<void> {
|
|
||||||
if (clientPromise != null) await (await clientPromise).close();
|
|
||||||
}
|
|
||||||
-642
@@ -1,642 +0,0 @@
|
|||||||
import { Filter } from "mongodb";
|
|
||||||
import { EJSON } from "bson";
|
|
||||||
import { parseLikePattern } from "../common/expression/parser.ts";
|
|
||||||
import { Expression } from "../types.ts";
|
|
||||||
import { decodeTag } from "../util.ts";
|
|
||||||
import {
|
|
||||||
SynthContextBase,
|
|
||||||
likeDisjoint,
|
|
||||||
likeImplies,
|
|
||||||
Clause,
|
|
||||||
} from "../common/expression/synth.ts";
|
|
||||||
import normalize from "../common/expression/normalize.ts";
|
|
||||||
import * as BI from "../common/expression/bigint.ts";
|
|
||||||
|
|
||||||
type Minterm = number[];
|
|
||||||
|
|
||||||
function getParam(exp: Expression, collection: string): string {
|
|
||||||
if (!Array.isArray(exp))
|
|
||||||
throw new Error("Left-hand operand must be a parameter");
|
|
||||||
|
|
||||||
if (exp[0] !== "PARAM")
|
|
||||||
throw new Error("Left-hand operand must be a parameter");
|
|
||||||
|
|
||||||
if (typeof exp[1] !== "string")
|
|
||||||
throw new Error("Left-hand operand must be a parameter");
|
|
||||||
|
|
||||||
const p = exp[1];
|
|
||||||
if (collection === "devices") {
|
|
||||||
if (p === "DeviceID.ID") return "_id";
|
|
||||||
else if (p === "DeviceID") return "_deviceId";
|
|
||||||
else if (p.startsWith("DeviceID.")) return "_deviceId._" + p.slice(9);
|
|
||||||
else if (p === "Events.Inform") return "_lastInform";
|
|
||||||
else if (p === "Events.Registered") return "_registered";
|
|
||||||
else if (p === "Events.0_BOOTSTRAP") return "_lastBootstrap";
|
|
||||||
else if (p === "Events.1_BOOT") return "_lastBoot";
|
|
||||||
else if (!p.endsWith("._value") && !p.startsWith("Tags."))
|
|
||||||
return `${p}._value`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypes(parameter: string, collection: string): string[] {
|
|
||||||
if (collection === "devices") {
|
|
||||||
if (parameter === "_id") return ["string"];
|
|
||||||
if (parameter === "_lastInform") return ["date"];
|
|
||||||
if (parameter === "_registered") return ["date"];
|
|
||||||
if (parameter === "_lastBootstrap") return ["date"];
|
|
||||||
if (parameter === "_lastBoot") return ["date"];
|
|
||||||
if (parameter.startsWith("_deviceId.")) return ["string"];
|
|
||||||
if (parameter === "Reboot._value") return ["date"];
|
|
||||||
if (parameter === "FactoryReset._value") return ["date"];
|
|
||||||
if (parameter.endsWith("_timestamp")) return ["date"];
|
|
||||||
if (parameter.startsWith("Downloads.")) {
|
|
||||||
if (parameter.endsWith("Download._value")) return ["date"];
|
|
||||||
if (parameter.endsWith("Time._value")) return ["date"];
|
|
||||||
if (parameter.endsWith("Name._value")) return ["string"];
|
|
||||||
if (parameter.endsWith("Type._value")) return ["string"];
|
|
||||||
}
|
|
||||||
if (parameter.endsWith("_value"))
|
|
||||||
return ["bool", "number", "date", "string"];
|
|
||||||
} else if (collection === "tasks") {
|
|
||||||
if (parameter === "_id") return ["oid"];
|
|
||||||
if (parameter === "timestamp") return ["date"];
|
|
||||||
if (parameter === "expiry") return ["date"];
|
|
||||||
if (parameter === "name") return ["string"];
|
|
||||||
if (parameter === "device") return ["string"];
|
|
||||||
} else if (collection === "faults") {
|
|
||||||
if (parameter === "_id") return ["string"];
|
|
||||||
if (parameter === "timestamp") return ["date"];
|
|
||||||
if (parameter === "expiry") return ["date"];
|
|
||||||
if (parameter === "code") return ["string"];
|
|
||||||
if (parameter === "retries") return ["number"];
|
|
||||||
if (parameter === "channel") return ["string"];
|
|
||||||
if (parameter === "device") return ["string"];
|
|
||||||
if (parameter === "message") return ["string"];
|
|
||||||
} else if (collection === "users") {
|
|
||||||
if (parameter === "_id") return ["string"];
|
|
||||||
if (parameter === "roles") return ["string"];
|
|
||||||
else if (parameter === "password")
|
|
||||||
throw new Error("Cannot query restricted parameters");
|
|
||||||
else if (parameter === "salt")
|
|
||||||
throw new Error("Cannot query restricted parameters");
|
|
||||||
} else if (collection === "config") {
|
|
||||||
if (parameter === "_id") return ["string"];
|
|
||||||
else if (parameter === "value") return ["string"];
|
|
||||||
} else if (collection === "files") {
|
|
||||||
if (parameter === "_id") return ["string"];
|
|
||||||
if (parameter === "metadata.fileType") return ["string"];
|
|
||||||
if (parameter === "metadata.oui") return ["string"];
|
|
||||||
if (parameter === "metadata.productClass") return ["string"];
|
|
||||||
if (parameter === "metadata.version") return ["string"];
|
|
||||||
} else if (collection === "permissions") {
|
|
||||||
if (parameter === "_id") return ["string"];
|
|
||||||
if (parameter === "role") return ["string"];
|
|
||||||
if (parameter === "resource") return ["devices"];
|
|
||||||
if (parameter === "access") return ["number"];
|
|
||||||
if (parameter === "filter") return ["string"];
|
|
||||||
if (parameter === "validate") return ["string"];
|
|
||||||
} else if (collection === "presets") {
|
|
||||||
if (parameter === "_id") return ["string"];
|
|
||||||
if (parameter === "weight") return ["number"];
|
|
||||||
if (parameter === "channel") return ["string"];
|
|
||||||
if (parameter === "precondition") return ["string"];
|
|
||||||
if (parameter.startsWith("events.")) return ["bool"];
|
|
||||||
} else if (collection === "provisions") {
|
|
||||||
if (parameter === "_id") return ["string"];
|
|
||||||
if (parameter === "script") return ["string"];
|
|
||||||
} else if (collection === "virtualParameters") {
|
|
||||||
if (parameter === "_id") return ["string"];
|
|
||||||
if (parameter === "script") return ["string"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameter === "_id") return ["oid", "string"];
|
|
||||||
return ["bool", "number", "date", "string"];
|
|
||||||
}
|
|
||||||
|
|
||||||
function roundOid(oid: string, roundUp: boolean): string {
|
|
||||||
const match = (oid.match(/^[0-9a-f]*/)?.[0] ?? "").slice(0, 24);
|
|
||||||
let num = BI.BigInt("0x0" + match);
|
|
||||||
let lastChar = 0;
|
|
||||||
if (oid.length > match.length) lastChar += oid.charCodeAt(match.length);
|
|
||||||
if (match.length < 24) lastChar -= 48;
|
|
||||||
if (lastChar > 0 && roundUp) num++;
|
|
||||||
num <<= BI.BigInt(4 * (24 - match.length));
|
|
||||||
if (lastChar < 0 && !roundUp) --num;
|
|
||||||
const str = num.toString(16);
|
|
||||||
if (str.length > 24 || str.startsWith("-")) return null;
|
|
||||||
return str.padStart(24, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupBy<T, K>(
|
|
||||||
input: T[],
|
|
||||||
callback: (item: T) => K,
|
|
||||||
): Iterable<[K, T[]]> {
|
|
||||||
const groups = new Map<K, T[]>();
|
|
||||||
for (const item of input) {
|
|
||||||
const key = callback(item);
|
|
||||||
let arr = groups.get(key);
|
|
||||||
if (!arr) groups.set(key, (arr = []));
|
|
||||||
arr.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups.entries();
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class MongoClause {
|
|
||||||
abstract readonly parameter: string;
|
|
||||||
abstract toQuery(truthy: boolean): Filter<unknown>;
|
|
||||||
toString(): string {
|
|
||||||
return JSON.stringify(this.toQuery(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MongoClauseArray extends MongoClause {
|
|
||||||
constructor(
|
|
||||||
public readonly parameter: string,
|
|
||||||
public readonly value: string,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
toQuery(truthy: boolean): Filter<unknown> {
|
|
||||||
if (truthy) return { [this.parameter]: { $eq: this.value } };
|
|
||||||
else return { [this.parameter]: { $ne: this.value } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MongoClauseCompare<T> extends MongoClause {
|
|
||||||
constructor(
|
|
||||||
public readonly parameter: string,
|
|
||||||
public readonly op: "$eq" | "$gt" | "$lt" | "$gte" | "$lte",
|
|
||||||
public readonly value: T,
|
|
||||||
public readonly type: "" | "bool" | "number" | "string" | "date" | "oid",
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
toQuery(truthy: boolean): Filter<unknown> {
|
|
||||||
let v: any = this.value;
|
|
||||||
if (this.type === "date" && typeof v === "number")
|
|
||||||
v = { $date: new Date(v).toISOString() };
|
|
||||||
if (this.type === "oid" && typeof v === "string") v = { $oid: v };
|
|
||||||
|
|
||||||
if (truthy) return { [this.parameter]: { [this.op]: v } };
|
|
||||||
else if (this.op === "$eq") return { [this.parameter]: { $ne: v } };
|
|
||||||
else return { [this.parameter]: { $not: { [this.op]: v } } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MongoClauseType extends MongoClause {
|
|
||||||
constructor(
|
|
||||||
public readonly parameter: string,
|
|
||||||
public readonly type: string,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
toQuery(truthy: boolean): Filter<unknown> {
|
|
||||||
if (truthy) return { [this.parameter]: { $type: this.type } };
|
|
||||||
else return { [this.parameter]: { $not: { $type: this.type } } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MongoClauseLike extends MongoClause {
|
|
||||||
readonly pattern: string[];
|
|
||||||
constructor(
|
|
||||||
public readonly parameter: string,
|
|
||||||
pat: string,
|
|
||||||
esc: string,
|
|
||||||
public readonly caseSensitive: boolean,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.pattern = parseLikePattern(pat, esc);
|
|
||||||
}
|
|
||||||
|
|
||||||
toQuery(truthy: boolean): Filter<unknown> {
|
|
||||||
const convChars = {
|
|
||||||
"-": "\\-",
|
|
||||||
"/": "\\/",
|
|
||||||
"\\": "\\/",
|
|
||||||
"^": "\\^",
|
|
||||||
$: "\\$",
|
|
||||||
"*": "\\*",
|
|
||||||
"+": "\\+",
|
|
||||||
"?": "\\?",
|
|
||||||
".": "\\.",
|
|
||||||
"(": "\\(",
|
|
||||||
")": "\\)",
|
|
||||||
"|": "\\|",
|
|
||||||
"[": "\\[",
|
|
||||||
"]": "\\]",
|
|
||||||
"{": "\\{",
|
|
||||||
"}": "\\}",
|
|
||||||
"\\%": ".*",
|
|
||||||
"\\_": ".",
|
|
||||||
};
|
|
||||||
|
|
||||||
const chars = this.pattern.map((c) => convChars[c] ?? c);
|
|
||||||
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
|
|
||||||
const l = chars.length - 1;
|
|
||||||
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
|
|
||||||
const pattern = chars.join("");
|
|
||||||
const options = this.caseSensitive ? "s" : "is";
|
|
||||||
if (truthy) {
|
|
||||||
return { [this.parameter]: { $regularExpression: { options, pattern } } };
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
[this.parameter]: {
|
|
||||||
$not: { $regularExpression: { options, pattern } },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
|
|
||||||
constructor(private readonly collection: string) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMinterms(clause: Clause, res: true | false | null): number[][] {
|
|
||||||
const exp = clause.expression();
|
|
||||||
if (!Array.isArray(exp)) throw new Error("Invalid query expression");
|
|
||||||
|
|
||||||
if (res === null) {
|
|
||||||
const minterms: number[][] = [];
|
|
||||||
for (const dep of clause.getNullables()) {
|
|
||||||
const e = dep.operand.expression();
|
|
||||||
const param = getParam(e, this.collection);
|
|
||||||
if (param.startsWith("Tags.") && this.collection === "devices") {
|
|
||||||
const t = decodeTag(param.slice(5));
|
|
||||||
const c = new MongoClauseArray("_tags", t);
|
|
||||||
minterms.push([this.getVar(c) << 1]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const c = new MongoClauseCompare(param, "$eq", null, "");
|
|
||||||
minterms.push([(this.getVar(c) << 1) ^ 1]);
|
|
||||||
}
|
|
||||||
return minterms;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([">", "<", "="].includes(exp[0])) {
|
|
||||||
const param = getParam(exp[1], this.collection);
|
|
||||||
|
|
||||||
let rhs = exp[2];
|
|
||||||
if (typeof rhs === "boolean") rhs = +rhs;
|
|
||||||
else if (typeof rhs !== "string" && typeof rhs !== "number")
|
|
||||||
throw new Error(`Right-hand operand must be a literal value`);
|
|
||||||
|
|
||||||
if (
|
|
||||||
param.startsWith("Tags.") &&
|
|
||||||
this.collection === "devices" &&
|
|
||||||
exp[0] === "="
|
|
||||||
) {
|
|
||||||
const t = decodeTag(param.slice(5));
|
|
||||||
const c = new MongoClauseArray("_tags", t);
|
|
||||||
if (typeof rhs === "string") rhs = 2;
|
|
||||||
if (exp[0] === "=") {
|
|
||||||
if ((rhs === 1) !== res) return [];
|
|
||||||
} else if (exp[0] === ">") {
|
|
||||||
if ((res && rhs >= 1) || (!res && rhs < 1)) return [];
|
|
||||||
} else if (exp[0] === "<") {
|
|
||||||
if ((res && rhs <= 1) || (!res && rhs > 1)) return [];
|
|
||||||
}
|
|
||||||
return [[(this.getVar(c) << 1) ^ 1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
let op: "$gt" | "$lt" | "$eq" | "$gte" | "$lte";
|
|
||||||
if (exp[0] === "=") op = "$eq";
|
|
||||||
else if (exp[0] === ">") op = res ? "$gt" : "$lte";
|
|
||||||
else if (exp[0] === "<") op = res ? "$lt" : "$gte";
|
|
||||||
|
|
||||||
const possibleTypes = new Set(getTypes(param, this.collection));
|
|
||||||
const clauses: MongoClause[] = [];
|
|
||||||
|
|
||||||
if (typeof rhs === "number") {
|
|
||||||
if (possibleTypes.has("number"))
|
|
||||||
clauses.push(new MongoClauseCompare(param, op, rhs, "number"));
|
|
||||||
|
|
||||||
if (possibleTypes.has("date"))
|
|
||||||
clauses.push(new MongoClauseCompare(param, op, rhs, "date"));
|
|
||||||
if (possibleTypes.has("bool") && (rhs === 0 || rhs === 1))
|
|
||||||
clauses.push(new MongoClauseCompare(param, op, !!rhs, "bool"));
|
|
||||||
} else if (typeof rhs === "string") {
|
|
||||||
if (possibleTypes.has("string"))
|
|
||||||
clauses.push(new MongoClauseCompare(param, op, rhs, "string"));
|
|
||||||
|
|
||||||
if (possibleTypes.has("oid")) {
|
|
||||||
const oid = roundOid(rhs, op === "$lt" || op === "$lte");
|
|
||||||
if (oid && (op !== "$eq" || oid === rhs))
|
|
||||||
clauses.push(new MongoClauseCompare(param, op, oid, "oid"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op === "$eq" && !res) {
|
|
||||||
clauses.push(new MongoClauseCompare(param, "$eq", null, ""));
|
|
||||||
return [clauses.map((c) => this.getVar(c) << 1)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the following clauses we could use $type operator, but we want the
|
|
||||||
// final query to use comparison operators to check for type because the
|
|
||||||
// $type operator in MongoDB doesn't use indexes
|
|
||||||
if (typeof rhs === "number") {
|
|
||||||
if (possibleTypes.has("bool")) {
|
|
||||||
if (
|
|
||||||
(rhs > 1 && (op === "$lt" || op === "$lte")) ||
|
|
||||||
(rhs < 0 && (op === "$gt" || op === "$gte"))
|
|
||||||
)
|
|
||||||
clauses.push(new MongoClauseCompare(param, "$gte", false, "bool"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op === "$gt" || op === "$gte") {
|
|
||||||
if (possibleTypes.has("string"))
|
|
||||||
clauses.push(new MongoClauseCompare(param, "$gte", "", "string"));
|
|
||||||
|
|
||||||
if (possibleTypes.has("oid")) {
|
|
||||||
clauses.push(
|
|
||||||
new MongoClauseCompare(
|
|
||||||
param,
|
|
||||||
"$gte",
|
|
||||||
"000000000000000000000000",
|
|
||||||
"oid",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (typeof rhs === "string") {
|
|
||||||
if (op === "$lt" || op === "$lte") {
|
|
||||||
if (possibleTypes.has("bool"))
|
|
||||||
clauses.push(new MongoClauseCompare(param, "$gte", false, "bool"));
|
|
||||||
|
|
||||||
if (possibleTypes.has("number")) {
|
|
||||||
clauses.push(new MongoClauseCompare(param, "$gte", 0, "number"));
|
|
||||||
clauses.push(new MongoClauseCompare(param, "$lt", 0, "number"));
|
|
||||||
}
|
|
||||||
if (possibleTypes.has("date")) {
|
|
||||||
clauses.push(new MongoClauseCompare(param, "$gte", 0, "date"));
|
|
||||||
clauses.push(new MongoClauseCompare(param, "$lt", 0, "date"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clauses.map((c) => [(this.getVar(c) << 1) ^ 1]);
|
|
||||||
} else if (exp[0] === "LIKE") {
|
|
||||||
const pat = exp[2];
|
|
||||||
if (typeof pat !== "string")
|
|
||||||
throw new Error("Right-hand operand of 'LIKE' must be a string");
|
|
||||||
let p = exp[1];
|
|
||||||
let caseSensitive = true;
|
|
||||||
if (
|
|
||||||
Array.isArray(p) &&
|
|
||||||
p[0] === "FUNC" &&
|
|
||||||
["UPPER", "LOWER"].includes(p[1])
|
|
||||||
) {
|
|
||||||
if (p[1] === "UPPER" && pat !== pat.toUpperCase())
|
|
||||||
return res ? [] : [[]];
|
|
||||||
if (p[1] === "LOWER" && pat !== pat.toLowerCase())
|
|
||||||
return res ? [] : [[]];
|
|
||||||
caseSensitive = false;
|
|
||||||
p = p[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
const param = getParam(p, this.collection);
|
|
||||||
const c = new MongoClauseLike(param, pat, exp[3], caseSensitive);
|
|
||||||
if (res) return [[(this.getVar(c) << 1) ^ 1]];
|
|
||||||
const typeClase = new MongoClauseType(param, "string");
|
|
||||||
const r = [[this.getVar(c) << 1, (this.getVar(typeClase) << 1) ^ 1]];
|
|
||||||
return r;
|
|
||||||
} else {
|
|
||||||
throw new Error("Invalid query expression");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDcSet(minterms: Minterm[]): number[][] {
|
|
||||||
const dcSet: number[][] = [];
|
|
||||||
|
|
||||||
const vars = new Set(minterms.flat().map((l) => l >> 1));
|
|
||||||
const clauses = Array.from(vars).map((v) => this.getClause(v));
|
|
||||||
|
|
||||||
for (const [parameter, clauses2] of groupBy(clauses, (c) => c.parameter)) {
|
|
||||||
const comparisons = clauses2.filter(
|
|
||||||
(c) => c instanceof MongoClauseCompare && c.value !== null,
|
|
||||||
) as MongoClauseCompare<unknown>[];
|
|
||||||
|
|
||||||
for (const [type, clauses3] of groupBy(comparisons, (c) => c.type)) {
|
|
||||||
const isType = this.getVar(new MongoClauseType(parameter, type));
|
|
||||||
const values = new Set(clauses3.map((c) => c.value));
|
|
||||||
const valuesSorted = Array.from(values).sort((a, b) =>
|
|
||||||
a > b ? 1 : -1,
|
|
||||||
);
|
|
||||||
for (const [i, v] of valuesSorted.entries()) {
|
|
||||||
const eq = this.getVar(
|
|
||||||
new MongoClauseCompare(parameter, "$eq", v, type),
|
|
||||||
);
|
|
||||||
const gt = this.getVar(
|
|
||||||
new MongoClauseCompare(parameter, "$gt", v, type),
|
|
||||||
);
|
|
||||||
const lt = this.getVar(
|
|
||||||
new MongoClauseCompare(parameter, "$lt", v, type),
|
|
||||||
);
|
|
||||||
const gte = this.getVar(
|
|
||||||
new MongoClauseCompare(parameter, "$gte", v, type),
|
|
||||||
);
|
|
||||||
const lte = this.getVar(
|
|
||||||
new MongoClauseCompare(parameter, "$lte", v, type),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (type === "bool") {
|
|
||||||
if (v === false) dcSet.push([(lt << 1) ^ 1]);
|
|
||||||
else if (v === true) dcSet.push([(gt << 1) ^ 1]);
|
|
||||||
} else if (type === "string") {
|
|
||||||
if (v === "") dcSet.push([(lt << 1) ^ 1]);
|
|
||||||
} else if (type === "oid") {
|
|
||||||
if (v < "000000000000000000000000") dcSet.push([(lte << 1) ^ 1]);
|
|
||||||
else if (v === "000000000000000000000000")
|
|
||||||
dcSet.push([(lt << 1) ^ 1]);
|
|
||||||
else if (v > "ffffffffffffffffffffffff")
|
|
||||||
dcSet.push([(gte << 1) ^ 1]);
|
|
||||||
else if (v === "ffffffffffffffffffffffff")
|
|
||||||
dcSet.push([(gt << 1) ^ 1]);
|
|
||||||
}
|
|
||||||
dcSet.push([(lt << 1) ^ 1, (gte << 1) ^ 1]);
|
|
||||||
dcSet.push([(gt << 1) ^ 1, (gte << 1) ^ 0]);
|
|
||||||
dcSet.push([(eq << 1) ^ 0, (lt << 1) ^ 0, (lte << 1) ^ 1]);
|
|
||||||
dcSet.push([(eq << 1) ^ 1, (gte << 1) ^ 0]);
|
|
||||||
dcSet.push([(eq << 1) ^ 1, (gt << 1) ^ 1]);
|
|
||||||
|
|
||||||
if (i === 0) {
|
|
||||||
dcSet.push([(isType << 1) ^ 0, (gte << 1) ^ 1]);
|
|
||||||
dcSet.push([(isType << 1) ^ 0, (lt << 1) ^ 1]);
|
|
||||||
} else {
|
|
||||||
const gt2 = this.getVar(
|
|
||||||
new MongoClauseCompare(
|
|
||||||
parameter,
|
|
||||||
"$gt",
|
|
||||||
valuesSorted[i - 1],
|
|
||||||
type,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const lte2 = this.getVar(
|
|
||||||
new MongoClauseCompare(
|
|
||||||
parameter,
|
|
||||||
"$lte",
|
|
||||||
valuesSorted[i - 1],
|
|
||||||
type,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
dcSet.push([(gt2 << 1) ^ 0, (gte << 1) ^ 1]);
|
|
||||||
dcSet.push([(gt2 << 1) ^ 0, (lte2 << 1) ^ 0, (lt << 1) ^ 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === valuesSorted.length - 1)
|
|
||||||
dcSet.push([(isType << 1) ^ 1, (gt << 1) ^ 0, (lte << 1) ^ 0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const likes = clauses2.filter(
|
|
||||||
(c) => c instanceof MongoClauseLike,
|
|
||||||
) as MongoClauseLike[];
|
|
||||||
if (likes.length) {
|
|
||||||
const isType = this.getVar(new MongoClauseType(parameter, "string"));
|
|
||||||
for (let i1 = 0; i1 < likes.length; ++i1) {
|
|
||||||
const l1 = likes[i1];
|
|
||||||
let p1 = l1.pattern;
|
|
||||||
dcSet.push([(this.getVar(l1) << 1) ^ 1, (isType << 1) ^ 0]);
|
|
||||||
for (let i2 = i1 + 1; i2 < likes.length; ++i2) {
|
|
||||||
const l2 = likes[i2];
|
|
||||||
let p2 = l2.pattern;
|
|
||||||
if (!l1.caseSensitive || !l2.caseSensitive) {
|
|
||||||
p1 = p1.map((c) => c.toLowerCase());
|
|
||||||
p2 = p2.map((c) => c.toLowerCase());
|
|
||||||
}
|
|
||||||
if (likeDisjoint(p1, p2)) {
|
|
||||||
dcSet.push([
|
|
||||||
(this.getVar(l1) << 1) ^ 1,
|
|
||||||
(this.getVar(l2) << 1) ^ 1,
|
|
||||||
]);
|
|
||||||
} else if (
|
|
||||||
(!l1.caseSensitive || l2.caseSensitive) &&
|
|
||||||
likeImplies(p1, p2)
|
|
||||||
) {
|
|
||||||
dcSet.push([
|
|
||||||
(this.getVar(l1) << 1) ^ 0,
|
|
||||||
(this.getVar(l2) << 1) ^ 1,
|
|
||||||
]);
|
|
||||||
} else if (
|
|
||||||
(!l2.caseSensitive || l1.caseSensitive) &&
|
|
||||||
likeImplies(p2, p1)
|
|
||||||
) {
|
|
||||||
dcSet.push([
|
|
||||||
(this.getVar(l1) << 1) ^ 1,
|
|
||||||
(this.getVar(l2) << 1) ^ 0,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNull = this.getVar(
|
|
||||||
new MongoClauseCompare(parameter, "$eq", null, ""),
|
|
||||||
);
|
|
||||||
|
|
||||||
const types = getTypes(parameter, this.collection).map((t) =>
|
|
||||||
this.getVar(new MongoClauseType(parameter, t)),
|
|
||||||
);
|
|
||||||
|
|
||||||
dcSet.push([(isNull << 1) ^ 0, ...types.map((t) => (t << 1) ^ 0)]);
|
|
||||||
for (let i = 0; i < types.length; ++i) {
|
|
||||||
const t1 = types[i];
|
|
||||||
dcSet.push([(t1 << 1) ^ 1, (isNull << 1) ^ 1]);
|
|
||||||
for (let j = i + 1; j < types.length; ++j) {
|
|
||||||
const t2 = types[j];
|
|
||||||
dcSet.push([(t1 << 1) ^ 1, (t2 << 1) ^ 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameter === "_id") dcSet.push([(isNull << 1) ^ 1]);
|
|
||||||
}
|
|
||||||
return dcSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
canRaise(i: number, s: Set<number>): boolean {
|
|
||||||
if (!(i & 1)) return true;
|
|
||||||
const c = this.getClause(i >> 1);
|
|
||||||
if (c instanceof MongoClauseCompare) {
|
|
||||||
for (const j of s) {
|
|
||||||
if (j === i) continue;
|
|
||||||
const c2 = this.getClause(j >> 1);
|
|
||||||
if (c2.parameter !== c.parameter) continue;
|
|
||||||
if (!(j & 1)) return false;
|
|
||||||
if (c2 instanceof MongoClauseType) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
toQuery(minterms: Minterm[]): Filter<unknown> {
|
|
||||||
const or = [];
|
|
||||||
|
|
||||||
for (const minterm of minterms) {
|
|
||||||
const query = {};
|
|
||||||
loop: for (const clause of minterm) {
|
|
||||||
if (!minterm.length) return {};
|
|
||||||
const negate = !!(clause & 1);
|
|
||||||
const c = this.getClause(clause >> 1);
|
|
||||||
const q = c.toQuery(negate);
|
|
||||||
if (Object.keys(q).length !== 1)
|
|
||||||
throw new Error("Invalid query expression");
|
|
||||||
const [param, value] = Object.entries(q)[0];
|
|
||||||
if (Object.getPrototypeOf(value).constructor !== Object)
|
|
||||||
throw new Error("Invalid query expression");
|
|
||||||
const dests = [query, ...(query["$and"] ?? [])];
|
|
||||||
for (const dest of dests) {
|
|
||||||
if (!(param in dest)) {
|
|
||||||
dest[param] = value;
|
|
||||||
continue loop;
|
|
||||||
}
|
|
||||||
|
|
||||||
let src = value;
|
|
||||||
let dst = dest[param];
|
|
||||||
|
|
||||||
if (src["$not"] && dst["$not"]) {
|
|
||||||
src = src["$not"];
|
|
||||||
dst = dst["$not"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Object.keys(src).some((k) => k in dst)) {
|
|
||||||
Object.assign(dst, src);
|
|
||||||
continue loop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query["$and"] ??= [];
|
|
||||||
query["$and"].push({ [param]: value });
|
|
||||||
}
|
|
||||||
or.push(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (or.length === 1) return or[0];
|
|
||||||
return { $or: or };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toMongoQuery(
|
|
||||||
exp: Expression,
|
|
||||||
resource: string,
|
|
||||||
): Filter<unknown> | false {
|
|
||||||
exp = normalize(exp);
|
|
||||||
const clause = Clause.fromExpression(normalize(exp));
|
|
||||||
const context = new MongoSynthContext(resource);
|
|
||||||
const minterms = clause.true(context);
|
|
||||||
const minimized = context.minimize(minterms);
|
|
||||||
if (!minimized.length) return false;
|
|
||||||
return EJSON.deserialize(context.toQuery(minimized));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validQuery(exp: Expression, resource: string): void {
|
|
||||||
const clause = Clause.fromExpression(normalize(exp));
|
|
||||||
const context = new MongoSynthContext(resource);
|
|
||||||
clause.true(context);
|
|
||||||
}
|
|
||||||
-183
@@ -1,183 +0,0 @@
|
|||||||
import { ObjectId } from "mongodb";
|
|
||||||
import { Expression, FaultStruct } from "../types.ts";
|
|
||||||
|
|
||||||
export interface Fault {
|
|
||||||
_id: string;
|
|
||||||
device: string;
|
|
||||||
channel: string;
|
|
||||||
timestamp: Date;
|
|
||||||
provisions: string;
|
|
||||||
retries: number;
|
|
||||||
code: string;
|
|
||||||
message: string;
|
|
||||||
detail?:
|
|
||||||
| FaultStruct
|
|
||||||
| {
|
|
||||||
name: string;
|
|
||||||
message: string;
|
|
||||||
stack?: string;
|
|
||||||
};
|
|
||||||
expiry?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskBase {
|
|
||||||
_id: ObjectId;
|
|
||||||
timestamp?: Date;
|
|
||||||
expiry?: Date;
|
|
||||||
name: string;
|
|
||||||
device: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskGetParameterValues extends TaskBase {
|
|
||||||
name: "getParameterValues";
|
|
||||||
parameterNames: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskSetParameterValues extends TaskBase {
|
|
||||||
name: "setParameterValues";
|
|
||||||
parameterValues: [string, string | number | boolean, string?][];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskRefreshObject extends TaskBase {
|
|
||||||
name: "refreshObject";
|
|
||||||
objectName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskReboot extends TaskBase {
|
|
||||||
name: "reboot";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskFactoryReset extends TaskBase {
|
|
||||||
name: "factoryReset";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskDownload extends TaskBase {
|
|
||||||
name: "download";
|
|
||||||
fileType: string;
|
|
||||||
fileName: string;
|
|
||||||
targetFileName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskAddObject extends TaskBase {
|
|
||||||
name: "addObject";
|
|
||||||
objectName: string;
|
|
||||||
parameterValues: [string, string | number | boolean, string?][];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskDeleteObject extends TaskBase {
|
|
||||||
name: "deleteObject";
|
|
||||||
objectName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskProvisions extends TaskBase {
|
|
||||||
name: "provisions";
|
|
||||||
provisions?: [string, ...Expression[]][];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Task =
|
|
||||||
| TaskGetParameterValues
|
|
||||||
| TaskSetParameterValues
|
|
||||||
| TaskRefreshObject
|
|
||||||
| TaskReboot
|
|
||||||
| TaskFactoryReset
|
|
||||||
| TaskDownload
|
|
||||||
| TaskAddObject
|
|
||||||
| TaskDeleteObject
|
|
||||||
| TaskProvisions;
|
|
||||||
|
|
||||||
export interface Operation {
|
|
||||||
_id: string;
|
|
||||||
name: string;
|
|
||||||
timestamp: Date;
|
|
||||||
channels: string;
|
|
||||||
retries: string;
|
|
||||||
provisions: string;
|
|
||||||
args: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
_id: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Cache {
|
|
||||||
_id: string;
|
|
||||||
value: string;
|
|
||||||
timestamp: Date;
|
|
||||||
expire: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Device {
|
|
||||||
_id: string;
|
|
||||||
_lastInform: Date;
|
|
||||||
_registered: Date;
|
|
||||||
_tags?: string[];
|
|
||||||
_timestamp?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Configuration =
|
|
||||||
| { type: "age"; name: string; age: number }
|
|
||||||
| { type: "value"; name: string; value: boolean | number | string }
|
|
||||||
| { type: "add_tag"; tag: string }
|
|
||||||
| { type: "delete_tag"; tag: string }
|
|
||||||
| { type: "add_object"; name: string; object: string }
|
|
||||||
| { type: "delete_object"; name: string; object: string }
|
|
||||||
| { type: "provision"; name: string; args?: Expression[] };
|
|
||||||
|
|
||||||
export interface Preset {
|
|
||||||
_id: string;
|
|
||||||
weight: number;
|
|
||||||
channel: string;
|
|
||||||
events: Record<string, boolean>;
|
|
||||||
configurations: Configuration[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Object {
|
|
||||||
_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Provision {
|
|
||||||
_id: string;
|
|
||||||
script: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VirtualParameter {
|
|
||||||
_id: string;
|
|
||||||
script: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface File {
|
|
||||||
_id: string;
|
|
||||||
length: number;
|
|
||||||
filename: string;
|
|
||||||
uploadDate: Date;
|
|
||||||
metadata?: {
|
|
||||||
fileType?: string;
|
|
||||||
oui?: string;
|
|
||||||
productClass?: string;
|
|
||||||
version?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Permission {
|
|
||||||
_id: string;
|
|
||||||
role: string;
|
|
||||||
resource: string;
|
|
||||||
access: 1 | 2 | 3;
|
|
||||||
filter?: string;
|
|
||||||
validate?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
_id: string;
|
|
||||||
password: string;
|
|
||||||
roles: string;
|
|
||||||
salt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Lock {
|
|
||||||
_id: string;
|
|
||||||
value: string;
|
|
||||||
timestamp: Date;
|
|
||||||
expire: Date;
|
|
||||||
}
|
|
||||||
-123
@@ -1,123 +0,0 @@
|
|||||||
import { Expression } from "../types.ts";
|
|
||||||
import { encodeTag } from "../util.ts";
|
|
||||||
|
|
||||||
// Optimize projection by removing overlaps
|
|
||||||
// This can modify the object
|
|
||||||
export function optimizeProjection(obj: { [path: string]: 1 }): {
|
|
||||||
[path: string]: 1;
|
|
||||||
} {
|
|
||||||
if (obj[""]) return { "": obj[""] };
|
|
||||||
|
|
||||||
const keys = Object.keys(obj).sort();
|
|
||||||
if (keys.length <= 1) return obj;
|
|
||||||
|
|
||||||
for (let i = 1; i < keys.length; ++i) {
|
|
||||||
const a = keys[i - 1];
|
|
||||||
const b = keys[i];
|
|
||||||
if (b.startsWith(a)) {
|
|
||||||
if (b.charAt(a.length) === "." || b.charAt(a.length - 1) === ".") {
|
|
||||||
delete obj[b];
|
|
||||||
keys.splice(i--, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertOldPrecondition(q: Record<string, unknown>): Expression {
|
|
||||||
function recursive(_query): Expression {
|
|
||||||
const expressions: Expression[] = [];
|
|
||||||
for (const [k, v] of Object.entries(_query)) {
|
|
||||||
if (k[0] === "$") {
|
|
||||||
if (k === "$and") {
|
|
||||||
const and: Expression = ["AND"];
|
|
||||||
for (const vv of Object.values(v)) and.push(recursive(vv));
|
|
||||||
expressions.push(and);
|
|
||||||
} else if (k === "$or") {
|
|
||||||
const or: Expression = ["OR"];
|
|
||||||
for (const vv of Object.values(v)) or.push(recursive(vv));
|
|
||||||
expressions.push(or);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Operator ${k} not supported`);
|
|
||||||
}
|
|
||||||
} else if (k === "_tags") {
|
|
||||||
if (typeof v === "object") {
|
|
||||||
if (Array.isArray(v)) throw new Error(`Invalid type`);
|
|
||||||
const conjs: Expression[] = [];
|
|
||||||
for (const [op, val] of Object.entries(v)) {
|
|
||||||
if (op === "$ne") {
|
|
||||||
if (typeof v["$ne"] !== "string")
|
|
||||||
throw new Error("Only string values are allowed for _tags");
|
|
||||||
conjs.push(["IS NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
|
|
||||||
} 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 {
|
|
||||||
expressions.push([
|
|
||||||
"IS NOT NULL",
|
|
||||||
["PARAM", `Tags.${encodeTag(v as string)}`],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} else if (k.startsWith("Tags.")) {
|
|
||||||
let exists: boolean;
|
|
||||||
if (typeof v === "boolean") exists = v;
|
|
||||||
else if (v.hasOwnProperty("$eq")) exists = !!v["$eq"];
|
|
||||||
else if (v.hasOwnProperty("$ne")) exists = !v["$ne"];
|
|
||||||
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
|
|
||||||
else throw new Error(`Invalid tag query`);
|
|
||||||
|
|
||||||
expressions.push([exists ? "IS NOT NULL" : "IS NULL", ["PARAM", k]]);
|
|
||||||
} else if (typeof v === "object") {
|
|
||||||
if (Array.isArray(v)) throw new Error(`Invalid type`);
|
|
||||||
|
|
||||||
const exps: Expression[] = [];
|
|
||||||
for (const [kk, vv] of Object.entries(v)) {
|
|
||||||
if (kk === "$eq") {
|
|
||||||
exps.push(["=", ["PARAM", k], vv]);
|
|
||||||
} else if (kk === "$ne") {
|
|
||||||
exps.push([
|
|
||||||
"OR",
|
|
||||||
["<>", ["PARAM", k], vv],
|
|
||||||
["IS NULL", ["PARAM", k]],
|
|
||||||
]);
|
|
||||||
} else if (kk === "$lt") {
|
|
||||||
exps.push(["<", ["PARAM", k], vv]);
|
|
||||||
} else if (kk === "$lte") {
|
|
||||||
exps.push(["<=", ["PARAM", k], vv]);
|
|
||||||
} else if (kk === "$gt") {
|
|
||||||
exps.push([">", ["PARAM", k], vv]);
|
|
||||||
} else if (kk === "$gte") {
|
|
||||||
exps.push([">=", ["PARAM", k], vv]);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Operator ${kk} not supported`);
|
|
||||||
}
|
|
||||||
if (!["string", "number", "boolean"].includes(typeof vv))
|
|
||||||
throw new Error(`Invalid value for ${kk} operator`);
|
|
||||||
}
|
|
||||||
if (exps.length === 1) {
|
|
||||||
expressions.push(exps[0]);
|
|
||||||
} else if (exps.length > 1) {
|
|
||||||
const and: Expression = ["AND"];
|
|
||||||
expressions.push(and.concat(exps));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expressions.push(["=", ["PARAM", k], v]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (expressions.length === 1) return expressions[0];
|
|
||||||
if (expressions.length === 0) return true;
|
|
||||||
return ["AND", ...expressions];
|
|
||||||
}
|
|
||||||
|
|
||||||
// empty filter
|
|
||||||
if (!Object.keys(q).length) return true;
|
|
||||||
|
|
||||||
return recursive(q);
|
|
||||||
}
|
|
||||||
+64
-96
@@ -1,9 +1,32 @@
|
|||||||
import { IncomingMessage, ServerResponse, ClientRequest } from "node:http";
|
/**
|
||||||
import { Socket } from "node:net";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import { appendFileSync } from "node:fs";
|
*
|
||||||
import { stringify } from "./common/yaml.ts";
|
* This file is part of GenieACS.
|
||||||
import * as config from "./config.ts";
|
*
|
||||||
import { getSocketEndpoints } from "./server.ts";
|
* 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,
|
||||||
|
ServerResponse,
|
||||||
|
ClientRequest,
|
||||||
|
RequestOptions
|
||||||
|
} from "http";
|
||||||
|
import { Socket } from "net";
|
||||||
|
import { appendFileSync } from "fs";
|
||||||
|
import * as yaml from "yaml";
|
||||||
|
import * as config from "./config";
|
||||||
|
|
||||||
const DEBUG_FILE = "" + config.get("DEBUG_FILE");
|
const DEBUG_FILE = "" + config.get("DEBUG_FILE");
|
||||||
const DEBUG_FORMAT = "" + config.get("DEBUG_FORMAT");
|
const DEBUG_FORMAT = "" + config.get("DEBUG_FORMAT");
|
||||||
@@ -22,27 +45,26 @@ function getConnectionTimestamp(connection: Socket): Date {
|
|||||||
export function incomingHttpRequest(
|
export function incomingHttpRequest(
|
||||||
httpRequest: IncomingMessage,
|
httpRequest: IncomingMessage,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
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 socketEndpoints = getSocketEndpoints(con);
|
|
||||||
const msg = {
|
const msg = {
|
||||||
event: "incoming HTTP request",
|
event: "incoming HTTP request",
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
remoteAddress: socketEndpoints.remoteAddress,
|
remoteAddress: con.remoteAddress,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
connection: getConnectionTimestamp(con),
|
connection: getConnectionTimestamp(con),
|
||||||
localPort: socketEndpoints.localPort,
|
localPort: con.localPort,
|
||||||
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`);
|
||||||
@@ -51,25 +73,24 @@ export function incomingHttpRequest(
|
|||||||
export function outgoingHttpResponse(
|
export function outgoingHttpResponse(
|
||||||
httpResponse: ServerResponse,
|
httpResponse: ServerResponse,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
body: string,
|
body: string
|
||||||
): 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 socketEndpoints = getSocketEndpoints(con);
|
|
||||||
const msg = {
|
const msg = {
|
||||||
event: "outgoing HTTP response",
|
event: "outgoing HTTP response",
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
remoteAddress: socketEndpoints.remoteAddress,
|
remoteAddress: con.remoteAddress,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
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`);
|
||||||
@@ -78,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`);
|
||||||
@@ -108,27 +128,26 @@ 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;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
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`);
|
||||||
@@ -137,24 +156,24 @@ export function outgoingHttpRequestError(
|
|||||||
export function incomingHttpResponse(
|
export function incomingHttpResponse(
|
||||||
httpResponse: IncomingMessage,
|
httpResponse: IncomingMessage,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
body: string,
|
body: string
|
||||||
): 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`);
|
||||||
@@ -164,7 +183,7 @@ export function outgoingUdpMessage(
|
|||||||
remoteAddress: string,
|
remoteAddress: string,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
remotePort: number,
|
remotePort: number,
|
||||||
body: string,
|
body: string
|
||||||
): void {
|
): void {
|
||||||
if (!DEBUG_FILE) return;
|
if (!DEBUG_FILE) return;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -174,62 +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")
|
|
||||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
|
||||||
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clientError(remoteAddress: string, err: Error): void {
|
|
||||||
if (!DEBUG_FILE) return;
|
|
||||||
const now = new Date();
|
|
||||||
const msg = {
|
|
||||||
event: "client error",
|
|
||||||
timestamp: now,
|
|
||||||
remoteAddress: remoteAddress,
|
|
||||||
error: err.message,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
|
|
||||||
else if (DEBUG_FORMAT === "json")
|
|
||||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
|
||||||
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function outgoingXmppStanza(deviceId: string, body: string): void {
|
|
||||||
if (!DEBUG_FILE) return;
|
|
||||||
const now = new Date();
|
|
||||||
const msg = {
|
|
||||||
event: "outgoing XMPP stanza",
|
|
||||||
timestamp: now,
|
|
||||||
deviceId: deviceId,
|
|
||||||
body: body,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
|
|
||||||
else if (DEBUG_FORMAT === "json")
|
|
||||||
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
|
|
||||||
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function incomingXmppStanza(deviceId: string, body: string): void {
|
|
||||||
if (!DEBUG_FILE) return;
|
|
||||||
const now = new Date();
|
|
||||||
const msg = {
|
|
||||||
event: "incoming XMPP stanza",
|
|
||||||
timestamp: now,
|
|
||||||
deviceId: deviceId,
|
|
||||||
body: body,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + 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`);
|
||||||
|
|||||||
+50
-56
@@ -1,16 +1,30 @@
|
|||||||
import Path from "./common/path.ts";
|
/**
|
||||||
import * as config from "./config.ts";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import * as device from "./device.ts";
|
*
|
||||||
import * as scheduling from "./scheduling.ts";
|
* This file is part of GenieACS.
|
||||||
import { SessionContext, Declaration } from "./types.ts";
|
*
|
||||||
|
* 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 Path from "./common/path";
|
||||||
|
import * as config from "./config";
|
||||||
|
import * as device from "./device";
|
||||||
|
import * as scheduling from "./scheduling";
|
||||||
|
|
||||||
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 ||
|
||||||
@@ -23,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);
|
||||||
|
|
||||||
@@ -35,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]);
|
||||||
@@ -55,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 ||
|
||||||
@@ -80,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];
|
||||||
}
|
}
|
||||||
@@ -99,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" ||
|
||||||
@@ -123,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({
|
||||||
@@ -142,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({
|
||||||
@@ -161,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" ||
|
||||||
@@ -186,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({
|
||||||
@@ -195,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({
|
||||||
@@ -204,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");
|
||||||
@@ -233,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;
|
||||||
@@ -241,7 +235,7 @@ export function instances(
|
|||||||
const unpacked = device.unpack(
|
const unpacked = device.unpack(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
path,
|
path,
|
||||||
startRevision + 1,
|
startRevision + 1
|
||||||
);
|
);
|
||||||
count = Math.max(0, unpacked.length + count);
|
count = Math.max(0, unpacked.length + count);
|
||||||
}
|
}
|
||||||
@@ -252,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;
|
||||||
|
|||||||
+53
-36
@@ -1,18 +1,31 @@
|
|||||||
import Path from "./common/path.ts";
|
/**
|
||||||
import {
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
DeviceData,
|
*
|
||||||
Declaration,
|
* This file is part of GenieACS.
|
||||||
Clear,
|
*
|
||||||
AttributeTimestamps,
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
Attributes,
|
* it under the terms of the GNU Affero General Public License as
|
||||||
} from "./types.ts";
|
* 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 Path from "./common/path";
|
||||||
|
import { DeviceData, Declaration, Clear } from "./types";
|
||||||
|
|
||||||
const CHANGE_FLAGS = {
|
const CHANGE_FLAGS = {
|
||||||
object: 2,
|
object: 2,
|
||||||
writable: 4,
|
writable: 4,
|
||||||
value: 8,
|
value: 8,
|
||||||
notification: 16,
|
notification: 16,
|
||||||
accessList: 32,
|
accessList: 32
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseBool(v): boolean {
|
function parseBool(v): boolean {
|
||||||
@@ -24,8 +37,8 @@ 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":
|
||||||
@@ -35,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") {
|
||||||
@@ -57,7 +77,7 @@ export function sanitizeParameterValue(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
parameterValue[0] = "" + parameterValue[0];
|
parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0]));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +88,7 @@ export function sanitizeParameterValue(
|
|||||||
export function getAliasDeclarations(
|
export function getAliasDeclarations(
|
||||||
path: Path,
|
path: Path,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
attrGet = null,
|
attrGet = null
|
||||||
): Declaration[] {
|
): Declaration[] {
|
||||||
const stripped = path.stripAlias();
|
const stripped = path.stripAlias();
|
||||||
let decs: Declaration[] = [
|
let decs: Declaration[] = [
|
||||||
@@ -78,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) {
|
||||||
@@ -89,8 +109,8 @@ 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
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +123,7 @@ export function getAliasDeclarations(
|
|||||||
export function unpack(
|
export function unpack(
|
||||||
deviceData: DeviceData,
|
deviceData: DeviceData,
|
||||||
path: Path,
|
path: Path,
|
||||||
revision?: number,
|
revision?: number
|
||||||
): Path[] {
|
): Path[] {
|
||||||
let allMatches = [] as Path[];
|
let allMatches = [] as Path[];
|
||||||
if (!path.alias) {
|
if (!path.alias) {
|
||||||
@@ -171,18 +191,18 @@ 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 = {};
|
||||||
|
|
||||||
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))
|
||||||
@@ -193,7 +213,7 @@ export function clear(
|
|||||||
path,
|
path,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
descendantsTimestamp ? 99 : path.length,
|
descendantsTimestamp ? 99 : path.length
|
||||||
)) {
|
)) {
|
||||||
const tracker = deviceData.trackers.get(p);
|
const tracker = deviceData.trackers.get(p);
|
||||||
for (const k in tracker) changeTrackers[k] |= tracker[k];
|
for (const k in tracker) changeTrackers[k] |= tracker[k];
|
||||||
@@ -253,9 +273,9 @@ 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);
|
||||||
|
|
||||||
@@ -282,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);
|
||||||
|
|
||||||
@@ -296,7 +316,7 @@ export function set(
|
|||||||
else if (
|
else if (
|
||||||
!compareEquality(
|
!compareEquality(
|
||||||
attributes[attrName][1],
|
attributes[attrName][1],
|
||||||
currentAttributes[attrName][1],
|
currentAttributes[attrName][1]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
changeFlags |= CHANGE_FLAGS[attrName];
|
changeFlags |= CHANGE_FLAGS[attrName];
|
||||||
@@ -315,7 +335,7 @@ export function set(
|
|||||||
path.slice(0, path.length - 1),
|
path.slice(0, path.length - 1),
|
||||||
timestamp,
|
timestamp,
|
||||||
{ object: [timestamp, 1] },
|
{ object: [timestamp, 1] },
|
||||||
toClear,
|
toClear
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,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;
|
||||||
@@ -375,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];
|
||||||
|
|||||||
+41
-36
@@ -1,18 +1,43 @@
|
|||||||
import { spawn, ChildProcess } from "node:child_process";
|
/**
|
||||||
import * as crypto from "node:crypto";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import readline from "node:readline";
|
*
|
||||||
import * as config from "./config.ts";
|
* This file is part of GenieACS.
|
||||||
import { Fault } from "./types.ts";
|
*
|
||||||
import { ROOT_DIR } from "./config.ts";
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
import * as logger from "./logger.ts";
|
* 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 { spawn, ChildProcess } from "child_process";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
import * as config from "./config";
|
||||||
|
import { Fault } from "./types";
|
||||||
|
import { ROOT_DIR } from "./config";
|
||||||
|
|
||||||
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");
|
||||||
@@ -20,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];
|
||||||
}
|
}
|
||||||
@@ -43,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);
|
||||||
@@ -81,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();
|
||||||
@@ -104,6 +109,6 @@ export async function killAll(): Promise<void> {
|
|||||||
Object.entries(processes).map(([k, p]) => {
|
Object.entries(processes).map(([k, p]) => {
|
||||||
delete processes[k];
|
delete processes[k];
|
||||||
return kill(p);
|
return kill(p);
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import { IncomingMessage } from "node:http";
|
|
||||||
import { TLSSocket } from "node:tls";
|
|
||||||
import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js";
|
|
||||||
import * as config from "./config.ts";
|
|
||||||
import { getSocketEndpoints } from "./server.ts";
|
|
||||||
|
|
||||||
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;
|
|
||||||
const socketEndpoints = getSocketEndpoints(soc);
|
|
||||||
origin = {
|
|
||||||
localAddress: socketEndpoints.localAddress,
|
|
||||||
localPort: socketEndpoints.localPort,
|
|
||||||
remoteAddress: socketEndpoints.remoteAddress,
|
|
||||||
remotePort: socketEndpoints.remotePort,
|
|
||||||
host: request.headers["host"],
|
|
||||||
encrypted: !!(request.socket as TLSSocket).encrypted,
|
|
||||||
};
|
|
||||||
|
|
||||||
const header = request.headers["forwarded"];
|
|
||||||
if (header) {
|
|
||||||
const ip = parse(socketEndpoints.remoteAddress);
|
|
||||||
if (
|
|
||||||
cidrs.some((cidr) => ip.kind() === cidr[0].kind() && ip.match(cidr))
|
|
||||||
) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,203 +1,63 @@
|
|||||||
import * as url from "node:url";
|
/**
|
||||||
import { IncomingMessage, ServerResponse } from "node:http";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import { PassThrough, pipeline, Readable } from "node:stream";
|
*
|
||||||
import { createHash } from "node:crypto";
|
* This file is part of GenieACS.
|
||||||
import { filesBucket, collections } from "./db/db.ts";
|
*
|
||||||
import * as logger from "./logger.ts";
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
import { getRequestOrigin } from "./forwarded.ts";
|
* it under the terms of the GNU Affero General Public License as
|
||||||
import memoize from "./common/memoize.ts";
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
const getFile = memoize(
|
import * as url from "url";
|
||||||
async (
|
import * as querystring from "querystring";
|
||||||
etag: string,
|
import { GridFSBucket } from "mongodb";
|
||||||
size: number,
|
import * as db from "./db";
|
||||||
filename: string,
|
import * as logger from "./logger";
|
||||||
): Promise<Iterable<Buffer>> => {
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
// Using for-await over the download stream can throw ERR_STREAM_PREMATURE_CLOSE
|
|
||||||
// for very small files. Possibly a bug in MongoDB driver or Nodejs itself.
|
|
||||||
// Using a PassThrough stream to avoid this.
|
|
||||||
const downloadStream = pipeline(
|
|
||||||
filesBucket.openDownloadStreamByName(filename),
|
|
||||||
new PassThrough(),
|
|
||||||
(err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
for await (const chunk of downloadStream) chunks.push(chunk);
|
|
||||||
// Node 12-14 don't throw error when stream is closed prematurely.
|
|
||||||
// However, we don't need to check for that since we're checking file size.
|
|
||||||
if (size !== chunks.reduce((a, b) => a + b.length, 0))
|
|
||||||
throw new Error("File size mismatch");
|
|
||||||
return chunks;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
async function* partialContent(
|
|
||||||
chunks: Iterable<Buffer>,
|
|
||||||
start: number,
|
|
||||||
end: number,
|
|
||||||
): AsyncIterable<Buffer> {
|
|
||||||
let bytesToSkip = start;
|
|
||||||
let bytesToRead = end - start;
|
|
||||||
|
|
||||||
for (let chunk of chunks) {
|
|
||||||
if (bytesToRead <= 0) return;
|
|
||||||
if (bytesToSkip >= chunk.length) {
|
|
||||||
bytesToSkip -= chunk.length;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
chunk = chunk.subarray(bytesToSkip, bytesToSkip + bytesToRead);
|
|
||||||
bytesToRead -= chunk.length;
|
|
||||||
bytesToSkip = 0;
|
|
||||||
yield chunk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateETag(file: {
|
|
||||||
_id: string;
|
|
||||||
uploadDate: Date;
|
|
||||||
length: number;
|
|
||||||
}): string {
|
|
||||||
const hash = createHash("md5");
|
|
||||||
hash.update(`${file._id}-${file.uploadDate.getTime()}-${file.length}`);
|
|
||||||
return hash.digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchEtag(etag: string, header: string): boolean {
|
|
||||||
for (let t of header.split(",")) {
|
|
||||||
t = t.trim();
|
|
||||||
if (t.startsWith("W/")) t = t.substring(2);
|
|
||||||
try {
|
|
||||||
t = JSON.parse(t);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
if (t === "*") return true;
|
|
||||||
if (etag === t) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listener(
|
|
||||||
request: IncomingMessage,
|
|
||||||
response: ServerResponse,
|
|
||||||
): Promise<void> {
|
|
||||||
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
||||||
response.writeHead(405, { Allow: "GET, HEAD" });
|
|
||||||
response.end("405 Method Not Allowed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export function listener(request, response): void {
|
||||||
const urlParts = url.parse(request.url, true);
|
const urlParts = url.parse(request.url, true);
|
||||||
const filename = decodeURIComponent(urlParts.pathname.substring(1));
|
if (request.method === "GET") {
|
||||||
|
const filename = querystring.unescape(urlParts.pathname.substring(1));
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: "Fetch file",
|
message: "Fetch file",
|
||||||
filename: filename,
|
filename: filename,
|
||||||
remoteAddress: getRequestOrigin(request).remoteAddress,
|
remoteAddress: request.connection.remoteAddress
|
||||||
method: request.method,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const file = await collections.files.findOne({ _id: filename });
|
db.filesCollection.findOne({ _id: filename }, (err, file) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
response.writeHead(404);
|
response.writeHead(404);
|
||||||
response.end();
|
response.end();
|
||||||
log.message += " not found";
|
log.message += " not found";
|
||||||
logger.accessError(log);
|
logger.accessError(log);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
logger.accessInfo(log);
|
|
||||||
|
|
||||||
const etag = generateETag(file);
|
|
||||||
const lastModified = file["uploadDate"];
|
|
||||||
lastModified.setMilliseconds(0);
|
|
||||||
|
|
||||||
let status = 200;
|
|
||||||
let start = 0;
|
|
||||||
let end = file.length;
|
|
||||||
|
|
||||||
if (request.headers["if-match"])
|
|
||||||
if (!matchEtag(etag, request.headers["if-match"])) status = 412;
|
|
||||||
|
|
||||||
if (request.headers["if-unmodified-since"]) {
|
|
||||||
const d = new Date(request.headers["if-unmodified-since"]);
|
|
||||||
if (lastModified > d) status = 412;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.headers["if-none-match"]) {
|
|
||||||
if (matchEtag(etag, request.headers["if-none-match"])) status = 304;
|
|
||||||
} else if (request.headers["if-modified-since"]) {
|
|
||||||
const d = new Date(request.headers["if-modified-since"]);
|
|
||||||
if (lastModified <= d) status = 304;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.headers.range && status === 200) {
|
|
||||||
const match = request.headers.range.match(/^bytes=(\d*)-(\d*)$/);
|
|
||||||
status = 416;
|
|
||||||
if (match && (match[1] || match[2])) {
|
|
||||||
if (match[2]) end = parseInt(match[2]) + 1;
|
|
||||||
if (match[1]) start = parseInt(match[1]);
|
|
||||||
else start = file.length - parseInt(match[2]);
|
|
||||||
if (start < end && end <= file.length) status = 206;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.headers["if-range"]) {
|
|
||||||
const h = request.headers["if-range"] as string;
|
|
||||||
const d = new Date(h);
|
|
||||||
if (!matchEtag(etag, h) && !(lastModified <= d)) {
|
|
||||||
status = 200;
|
|
||||||
start = 0;
|
|
||||||
end = file.length;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 412) {
|
response.writeHead(200, {
|
||||||
response.writeHead(412);
|
"Content-Type": file.contentType || "application/octet-stream",
|
||||||
response.end();
|
"Content-Length": file.length
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 304) {
|
const bucket = new GridFSBucket(db.client.db());
|
||||||
response.writeHead(304, {
|
const downloadStream = bucket.openDownloadStreamByName(filename);
|
||||||
ETag: etag,
|
downloadStream.pipe(response);
|
||||||
"Last-Modified": lastModified.toUTCString(),
|
|
||||||
|
logger.accessInfo(log);
|
||||||
});
|
});
|
||||||
response.end();
|
} else {
|
||||||
return;
|
response.writeHead(405, { Allow: "GET" });
|
||||||
|
response.end("405 Method Not Allowed");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 416) {
|
|
||||||
response.writeHead(416, {
|
|
||||||
"Content-Range": `bytes */${file.length}`,
|
|
||||||
"Content-Length": "0",
|
|
||||||
});
|
|
||||||
response.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.writeHead(status, {
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
"Content-Length": end - start,
|
|
||||||
"Accept-Ranges": "bytes",
|
|
||||||
ETag: etag,
|
|
||||||
"Last-Modified": lastModified.toUTCString(),
|
|
||||||
...(status === 206 && {
|
|
||||||
"Content-Range": `bytes ${start}-${end - 1}/${file.length}`,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (request.method === "HEAD") {
|
|
||||||
response.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunks = await getFile(etag, file.length, filename);
|
|
||||||
|
|
||||||
pipeline(Readable.from(partialContent(chunks, start, end)), response, () => {
|
|
||||||
// Ignore errors resulting from client disconnecting
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
+21
-2
@@ -1,4 +1,23 @@
|
|||||||
import Path from "./common/path.ts";
|
import Path from "./common/path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
const WILDCARD_MULTIPLIER = 2;
|
const WILDCARD_MULTIPLIER = 2;
|
||||||
const UNDISCOVERED_DEPTH = 7;
|
const UNDISCOVERED_DEPTH = 7;
|
||||||
@@ -12,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][] = [];
|
||||||
|
|||||||
+85
-76
@@ -1,14 +1,26 @@
|
|||||||
import { getRevision, getUiConfig, getUsers } from "./ui/local-cache.ts";
|
/**
|
||||||
import { generateSalt, hashPassword } from "./auth.ts";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import { collections } from "./db/db.ts";
|
*
|
||||||
import {
|
* This file is part of GenieACS.
|
||||||
putConfig,
|
*
|
||||||
putPermission,
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
putPreset,
|
* it under the terms of the GNU Affero General Public License as
|
||||||
putProvision,
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
putUser,
|
* License, or (at your option) any later version.
|
||||||
} from "./ui/db.ts";
|
*
|
||||||
import { del } from "./cache.ts";
|
* 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 localCache from "./local-cache";
|
||||||
|
import { generateSalt, hashPassword } from "./auth";
|
||||||
|
import * as db from "./ui/db";
|
||||||
|
import { del } from "./cache";
|
||||||
|
|
||||||
interface Status {
|
interface Status {
|
||||||
users: boolean;
|
users: boolean;
|
||||||
@@ -47,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;
|
||||||
@@ -72,24 +84,22 @@ declare("Device.ManagementServer.PeriodicInformTime", {value: daily}, {value: in
|
|||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
export async function getStatus(): Promise<Status> {
|
export async function getStatus(): Promise<Status> {
|
||||||
const [configSnapshot, presetCount] = await Promise.all([
|
const configSnapshot = await localCache.getCurrentSnapshot();
|
||||||
getRevision(),
|
const users = localCache.getUsers(configSnapshot);
|
||||||
collections.presets.countDocuments(),
|
const presets = localCache.getPresets(configSnapshot);
|
||||||
]);
|
const ui = localCache.getUiConfig(configSnapshot);
|
||||||
const users = getUsers(configSnapshot);
|
|
||||||
const ui = getUiConfig(configSnapshot);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users: !Object.keys(users).length,
|
users: !Object.keys(users).length,
|
||||||
presets: !presetCount,
|
presets: !presets.length,
|
||||||
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 = [];
|
||||||
|
|
||||||
@@ -107,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"] }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,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'" }
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,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" },
|
||||||
@@ -268,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'" }
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,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'" },
|
||||||
@@ -312,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" }
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,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'" }
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,23 +378,23 @@ 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 }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resources["permissions"]) {
|
if (resources["permissions"]) {
|
||||||
for (const p of resources["permissions"]) {
|
for (const p of resources["permissions"]) {
|
||||||
p["_id"] = `${p["role"]}:${p["resource"]}:${p["access"]}`;
|
p["_id"] = `${p["role"]}:${p["resource"]}:${p["access"]}`;
|
||||||
proms.push(putPermission(p["_id"], p));
|
proms.push(db.putPermission(p["_id"], p));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,21 +405,21 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
|
|||||||
u["roles"] = (u["roles"] || []).join(",");
|
u["roles"] = (u["roles"] || []).join(",");
|
||||||
u["_id"] = u["username"];
|
u["_id"] = u["username"];
|
||||||
delete u["username"];
|
delete u["username"];
|
||||||
proms.push(putUser(u["_id"], u));
|
proms.push(db.putUser(u["_id"], u));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resources["provisions"]) {
|
if (resources["provisions"]) {
|
||||||
for (const p of resources["provisions"])
|
for (const p of resources["provisions"])
|
||||||
proms.push(putProvision(p["_id"], p));
|
proms.push(db.putProvision(p["_id"], p));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resources["presets"])
|
if (resources["presets"])
|
||||||
for (const p of resources["presets"]) proms.push(putPreset(p["_id"], p));
|
for (const p of resources["presets"]) proms.push(db.putPreset(p["_id"], p));
|
||||||
|
|
||||||
if (resources["config"])
|
if (resources["config"])
|
||||||
for (const c of resources["config"]) proms.push(putConfig(c["_id"], c));
|
for (const c of resources["config"]) proms.push(db.putConfig(c["_id"], c));
|
||||||
|
|
||||||
await proms;
|
await proms;
|
||||||
await Promise.all([del("ui-local-cache-hash"), del("cwmp-local-cache-hash")]);
|
return del("presets_hash");
|
||||||
}
|
}
|
||||||
+20
-1
@@ -1,9 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
interface Instance {
|
interface Instance {
|
||||||
[name: string]: string;
|
[name: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|||||||
+502
-51
@@ -1,73 +1,524 @@
|
|||||||
import { setTimeoutPromise } from "./util.ts";
|
/**
|
||||||
import { get, set } from "./cache.ts";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import { acquireLock, releaseLock } from "./lock.ts";
|
*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
const REFRESH = 5000;
|
import * as vm from "vm";
|
||||||
const EVICT_TIMEOUT = 120000;
|
import * as crypto from "crypto";
|
||||||
|
import * as config from "./config";
|
||||||
|
import * as db from "./db";
|
||||||
|
import * as cache from "./cache";
|
||||||
|
import { mongoQueryToFilter } from "./mongodb-functions";
|
||||||
|
import * as logger from "./logger";
|
||||||
|
import * as scheduling from "./scheduling";
|
||||||
|
import * as expression from "./common/expression";
|
||||||
|
import { parse } from "./common/expression-parser";
|
||||||
|
import { Preset, Expression } from "./types";
|
||||||
|
|
||||||
export class LocalCache<T> {
|
interface Permissions {
|
||||||
private nextRefresh = 1;
|
[role: string]: {
|
||||||
private currentRevision: string = null;
|
[access: number]: {
|
||||||
private snapshots: Map<string, T> = new Map();
|
[resource: string]: {
|
||||||
|
access: number;
|
||||||
constructor(
|
filter: Expression;
|
||||||
private cacheKey: string,
|
validate?: Expression;
|
||||||
private callback: () => Promise<[string, T]>,
|
};
|
||||||
) {}
|
};
|
||||||
|
};
|
||||||
async getRevision(): Promise<string> {
|
}
|
||||||
if (Date.now() > this.nextRefresh) await this.refresh();
|
|
||||||
return this.currentRevision;
|
const REFRESH = 3000;
|
||||||
|
const EVICT_TIMEOUT = 60000;
|
||||||
|
|
||||||
|
const snapshots = new Map();
|
||||||
|
let currentSnapshot = null;
|
||||||
|
let nextRefresh = 1;
|
||||||
|
|
||||||
|
function computeHash(snapshot): string {
|
||||||
|
// MD5 hash for presets, provisions, virtual parameters for detecting changes
|
||||||
|
const h = crypto.createHash("md5");
|
||||||
|
for (const p of snapshot.presets) {
|
||||||
|
h.update(JSON.stringify(p.name));
|
||||||
|
h.update(JSON.stringify(p.channel));
|
||||||
|
h.update(JSON.stringify(p.schedule));
|
||||||
|
h.update(JSON.stringify(p.events));
|
||||||
|
h.update(JSON.stringify(p.precondition));
|
||||||
|
h.update(JSON.stringify(p.provisions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let keys;
|
||||||
|
|
||||||
|
keys = Object.keys(snapshot.provisions).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (const k of keys) h.update(snapshot.provisions[k].md5);
|
||||||
|
|
||||||
|
keys = Object.keys(snapshot.virtualParameters).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (const k of keys) h.update(snapshot.virtualParameters[k].md5);
|
||||||
|
|
||||||
hasRevision(revision: string): boolean {
|
keys = Object.keys(snapshot.config).sort();
|
||||||
return this.snapshots.has(revision);
|
h.update(JSON.stringify(keys));
|
||||||
|
for (const k of keys) h.update(JSON.stringify(snapshot.config[k]));
|
||||||
|
|
||||||
|
keys = Object.keys(snapshot.files).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (const k of keys) h.update(JSON.stringify(snapshot.files[k]));
|
||||||
|
|
||||||
|
keys = Object.keys(snapshot.users).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (const k of keys) h.update(JSON.stringify(snapshot.users[k]));
|
||||||
|
|
||||||
|
const roles = Object.keys(snapshot.permissions).sort();
|
||||||
|
h.update(JSON.stringify(roles));
|
||||||
|
for (const r of roles) {
|
||||||
|
const levels = Object.keys(snapshot.permissions[r]).sort();
|
||||||
|
h.update(JSON.stringify(levels));
|
||||||
|
for (const l of levels) {
|
||||||
|
keys = Object.keys(snapshot.permissions[r][l]).sort();
|
||||||
|
h.update(JSON.stringify(keys));
|
||||||
|
for (const k of keys)
|
||||||
|
h.update(JSON.stringify(snapshot.permissions[r][l][k]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return h.digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
get(revision: string): T {
|
function flattenObject(src, prefix = "", dst = {}): {} {
|
||||||
const snapshot = this.snapshots.get(revision);
|
for (const k of Object.keys(src)) {
|
||||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
const v = src[k];
|
||||||
return snapshot;
|
if (typeof v === "object" && !Array.isArray(v))
|
||||||
|
flattenObject(v, `${prefix}${k}.`, dst);
|
||||||
|
else dst[`${prefix}${k}`] = v;
|
||||||
}
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
async refresh(): Promise<void> {
|
async function fetchPresets(): Promise<Preset[]> {
|
||||||
if (!this.nextRefresh) {
|
const res = await db.getPresets();
|
||||||
await setTimeoutPromise(20);
|
let objects = await db.getObjects();
|
||||||
await this.refresh();
|
|
||||||
return;
|
objects = objects.map(obj => {
|
||||||
|
// Flatten object
|
||||||
|
obj = flattenObject(obj);
|
||||||
|
|
||||||
|
// If no keys are defined, consider all parameters as keys to keep the
|
||||||
|
// same behavior from v1.0
|
||||||
|
if (!obj["_keys"] || !obj["_keys"].length)
|
||||||
|
obj["_keys"] = Object.keys(obj).filter(k => !k.startsWith("_"));
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.sort((a, b) => {
|
||||||
|
if (a["weight"] === b["weight"])
|
||||||
|
return a["_id"] > b["_id"] ? 1 : a["_id"] < b["_id"] ? -1 : 0;
|
||||||
|
else return a["weight"] - b["weight"];
|
||||||
|
});
|
||||||
|
|
||||||
|
const presets = [] as Preset[];
|
||||||
|
for (const preset of res) {
|
||||||
|
let schedule: { md5: string; duration: number; schedule: any } = null;
|
||||||
|
if (preset["schedule"]) {
|
||||||
|
const parts = preset["schedule"].trim().split(/\s+/);
|
||||||
|
schedule = {
|
||||||
|
md5: crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(preset["schedule"])
|
||||||
|
.digest("hex"),
|
||||||
|
duration: null,
|
||||||
|
schedule: null
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
schedule.duration = +parts.shift() * 1000;
|
||||||
|
schedule.schedule = scheduling.parseCron(parts.join(" "));
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn({
|
||||||
|
message: "Invalid preset schedule",
|
||||||
|
preset: preset["_id"],
|
||||||
|
schedule: preset["schedule"]
|
||||||
|
});
|
||||||
|
schedule.schedule = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const events = preset["events"] || {};
|
||||||
|
let precondition = true as Expression;
|
||||||
|
if (preset["precondition"]) {
|
||||||
|
try {
|
||||||
|
precondition = parse(preset["precondition"]);
|
||||||
|
} catch (error) {
|
||||||
|
precondition = mongoQueryToFilter(JSON.parse(preset["precondition"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplify expression
|
||||||
|
precondition = expression.evaluate(precondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _provisions = preset["provisions"] || [];
|
||||||
|
|
||||||
const now = Date.now();
|
// Generate provisions from the old configuration format
|
||||||
if (now < this.nextRefresh) return;
|
for (const c of preset["configurations"]) {
|
||||||
this.nextRefresh = 0;
|
switch (c.type) {
|
||||||
|
case "age":
|
||||||
|
_provisions.push(["refresh", c.name, c.age]);
|
||||||
|
break;
|
||||||
|
|
||||||
const dbHash = await get(this.cacheKey);
|
case "value":
|
||||||
|
_provisions.push(["value", c.name, c.value]);
|
||||||
|
break;
|
||||||
|
|
||||||
if (this.currentRevision && dbHash === this.currentRevision) {
|
case "add_tag":
|
||||||
this.nextRefresh = now + (REFRESH - (now % REFRESH));
|
_provisions.push(["tag", c.tag, true]);
|
||||||
return;
|
break;
|
||||||
|
|
||||||
|
case "delete_tag":
|
||||||
|
_provisions.push(["tag", c.tag, false]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "provision":
|
||||||
|
_provisions.push([c.name].concat(c.args || []));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "add_object":
|
||||||
|
for (const obj of objects) {
|
||||||
|
if (obj["_id"] === c.object) {
|
||||||
|
const alias = obj["_keys"]
|
||||||
|
.map(k => `${k}:${JSON.stringify(obj[k])}`)
|
||||||
|
.join(",");
|
||||||
|
const p = `${c.name}.[${alias}]`;
|
||||||
|
_provisions.push(["instances", p, 1]);
|
||||||
|
|
||||||
|
for (const k in obj) {
|
||||||
|
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
|
||||||
|
_provisions.push(["value", `${p}.${k}`, obj[k]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "delete_object":
|
||||||
|
for (const obj of objects) {
|
||||||
|
if (obj["_id"] === c.object) {
|
||||||
|
const alias = obj["_keys"]
|
||||||
|
.map(k => `${k}:${JSON.stringify(obj[k])}`)
|
||||||
|
.join(",");
|
||||||
|
const p = `${c.name}.[${alias}]`;
|
||||||
|
_provisions.push(["instances", p, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown configuration type ${c.type}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
presets.push({
|
||||||
|
name: preset["_id"],
|
||||||
|
channel: (preset["channel"] as string) || "default",
|
||||||
|
schedule: schedule,
|
||||||
|
events: events,
|
||||||
|
precondition: precondition,
|
||||||
|
provisions: _provisions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchProvisions(): Promise<{}> {
|
||||||
|
const res = await db.getProvisions();
|
||||||
|
|
||||||
|
const provisions = {};
|
||||||
|
for (const r of res) {
|
||||||
|
provisions[r["_id"]] = {};
|
||||||
|
provisions[r["_id"]].md5 = crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(r["script"])
|
||||||
|
.digest("hex");
|
||||||
|
provisions[r["_id"]].script = new vm.Script(
|
||||||
|
`"use strict";(function(){\n${r["script"]}\n})();`,
|
||||||
|
{ filename: r["_id"], lineOffset: -1, timeout: 50 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return provisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchVirtualParameters(): Promise<{}> {
|
||||||
|
const res = await db.getVirtualParameters();
|
||||||
|
|
||||||
|
const virtualParameters = {};
|
||||||
|
for (const r of res) {
|
||||||
|
virtualParameters[r["_id"]] = {};
|
||||||
|
virtualParameters[r["_id"]].md5 = crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(r["script"])
|
||||||
|
.digest("hex");
|
||||||
|
virtualParameters[r["_id"]].script = new vm.Script(
|
||||||
|
`"use strict";(function(){\n${r["script"]}\n})();`,
|
||||||
|
{ filename: r["_id"], lineOffset: -1, timeout: 50 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return virtualParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPermissions(): Promise<Permissions> {
|
||||||
|
const perms = await db.getPermissions();
|
||||||
|
const permissions: Permissions = {};
|
||||||
|
|
||||||
|
for (const p of perms) {
|
||||||
|
if (!permissions[p.role]) permissions[p.role] = {};
|
||||||
|
if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {};
|
||||||
|
|
||||||
const lockToken = await acquireLock(this.cacheKey, 5000);
|
permissions[p.role][p.access][p.resource] = {
|
||||||
|
access: p.access,
|
||||||
|
filter: parse(p.filter || "true")
|
||||||
|
};
|
||||||
|
if (p.validate)
|
||||||
|
permissions[p.role][p.access][p.resource].validate = parse(p.validate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFiles(): Promise<{}> {
|
||||||
|
const res = await db.getFiles();
|
||||||
|
const files = {};
|
||||||
|
|
||||||
|
for (const r of res) {
|
||||||
|
const id = r["filename"] || r["_id"].toString();
|
||||||
|
files[id] = {};
|
||||||
|
files[id].length = r["length"];
|
||||||
|
files[id].md5 = r["md5"];
|
||||||
|
files[id].contentType = r["contentType"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUsers(): Promise<{}> {
|
||||||
|
const _users = await db.getUsers();
|
||||||
|
const users = {};
|
||||||
|
|
||||||
|
for (const user of _users) {
|
||||||
|
users[user._id] = {
|
||||||
|
password: user.password,
|
||||||
|
salt: user.salt,
|
||||||
|
roles: user.roles.split(",").map(s => s.trim())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchConfig(): Promise<[{}, {}]> {
|
||||||
|
const conf = await db.getConfig();
|
||||||
|
|
||||||
const [hash, snapshot] = await this.callback();
|
conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0));
|
||||||
|
|
||||||
if (this.currentRevision) {
|
const ui = {
|
||||||
const r = this.currentRevision;
|
filters: {},
|
||||||
const s = this.snapshots.get(r);
|
device: {},
|
||||||
setTimeout(() => {
|
index: {},
|
||||||
if (this.snapshots.get(r) === s) this.snapshots.delete(r);
|
overview: {}
|
||||||
}, EVICT_TIMEOUT).unref();
|
};
|
||||||
|
const _config = {};
|
||||||
|
|
||||||
|
for (const c of conf) {
|
||||||
|
// Evaluate expressions to simplify them
|
||||||
|
const val = expression.evaluate(c.value);
|
||||||
|
_config[c.id] = val;
|
||||||
|
if (c.id.startsWith("ui.")) {
|
||||||
|
const keys = c.id.split(".");
|
||||||
|
// remove the first key(ui)
|
||||||
|
keys.shift();
|
||||||
|
let ref = ui;
|
||||||
|
while (keys.length > 1) {
|
||||||
|
const k = keys.shift();
|
||||||
|
if (typeof ref[k] !== "object") ref[k] = {};
|
||||||
|
ref = ref[k];
|
||||||
|
}
|
||||||
|
ref[keys[0]] = val;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [_config, ui];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh(): Promise<void> {
|
||||||
|
if (!nextRefresh) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 20));
|
||||||
|
await refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextRefresh = 0;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const dbHash = await cache.get("presets_hash");
|
||||||
|
|
||||||
|
if (currentSnapshot && dbHash === currentSnapshot) {
|
||||||
|
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlockOrExtend = await cache.lock("presets_hash_lock", 3);
|
||||||
|
|
||||||
this.currentRevision = hash;
|
const res = await Promise.all([
|
||||||
this.snapshots.set(hash, snapshot);
|
fetchPresets(),
|
||||||
|
fetchProvisions(),
|
||||||
|
fetchVirtualParameters(),
|
||||||
|
fetchFiles(),
|
||||||
|
fetchPermissions(),
|
||||||
|
fetchUsers(),
|
||||||
|
fetchConfig()
|
||||||
|
]);
|
||||||
|
|
||||||
if (lockToken) {
|
const snapshot = {
|
||||||
if (hash !== dbHash) await set(this.cacheKey, hash, 300);
|
presets: res[0],
|
||||||
await releaseLock(this.cacheKey, lockToken);
|
provisions: res[1],
|
||||||
}
|
virtualParameters: res[2],
|
||||||
|
files: res[3],
|
||||||
|
permissions: res[4],
|
||||||
|
users: res[5],
|
||||||
|
config: res[6][0],
|
||||||
|
ui: res[6][1]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentSnapshot) {
|
||||||
|
const h = currentSnapshot;
|
||||||
|
const s = snapshots.get(h);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (snapshots.get(h) === s) snapshots.delete(h);
|
||||||
|
}, EVICT_TIMEOUT).unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSnapshot = computeHash(snapshot);
|
||||||
|
snapshots.set(currentSnapshot, snapshot);
|
||||||
|
await cache.set("presets_hash", currentSnapshot, 300);
|
||||||
|
await unlockOrExtend(0);
|
||||||
|
|
||||||
|
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentSnapshot(): Promise<string> {
|
||||||
|
if (Date.now() > nextRefresh) await refresh();
|
||||||
|
return currentSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasSnapshot(hash): boolean {
|
||||||
|
return snapshots.has(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPresets(snapshotKey): Preset[] {
|
||||||
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
return snapshot.presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProvisions(
|
||||||
|
snapshotKey
|
||||||
|
): { [name: string]: { md5: string; script: vm.Script } } {
|
||||||
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
return snapshot.provisions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVirtualParameters(
|
||||||
|
snapshotKey
|
||||||
|
): { [name: string]: { md5: string; script: vm.Script } } {
|
||||||
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
return snapshot.virtualParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFiles(
|
||||||
|
snapshotKey
|
||||||
|
): { [name: string]: { length: number; md5: string; contentType: string } } {
|
||||||
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
return snapshot.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfig(
|
||||||
|
snapshotKey,
|
||||||
|
key,
|
||||||
|
context?,
|
||||||
|
now?
|
||||||
|
): string | number | boolean | null {
|
||||||
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
|
||||||
|
const oldOpts = {
|
||||||
|
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
|
||||||
|
"cwmp.debug": "DEBUG",
|
||||||
|
"cwmp.retryDelay": "RETRY_DELAY",
|
||||||
|
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
|
||||||
|
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
|
||||||
|
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
|
||||||
|
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
|
||||||
|
"cwmp.cookiesPath": "COOKIES_PATH",
|
||||||
|
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
|
||||||
|
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
|
||||||
|
"cwmp.connectionRequestAllowBasicAuth":
|
||||||
|
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
|
||||||
|
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
|
||||||
|
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
|
||||||
|
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT"
|
||||||
|
};
|
||||||
|
|
||||||
this.nextRefresh = now + (REFRESH - (now % REFRESH));
|
if (!(key in snapshot.config)) {
|
||||||
|
if (key in oldOpts)
|
||||||
|
return config.get(oldOpts[key], context ? context.id : null);
|
||||||
|
else return null;
|
||||||
}
|
}
|
||||||
|
return expression.evaluate(snapshot.config[key], context, now || Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigExpression(snapshotKey, key): Expression {
|
||||||
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
|
||||||
|
return snapshot.config[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUsers(snapshotKey): {} {
|
||||||
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
|
||||||
|
return snapshot.users;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPermissions(snapshotKey): {} {
|
||||||
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
|
||||||
|
return snapshot.permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUiConfig(snapshotKey): {} {
|
||||||
|
const snapshot = snapshots.get(snapshotKey);
|
||||||
|
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||||
|
|
||||||
|
return snapshot.ui;
|
||||||
}
|
}
|
||||||
-50
@@ -1,50 +0,0 @@
|
|||||||
import { collections } from "./db/db.ts";
|
|
||||||
|
|
||||||
const CLOCK_SKEW_TOLERANCE = 30000;
|
|
||||||
|
|
||||||
export async function acquireLock(
|
|
||||||
lockName: string,
|
|
||||||
ttl: number,
|
|
||||||
timeout = 0,
|
|
||||||
token = Math.random().toString(36).slice(2),
|
|
||||||
): Promise<string> {
|
|
||||||
try {
|
|
||||||
const now = Date.now();
|
|
||||||
const r = await collections.locks.findOneAndUpdate(
|
|
||||||
{ _id: lockName, value: token },
|
|
||||||
{
|
|
||||||
$set: {
|
|
||||||
expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE),
|
|
||||||
},
|
|
||||||
$currentDate: { timestamp: true },
|
|
||||||
},
|
|
||||||
{ upsert: true, returnDocument: "after" },
|
|
||||||
);
|
|
||||||
if (Math.abs(r.value.timestamp.getTime() - now) > CLOCK_SKEW_TOLERANCE)
|
|
||||||
throw new Error("Database clock skew too great");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 11000) throw err;
|
|
||||||
if (!(timeout > 0)) return null;
|
|
||||||
const w = 50 + Math.random() * 50;
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, w));
|
|
||||||
return acquireLock(lockName, ttl, timeout - w, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function releaseLock(
|
|
||||||
lockName: string,
|
|
||||||
token: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const res = await collections.locks.deleteOne({
|
|
||||||
_id: lockName,
|
|
||||||
value: token,
|
|
||||||
});
|
|
||||||
if (res.deletedCount !== 1) throw new Error("Lock expired");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getToken(lockName: string): Promise<string> {
|
|
||||||
const res = await collections.locks.findOne({ _id: lockName });
|
|
||||||
return res?.value;
|
|
||||||
}
|
|
||||||
+71
-80
@@ -1,15 +1,26 @@
|
|||||||
import * as fs from "node:fs";
|
/**
|
||||||
import * as os from "node:os";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import * as config from "./config.ts";
|
*
|
||||||
import { getRequestOrigin } from "./forwarded.ts";
|
* This file is part of GenieACS.
|
||||||
import {
|
*
|
||||||
SessionContext,
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
AcsRequest,
|
* it under the terms of the GNU Affero General Public License as
|
||||||
CpeRequest,
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
CpeFault,
|
* License, or (at your option) any later version.
|
||||||
InformRequest,
|
*
|
||||||
Fault,
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
} from "./types.ts";
|
* 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 os from "os";
|
||||||
|
|
||||||
|
import * as config from "./config";
|
||||||
|
|
||||||
const REOPEN_EVERY = 60000;
|
const REOPEN_EVERY = 60000;
|
||||||
|
|
||||||
@@ -55,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);
|
||||||
}
|
}
|
||||||
@@ -79,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);
|
||||||
}
|
}
|
||||||
@@ -93,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}`;
|
||||||
@@ -109,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);
|
||||||
}
|
}
|
||||||
@@ -135,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,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>";
|
||||||
@@ -229,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);
|
||||||
@@ -272,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);
|
||||||
@@ -287,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);
|
||||||
@@ -312,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);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,653 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ObjectID } from "mongodb";
|
||||||
|
import { map, parse, stringify } from "./common/expression-parser";
|
||||||
|
import { likePatternToRegExp, evaluate } from "./common/expression";
|
||||||
|
import { Expression, Fault, Task } from "./types";
|
||||||
|
|
||||||
|
const isArray = Array.isArray;
|
||||||
|
|
||||||
|
export function processDeviceFilter(filter): Expression {
|
||||||
|
return map(filter, exp => {
|
||||||
|
if (!isArray(exp)) return exp;
|
||||||
|
|
||||||
|
if (exp[0] === "PARAM") {
|
||||||
|
const p = exp[1];
|
||||||
|
if (p === "DeviceID.ID") return ["PARAM", "_id"];
|
||||||
|
else if (p === "DeviceID") return ["PARAM", "_deviceId"];
|
||||||
|
else if (p.startsWith("DeviceID."))
|
||||||
|
return ["PARAM", "_deviceId._" + p.slice(9)];
|
||||||
|
else if (p === "Events.Inform") return ["PARAM", "_lastInform"];
|
||||||
|
else if (p === "Events.Registered") return ["PARAM", "_registered"];
|
||||||
|
else if (p === "Events.0_BOOTSTRAP") return ["PARAM", "_lastBootstrap"];
|
||||||
|
else if (p === "Events.1_BOOT") return ["PARAM", "_lastBoot"];
|
||||||
|
} else if (
|
||||||
|
isArray(exp[1]) &&
|
||||||
|
exp[1][0] === "PARAM" &&
|
||||||
|
exp[1][1].startsWith("Tags.")
|
||||||
|
) {
|
||||||
|
const t = exp[1][1].slice(5);
|
||||||
|
if (exp[0] === "IS NULL") return ["<>", ["PARAM", "_tags"], t];
|
||||||
|
else if (exp[0] === "IS NOT NULL") return ["=", ["PARAM", "_tags"], t];
|
||||||
|
else if (exp[0] === "=" && exp[2] === true)
|
||||||
|
return ["=", ["PARAM", "_tags"], t];
|
||||||
|
else if (exp[0] === "<>" && exp[2] !== true)
|
||||||
|
return ["=", ["PARAM", "_tags"], t];
|
||||||
|
} else if (
|
||||||
|
["=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"].includes(exp[0])
|
||||||
|
) {
|
||||||
|
let e = map(exp, ee => {
|
||||||
|
if (
|
||||||
|
isArray(ee) &&
|
||||||
|
ee[0] === "PARAM" &&
|
||||||
|
typeof ee[1] === "string" &&
|
||||||
|
!ee[1].startsWith("_")
|
||||||
|
)
|
||||||
|
return ["PARAM", `${ee[1]}._value`];
|
||||||
|
return ee;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof e[2] === "number") {
|
||||||
|
const alt = (e as any[]).slice();
|
||||||
|
alt[2] = new Date(e[2]);
|
||||||
|
e = ["OR", e, alt];
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
return exp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processTasksFilter(filter): Expression {
|
||||||
|
return map(filter, exp => {
|
||||||
|
if (!isArray(exp)) return exp;
|
||||||
|
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
|
||||||
|
const e = exp.slice();
|
||||||
|
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")
|
||||||
|
e[2] = new Date(e[2]);
|
||||||
|
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
|
||||||
|
e[2] = new Date(e[2]);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
return exp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processFaultsFilter(filter): Expression {
|
||||||
|
return map(filter, exp => {
|
||||||
|
if (!isArray(exp)) return exp;
|
||||||
|
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
|
||||||
|
const e = exp.slice();
|
||||||
|
if (e[1][0] === "PARAM" && e[1][1] === "timestamp") e[2] = new Date(e[2]);
|
||||||
|
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
|
||||||
|
e[2] = new Date(e[2]);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
return exp;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterToMongoQuery(exp: Expression): {} {
|
||||||
|
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];
|
||||||
|
|
||||||
|
if (ops[op] === 0) {
|
||||||
|
for (let i = 1; i < filter.length; ++i) {
|
||||||
|
if (!isArray(filter[i]) || ops[filter[i][0]] == null)
|
||||||
|
throw new Error(`Invalid expression in ${op} clause`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!negate && op === "AND") || (negate && op === "OR")) {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _exp = evaluate(exp, null, Date.now());
|
||||||
|
|
||||||
|
if (!isArray(_exp)) {
|
||||||
|
if (_exp === true) return {};
|
||||||
|
throw new Error("Primitives are not valid queries");
|
||||||
|
}
|
||||||
|
|
||||||
|
return recursive(_exp, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processDeviceProjection(projection: {}): {} {
|
||||||
|
if (!projection) return projection;
|
||||||
|
const p = {};
|
||||||
|
for (const [k, v] of Object.entries(projection)) {
|
||||||
|
if (k === "DeviceID.ID") {
|
||||||
|
p["_id"] = 1;
|
||||||
|
} else if (k.startsWith("DeviceID")) {
|
||||||
|
p["_deviceId._SerialNumber"] = v;
|
||||||
|
p["_deviceId._OUI"] = v;
|
||||||
|
p["_deviceId._ProductClass"] = v;
|
||||||
|
p["_deviceId._Manufacturer"] = v;
|
||||||
|
} else if (k.startsWith("Tags")) {
|
||||||
|
p["_tags"] = v;
|
||||||
|
} else if (k.startsWith("Events")) {
|
||||||
|
p["_lastInform"] = v;
|
||||||
|
p["_registered"] = v;
|
||||||
|
p["_lastBoot"] = v;
|
||||||
|
p["_lastBootstrap"] = v;
|
||||||
|
} else {
|
||||||
|
p[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processDeviceSort(sort: {}): {} {
|
||||||
|
if (!sort) return sort;
|
||||||
|
const s = {};
|
||||||
|
for (const [k, v] of Object.entries(sort)) {
|
||||||
|
if (k === "DeviceID.ID") s["_id"] = v;
|
||||||
|
else if (k.startsWith("DeviceID.")) s[`_deviceId._${k.slice(9)}`] = v;
|
||||||
|
else if (k === "Events.Inform") s["_lastInform"] = v;
|
||||||
|
else if (k === "Events.Registered") s["_registered"] = v;
|
||||||
|
else if (k === "Events.1_BOOT") s["_lastBoot"] = v;
|
||||||
|
else if (k === "Events.0_BOOTSTRAP") s["_lastBootstrap"] = v;
|
||||||
|
else s[`${k}._value`] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDate(d): number | string {
|
||||||
|
const n = +d;
|
||||||
|
return isNaN(n) ? "" + d : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlatAttributes {
|
||||||
|
object?: boolean;
|
||||||
|
objectTimestamp?: number;
|
||||||
|
writable?: boolean;
|
||||||
|
writableTimestamp?: number;
|
||||||
|
value?: [string | number | boolean, string];
|
||||||
|
valueTimestamp?: number;
|
||||||
|
notification?: number;
|
||||||
|
notificationTimestamp?: number;
|
||||||
|
accessList?: string[];
|
||||||
|
accessListTimestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlatDevice {
|
||||||
|
[param: string]: FlatAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenDevice(device): FlatDevice {
|
||||||
|
function recursive(
|
||||||
|
input,
|
||||||
|
root: string,
|
||||||
|
output: FlatDevice,
|
||||||
|
timestamp: number
|
||||||
|
): void {
|
||||||
|
for (const [name, tree] of Object.entries(input)) {
|
||||||
|
if (!root) {
|
||||||
|
if (name === "_lastInform") {
|
||||||
|
output["Events.Inform"] = {
|
||||||
|
value: [parseDate(tree), "xsd:dateTime"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
} else if (name === "_registered") {
|
||||||
|
output["Events.Registered"] = {
|
||||||
|
value: [parseDate(tree), "xsd:dateTime"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
} else if (name === "_lastBoot") {
|
||||||
|
output["Events.1_BOOT"] = {
|
||||||
|
value: [parseDate(tree), "xsd:dateTime"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
} else if (name === "_lastBootstrap") {
|
||||||
|
output["Events.0_BOOTSTRAP"] = {
|
||||||
|
value: [parseDate(tree), "xsd:dateTime"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
} else if (name === "_id") {
|
||||||
|
output["DeviceID.ID"] = {
|
||||||
|
value: [tree as string, "xsd:string"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
} else if (name === "_deviceId") {
|
||||||
|
output["DeviceID.Manufacturer"] = {
|
||||||
|
value: [tree["_Manufacturer"], "xsd:string"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
output["DeviceID.OUI"] = {
|
||||||
|
value: [tree["_OUI"], "xsd:string"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
output["DeviceID.ProductClass"] = {
|
||||||
|
value: [tree["_ProductClass"], "xsd:string"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
output["DeviceID.SerialNumber"] = {
|
||||||
|
value: [tree["_SerialNumber"], "xsd:string"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
} else if (name === "_tags") {
|
||||||
|
output["Tags"] = {
|
||||||
|
writable: false,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: true,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const t of tree as string[]) {
|
||||||
|
output[`Tags.${t}`] = {
|
||||||
|
value: [true, "xsd:boolean"],
|
||||||
|
valueTimestamp: timestamp,
|
||||||
|
writable: true,
|
||||||
|
writableTimestamp: timestamp,
|
||||||
|
object: false,
|
||||||
|
objectTimestamp: timestamp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.startsWith("_")) continue;
|
||||||
|
|
||||||
|
let childrenTimestamp = timestamp;
|
||||||
|
|
||||||
|
if (!root) childrenTimestamp = +(input["_timestamp"] || 1);
|
||||||
|
else if (+input["_timestamp"] > timestamp)
|
||||||
|
childrenTimestamp = +input["_timestamp"];
|
||||||
|
|
||||||
|
const attrs: FlatAttributes = {};
|
||||||
|
if (tree["_value"] != null) {
|
||||||
|
attrs.value = [
|
||||||
|
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
|
||||||
|
tree["_type"]
|
||||||
|
];
|
||||||
|
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp);
|
||||||
|
attrs.object = false;
|
||||||
|
attrs.objectTimestamp = childrenTimestamp;
|
||||||
|
} else if (tree["_object"] != null) {
|
||||||
|
attrs.object = tree["_object"];
|
||||||
|
attrs.objectTimestamp = childrenTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree["_writable"] != null) {
|
||||||
|
attrs.writable = tree["_writable"];
|
||||||
|
attrs.writableTimestamp = childrenTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree["_notification"] != null) {
|
||||||
|
attrs.notification = tree["_notification"];
|
||||||
|
attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree["_accessList"] != null) {
|
||||||
|
attrs.accessList = tree["_accessList"];
|
||||||
|
attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = root ? `${root}.${name}` : name;
|
||||||
|
output[r] = attrs;
|
||||||
|
|
||||||
|
if (attrs.object || tree["object"] == null)
|
||||||
|
recursive(tree, r, output, childrenTimestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDevice: FlatDevice = {};
|
||||||
|
const timestamp = new Date(device["_lastInform"] || 1).getTime();
|
||||||
|
recursive(device, "", newDevice, timestamp);
|
||||||
|
return newDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenFault(fault): Fault {
|
||||||
|
const f = Object.assign({}, fault);
|
||||||
|
if (f.timestamp) f.timestamp = +f.timestamp;
|
||||||
|
if (f.expiry) f.expiry = +f.expiry;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenTask(task): Task {
|
||||||
|
const t = Object.assign({}, task);
|
||||||
|
t._id = "" + t._id;
|
||||||
|
if (t.timestamp) t.timestamp = +t.timestamp;
|
||||||
|
if (t.expiry) t.expiry = +t.expiry;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mongoQueryToFilter(query): Expression {
|
||||||
|
function recursive(_query): Expression {
|
||||||
|
const expressions: Expression[] = [];
|
||||||
|
for (const [k, v] of Object.entries(_query)) {
|
||||||
|
if (k[0] === "$") {
|
||||||
|
if (k === "$and") {
|
||||||
|
const and: Expression = ["AND"];
|
||||||
|
for (const vv of Object.values(v)) and.push(recursive(vv));
|
||||||
|
expressions.push(and);
|
||||||
|
} else if (k === "$or") {
|
||||||
|
const or: Expression = ["OR"];
|
||||||
|
for (const vv of Object.values(v)) or.push(recursive(vv));
|
||||||
|
expressions.push(or);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Operator ${k} not supported`);
|
||||||
|
}
|
||||||
|
} else if (k === "_tags") {
|
||||||
|
if (typeof v === "object") {
|
||||||
|
if (isArray(v)) throw new Error(`Invalid type`);
|
||||||
|
|
||||||
|
if (v.hasOwnProperty("$ne"))
|
||||||
|
expressions.push(["IS NULL", ["PARAM", `Tags.${v["$ne"]}`]]);
|
||||||
|
else if (v.hasOwnProperty("$eq"))
|
||||||
|
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v["$eq"]}`]]);
|
||||||
|
else throw new Error(`Invalid tag query`);
|
||||||
|
} else {
|
||||||
|
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]);
|
||||||
|
}
|
||||||
|
} else if (k.startsWith("Tags.")) {
|
||||||
|
let exists: boolean;
|
||||||
|
if (typeof v === "boolean") exists = v;
|
||||||
|
else if (v.hasOwnProperty("$eq")) exists = !!v["$eq"];
|
||||||
|
else if (v.hasOwnProperty("$ne")) exists = !v["$ne"];
|
||||||
|
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
|
||||||
|
else throw new Error(`Invalid tag query`);
|
||||||
|
|
||||||
|
expressions.push([exists ? "IS NOT NULL" : "IS NULL", ["PARAM", k]]);
|
||||||
|
} else if (typeof v === "object") {
|
||||||
|
if (isArray(v)) throw new Error(`Invalid type`);
|
||||||
|
|
||||||
|
const exps: Expression[] = [];
|
||||||
|
for (const [kk, vv] of Object.entries(v)) {
|
||||||
|
let op;
|
||||||
|
switch (kk) {
|
||||||
|
case "$eq":
|
||||||
|
op = "=";
|
||||||
|
break;
|
||||||
|
case "$ne":
|
||||||
|
op = "<>";
|
||||||
|
break;
|
||||||
|
case "$lt":
|
||||||
|
op = "<";
|
||||||
|
break;
|
||||||
|
case "$lte":
|
||||||
|
op = "<=";
|
||||||
|
break;
|
||||||
|
case "$gt":
|
||||||
|
op = ">";
|
||||||
|
break;
|
||||||
|
case "$gte":
|
||||||
|
op = ">=";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Operator ${kk} not supported`);
|
||||||
|
}
|
||||||
|
exps.push([op, ["PARAM", k], vv]);
|
||||||
|
}
|
||||||
|
if (exps.length === 1) {
|
||||||
|
expressions.push(exps[0]);
|
||||||
|
} else {
|
||||||
|
const and: Expression = ["AND"];
|
||||||
|
expressions.push(and.concat(exps));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expressions.push(["=", ["PARAM", k], v]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (expressions.length === 1) return expressions[0];
|
||||||
|
const and: Expression = ["AND"];
|
||||||
|
return and.concat(expressions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty filter
|
||||||
|
if (!Object.keys(query).length) return true;
|
||||||
|
|
||||||
|
return recursive(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenPreset(preset): {} {
|
||||||
|
const p = Object.assign({}, preset);
|
||||||
|
if (p.precondition) {
|
||||||
|
try {
|
||||||
|
// Try parse to check expression validity
|
||||||
|
parse(p.precondition);
|
||||||
|
} catch (error) {
|
||||||
|
p.precondition = mongoQueryToFilter(JSON.parse(p.precondition));
|
||||||
|
p.precondition = p.precondition.length ? stringify(p.precondition) : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.events) {
|
||||||
|
const e = [];
|
||||||
|
for (const [k, v] of Object.entries(p.events)) e.push(v ? k : `-${k}`);
|
||||||
|
p.events = e.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const provision = p.configurations[0];
|
||||||
|
if (
|
||||||
|
p.configurations.length === 1 &&
|
||||||
|
provision.type === "provision" &&
|
||||||
|
provision.name &&
|
||||||
|
provision.name.length
|
||||||
|
) {
|
||||||
|
p.provision = provision.name;
|
||||||
|
p.provisionArgs = provision.args
|
||||||
|
? JSON.stringify(provision.args).slice(1, -1)
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
delete p.configurations;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenFile(file): {} {
|
||||||
|
const f = {};
|
||||||
|
f["_id"] = file["_id"];
|
||||||
|
if (file.metadata) {
|
||||||
|
f["metadata.fileType"] = file.metadata.fileType || "";
|
||||||
|
f["metadata.oui"] = file.metadata.oui || "";
|
||||||
|
f["metadata.productClass"] = file.metadata.productClass || "";
|
||||||
|
f["metadata.version"] = file.metadata.version || "";
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preProcessPreset(data): {} {
|
||||||
|
const preset = Object.assign({}, data);
|
||||||
|
|
||||||
|
if (!preset.precondition) preset.precondition = "";
|
||||||
|
// Try parse to check expression validity
|
||||||
|
parse(preset.precondition);
|
||||||
|
|
||||||
|
preset.weight = parseInt(preset.weight) || 0;
|
||||||
|
|
||||||
|
const events = {};
|
||||||
|
if (preset.events) {
|
||||||
|
for (let e of preset.events.split(",")) {
|
||||||
|
let v = true;
|
||||||
|
e = e.trim();
|
||||||
|
if (e.startsWith("-")) {
|
||||||
|
v = false;
|
||||||
|
e = e.slice(1).trim();
|
||||||
|
}
|
||||||
|
if (e) events[e] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preset.events = events;
|
||||||
|
|
||||||
|
if (!preset.provision) throw new Error("Invalid preset provision");
|
||||||
|
|
||||||
|
const configuration = {
|
||||||
|
type: "provision",
|
||||||
|
name: preset.provision,
|
||||||
|
args: null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (preset.provisionArgs)
|
||||||
|
configuration.args = JSON.parse(`[${preset.provisionArgs}]`);
|
||||||
|
|
||||||
|
delete preset.provision;
|
||||||
|
delete preset.provisionArgs;
|
||||||
|
preset.configurations = [configuration];
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
+657
-617
File diff suppressed because it is too large.
Load diff
+60
-69
@@ -1,8 +1,26 @@
|
|||||||
import { platform } from "node:os";
|
/**
|
||||||
import { exec } from "node:child_process";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import { domainToASCII } from "node:url";
|
*
|
||||||
|
* 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 interface PingResult {
|
import { platform } from "os";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
|
||||||
|
interface Ping {
|
||||||
packetsTransmitted: number;
|
packetsTransmitted: number;
|
||||||
packetsReceived: number;
|
packetsReceived: number;
|
||||||
packetLoss: number;
|
packetLoss: number;
|
||||||
@@ -12,87 +30,60 @@ 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 parsePing(osPlatform: string, stdout: string): PingResult {
|
|
||||||
let parseRegExp1: RegExp, parseRegExp2: RegExp, parsed: PingResult;
|
|
||||||
switch (osPlatform) {
|
|
||||||
case "linux":
|
|
||||||
parseRegExp1 =
|
|
||||||
/(\d+) packets transmitted, (\d+) .*received, ([\d.]+)% .*loss[^]*= ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
|
|
||||||
parseRegExp2 =
|
|
||||||
/(\d+) packets transmitted, (\d+) .*received, ([\d.]+)% .*loss/;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "freebsd":
|
|
||||||
parseRegExp1 =
|
|
||||||
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
|
|
||||||
parseRegExp2 =
|
|
||||||
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const m1 = stdout.match(parseRegExp1);
|
|
||||||
if (m1) {
|
|
||||||
parsed = {
|
|
||||||
packetsTransmitted: +m1[1],
|
|
||||||
packetsReceived: +m1[2],
|
|
||||||
packetLoss: +m1[3],
|
|
||||||
min: +m1[4],
|
|
||||||
avg: +m1[5],
|
|
||||||
max: +m1[6],
|
|
||||||
mdev: +m1[7],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const m2 = stdout.match(parseRegExp2);
|
|
||||||
if (m2) {
|
|
||||||
parsed = {
|
|
||||||
packetsTransmitted: +m2[1],
|
|
||||||
packetsReceived: +m2[2],
|
|
||||||
packetLoss: +m2[3],
|
|
||||||
min: null,
|
|
||||||
avg: null,
|
|
||||||
max: null,
|
|
||||||
mdev: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
// Validate input to prevent possible remote code execution
|
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
|
||||||
// Credit to Alex Hordijk for reporting this vulnerability
|
|
||||||
if (!isValidHost(host)) return callback(new Error("Invalid host"));
|
|
||||||
host = host.replace("[", "").replace("]", "");
|
|
||||||
let cmd: string;
|
|
||||||
|
|
||||||
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 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
|
||||||
|
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 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
|
||||||
|
parseRegExp2 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return callback(new Error("Platform not supported"));
|
return callback(new Error("Platform not supported"));
|
||||||
}
|
}
|
||||||
|
|
||||||
exec(cmd, (err, stdout) => {
|
exec(cmd, (err, stdout) => {
|
||||||
if (err) return callback(err);
|
let parsed: Ping;
|
||||||
const parsed: PingResult = parsePing(platform(), stdout);
|
if (stdout) {
|
||||||
return callback(err, parsed, stdout);
|
const m1 = stdout.match(parseRegExp1);
|
||||||
|
if (m1) {
|
||||||
|
parsed = {
|
||||||
|
packetsTransmitted: +m1[1],
|
||||||
|
packetsReceived: +m1[2],
|
||||||
|
packetLoss: +m1[3],
|
||||||
|
min: +m1[4],
|
||||||
|
avg: +m1[5],
|
||||||
|
max: +m1[6],
|
||||||
|
mdev: +m1[7]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const m2 = stdout.match(parseRegExp2);
|
||||||
|
if (m2) {
|
||||||
|
parsed = {
|
||||||
|
packetsTransmitted: +m2[1],
|
||||||
|
packetsReceived: +m2[2],
|
||||||
|
packetLoss: +m2[3],
|
||||||
|
min: null,
|
||||||
|
avg: null,
|
||||||
|
max: null,
|
||||||
|
mdev: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err, parsed, stdout);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+119
-32
@@ -1,6 +1,23 @@
|
|||||||
function isObject(obj: any): boolean {
|
/**
|
||||||
return Object.prototype.toString.call(obj) === "[object Object]";
|
* 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 common from "./common";
|
||||||
|
|
||||||
function stringToRegexp(input, flags?): RegExp | false {
|
function stringToRegexp(input, flags?): RegExp | false {
|
||||||
if (input.indexOf("*") === -1) return false;
|
if (input.indexOf("*") === -1) return false;
|
||||||
@@ -17,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]) });
|
||||||
|
|
||||||
@@ -35,25 +52,14 @@ function normalize(input): any {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXPAND_OPS = new Set([
|
export function expandValue(value): any[] {
|
||||||
"$eq",
|
if (common.typeOf(value) === common.ARRAY_TYPE) {
|
||||||
"$gt",
|
|
||||||
"$gte",
|
|
||||||
"$in",
|
|
||||||
"$lt",
|
|
||||||
"$lte",
|
|
||||||
"$ne",
|
|
||||||
"$nin",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function expandValue(value: unknown): unknown[] {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +69,7 @@ function expandValue(value: unknown): unknown[] {
|
|||||||
const values = [];
|
const values = [];
|
||||||
for (const [k, v] of Object.entries(value)) {
|
for (const [k, v] of Object.entries(value)) {
|
||||||
keys.push(k);
|
keys.push(k);
|
||||||
if (EXPAND_OPS.has(k)) values.push(expandValue(v));
|
values.push(expandValue(v));
|
||||||
else values.push([v]);
|
|
||||||
indices.push(0);
|
indices.push(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,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) {
|
||||||
@@ -113,7 +116,7 @@ export function expand(
|
|||||||
if (v && (v["$ne"] != null || v["$not"] != null)) {
|
if (v && (v["$ne"] != null || v["$not"] != null)) {
|
||||||
if (Object.keys(v).length > 1)
|
if (Object.keys(v).length > 1)
|
||||||
throw new Error("Cannot mix $ne or $not with other operators");
|
throw new Error("Cannot mix $ne or $not with other operators");
|
||||||
for (const c of conditions) newQuery["$and"].push(c);
|
for (const c of conditions) newQuery["$an"].push(c);
|
||||||
} else {
|
} else {
|
||||||
newQuery["$and"].push({ $or: conditions });
|
newQuery["$and"].push({ $or: conditions });
|
||||||
}
|
}
|
||||||
@@ -126,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":
|
||||||
|
|||||||
+63
-55
@@ -1,11 +1,30 @@
|
|||||||
import * as vm from "node:vm";
|
/**
|
||||||
|
* 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 vm from "vm";
|
||||||
import seedrandom from "seedrandom";
|
import seedrandom from "seedrandom";
|
||||||
import * as device from "./device.ts";
|
import * as device from "./device";
|
||||||
import * as extensions from "./extensions.ts";
|
import * as extensions from "./extensions";
|
||||||
import * as logger from "./logger.ts";
|
import * as logger from "./logger";
|
||||||
import * as scheduling from "./scheduling.ts";
|
import * as scheduling from "./scheduling";
|
||||||
import Path from "./common/path.ts";
|
import Path from "./common/path";
|
||||||
import { Fault, SessionContext, ScriptResult } from "./types.ts";
|
import { Fault, SessionContext, ScriptResult } from "./types";
|
||||||
|
|
||||||
// Used for throwing to exit user script and commit
|
// Used for throwing to exit user script and commit
|
||||||
const COMMIT = Symbol();
|
const COMMIT = Symbol();
|
||||||
@@ -15,7 +34,7 @@ const EXT = Symbol();
|
|||||||
|
|
||||||
const UNDEFINED = undefined;
|
const UNDEFINED = undefined;
|
||||||
|
|
||||||
const context = vm.createContext(undefined, { microtaskMode: "afterEvaluate" });
|
const context = vm.createContext();
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
|
||||||
@@ -31,7 +50,7 @@ function runExtension(sessionContext, key, extCall): Promise<Fault> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let prom = re.get(key);
|
let prom = re.get(key);
|
||||||
if (prom == null) {
|
if (!prom) {
|
||||||
re.set(
|
re.set(
|
||||||
key,
|
key,
|
||||||
(prom = new Promise((resolve, reject) => {
|
(prom = new Promise((resolve, reject) => {
|
||||||
@@ -43,7 +62,7 @@ function runExtension(sessionContext, key, extCall): Promise<Fault> {
|
|||||||
resolve(fault);
|
resolve(fault);
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +78,7 @@ class SandboxDate {
|
|||||||
number?,
|
number?,
|
||||||
number?,
|
number?,
|
||||||
number?,
|
number?,
|
||||||
number?,
|
number?
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
if (argumentList.length) return new Date(...argumentList);
|
if (argumentList.length) return new Date(...argumentList);
|
||||||
@@ -90,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 {
|
||||||
@@ -108,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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,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) {
|
||||||
@@ -124,7 +133,7 @@ class ParameterWrapper {
|
|||||||
unpacked = device.unpack(
|
unpacked = device.unpack(
|
||||||
state.sessionContext.deviceData,
|
state.sessionContext.deviceData,
|
||||||
path,
|
path,
|
||||||
state.revision,
|
state.revision
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,18 +141,18 @@ class ParameterWrapper {
|
|||||||
|
|
||||||
const attr = state.sessionContext.deviceData.attributes.get(
|
const attr = state.sessionContext.deviceData.attributes.get(
|
||||||
unpacked[0],
|
unpacked[0],
|
||||||
state.revision,
|
state.revision
|
||||||
)[attrName];
|
)[attrName];
|
||||||
|
|
||||||
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) {
|
||||||
@@ -151,18 +160,18 @@ class ParameterWrapper {
|
|||||||
unpacked = device.unpack(
|
unpacked = device.unpack(
|
||||||
state.sessionContext.deviceData,
|
state.sessionContext.deviceData,
|
||||||
path,
|
path,
|
||||||
state.revision,
|
state.revision
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -170,17 +179,17 @@ class ParameterWrapper {
|
|||||||
unpacked = device.unpack(
|
unpacked = device.unpack(
|
||||||
state.sessionContext.deviceData,
|
state.sessionContext.deviceData,
|
||||||
path,
|
path,
|
||||||
state.revision,
|
state.revision
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -188,7 +197,7 @@ class ParameterWrapper {
|
|||||||
unpacked = device.unpack(
|
unpacked = device.unpack(
|
||||||
state.sessionContext.deviceData,
|
state.sessionContext.deviceData,
|
||||||
path,
|
path,
|
||||||
state.revision,
|
state.revision
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +210,7 @@ class ParameterWrapper {
|
|||||||
function declare(
|
function declare(
|
||||||
path: string,
|
path: string,
|
||||||
timestamps: { [attr: string]: number },
|
timestamps: { [attr: string]: number },
|
||||||
values: { [attr: string]: any },
|
values: { [attr: string]: any }
|
||||||
): ParameterWrapper {
|
): ParameterWrapper {
|
||||||
state.uncommitted = true;
|
state.uncommitted = true;
|
||||||
if (!timestamps) timestamps = {};
|
if (!timestamps) timestamps = {};
|
||||||
@@ -216,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();
|
||||||
@@ -236,7 +245,6 @@ function declare(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [attrName, attrTimestamp] of Object.entries(timestamps)) {
|
for (const [attrName, attrTimestamp] of Object.entries(timestamps)) {
|
||||||
if (!(attrTimestamp >= 1)) continue;
|
|
||||||
if (attrName === "path") {
|
if (attrName === "path") {
|
||||||
declaration.pathGet = attrTimestamp;
|
declaration.pathGet = attrTimestamp;
|
||||||
} else {
|
} else {
|
||||||
@@ -267,14 +275,14 @@ function commit(): void {
|
|||||||
throw COMMIT;
|
throw COMMIT;
|
||||||
} else if (state.revision > state.maxRevision + 1) {
|
} else if (state.revision > state.maxRevision + 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Declare function should not be called from within a try/catch block",
|
"Declare function should not be called from within a try/catch block"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -284,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"];
|
||||||
@@ -324,20 +332,20 @@ 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) {
|
||||||
fault.detail["stack"] = err.stack;
|
fault.detail["stack"] = err.stack;
|
||||||
// Trim the stack trace at the self-executing anonymous wrapper function
|
// Trim the stack trace at the self-executing anonymous wrapper function
|
||||||
const stackTrimIndex = fault.detail["stack"].match(
|
const stackTrimIndex = fault.detail["stack"].match(
|
||||||
/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/,
|
/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/
|
||||||
);
|
);
|
||||||
if (stackTrimIndex) {
|
if (stackTrimIndex) {
|
||||||
fault.detail["stack"] = fault.detail["stack"].slice(
|
fault.detail["stack"] = fault.detail["stack"].slice(
|
||||||
0,
|
0,
|
||||||
stackTrimIndex.index,
|
stackTrimIndex.index
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -347,11 +355,11 @@ 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,
|
||||||
extCounter = 0,
|
extCounter = 0
|
||||||
): Promise<ScriptResult> {
|
): Promise<ScriptResult> {
|
||||||
state = {
|
state = {
|
||||||
sessionContext: sessionContext,
|
sessionContext: sessionContext,
|
||||||
@@ -362,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];
|
||||||
@@ -372,7 +380,7 @@ export async function run(
|
|||||||
let ret, status;
|
let ret, status;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ret = script.runInContext(context, { displayErrors: false, timeout: 50 });
|
ret = script.runInContext(context, { displayErrors: false });
|
||||||
status = 0;
|
status = 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err === COMMIT) {
|
if (err === COMMIT) {
|
||||||
@@ -385,7 +393,7 @@ export async function run(
|
|||||||
clear: null,
|
clear: null,
|
||||||
declare: null,
|
declare: null,
|
||||||
done: false,
|
done: false,
|
||||||
returnValue: null,
|
returnValue: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,7 +404,7 @@ export async function run(
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.entries(_state.extensions).map(async ([k, v]) => {
|
Object.entries(_state.extensions).map(async ([k, v]) => {
|
||||||
fault = (await runExtension(_state.sessionContext, k, v)) || fault;
|
fault = (await runExtension(_state.sessionContext, k, v)) || fault;
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fault) {
|
if (fault) {
|
||||||
@@ -405,7 +413,7 @@ export async function run(
|
|||||||
clear: null,
|
clear: null,
|
||||||
declare: null,
|
declare: null,
|
||||||
done: false,
|
done: false,
|
||||||
returnValue: null,
|
returnValue: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +424,7 @@ export async function run(
|
|||||||
sessionContext,
|
sessionContext,
|
||||||
startRevision,
|
startRevision,
|
||||||
maxRevision,
|
maxRevision,
|
||||||
extCounter - _state.extCounter,
|
extCounter - _state.extCounter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+31
-17
@@ -1,8 +1,30 @@
|
|||||||
import * as crypto from "node:crypto";
|
/**
|
||||||
import * as later from "@breejs/later";
|
* 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 crypto from "crypto";
|
||||||
|
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) ^
|
||||||
@@ -11,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;
|
||||||
|
|||||||
+46
-129
@@ -1,14 +1,30 @@
|
|||||||
import { readFileSync } from "node:fs";
|
/**
|
||||||
import * as http from "node:http";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import * as https from "node:https";
|
*
|
||||||
import { Socket } from "node:net";
|
* This file is part of GenieACS.
|
||||||
import * as path from "node:path";
|
*
|
||||||
import { X509Certificate, createPrivateKey } from "node:crypto";
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
import { ROOT_DIR } from "./config.ts";
|
* 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 http from "http";
|
||||||
|
import * as https from "https";
|
||||||
|
import * as path from "path";
|
||||||
|
import { ROOT_DIR } from "./config";
|
||||||
|
|
||||||
let server: http.Server | https.Server;
|
let server: http.Server | https.Server;
|
||||||
let listener: http.RequestListener;
|
let listener: (...args) => void;
|
||||||
let stopping = false;
|
|
||||||
|
|
||||||
function closeServer(timeout, callback): void {
|
function closeServer(timeout, callback): void {
|
||||||
if (!server) return void callback();
|
if (!server) return void callback();
|
||||||
@@ -35,132 +51,38 @@ function closeServer(timeout, callback): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerOptions {
|
|
||||||
port?: number;
|
|
||||||
host?: string;
|
|
||||||
ssl?: { key: string; cert: string };
|
|
||||||
timeout?: number;
|
|
||||||
keepAliveTimeout?: number;
|
|
||||||
requestTimeout?: number;
|
|
||||||
onConnection?: (socket: Socket) => void;
|
|
||||||
onClientError?: (err: Error, socket: Socket) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SocketEndpoint {
|
|
||||||
localAddress: string;
|
|
||||||
localPort: number;
|
|
||||||
remoteAddress: string;
|
|
||||||
remotePort: number;
|
|
||||||
remoteFamily: "IPv4" | "IPv6";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save this info as they're not accessible after a socket has been closed
|
|
||||||
const socketEndpoints: WeakMap<Socket, SocketEndpoint> = new WeakMap();
|
|
||||||
|
|
||||||
type Promisify<T extends (...args: any) => any> = (
|
|
||||||
...args: Parameters<T>
|
|
||||||
) => Promise<ReturnType<T>>;
|
|
||||||
|
|
||||||
function getValidPrivKeys(value: string): Buffer[] {
|
|
||||||
return value.split(":").map((str) => {
|
|
||||||
str = str.trim();
|
|
||||||
const buf = str.startsWith("-----BEGIN ")
|
|
||||||
? Buffer.from(str)
|
|
||||||
: readFileSync(path.resolve(ROOT_DIR, str));
|
|
||||||
|
|
||||||
try {
|
|
||||||
createPrivateKey(buf);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Invalid private key: ${err.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getValidCerts(value: string): Buffer[] {
|
|
||||||
return value.split(":").map((str) => {
|
|
||||||
str = str.trim();
|
|
||||||
const buf = str.startsWith("-----BEGIN ")
|
|
||||||
? Buffer.from(str)
|
|
||||||
: readFileSync(path.resolve(ROOT_DIR, str));
|
|
||||||
|
|
||||||
try {
|
|
||||||
new X509Certificate(buf);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Invalid certificate: ${err.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function start(
|
export function start(
|
||||||
options: ServerOptions,
|
port,
|
||||||
_listener: Promisify<http.RequestListener>,
|
networkInterface,
|
||||||
|
ssl,
|
||||||
|
_listener,
|
||||||
|
onConnection?,
|
||||||
|
keepAliveTimeout: number = -1
|
||||||
): void {
|
): void {
|
||||||
listener = (req, res) => {
|
listener = _listener;
|
||||||
if (stopping) res.setHeader("Connection", "close");
|
|
||||||
_listener(req, res).catch((err) => {
|
|
||||||
try {
|
|
||||||
res.socket.unref();
|
|
||||||
if (res.headersSent) {
|
|
||||||
res.writeHead(500, { Connection: "close" });
|
|
||||||
res.end(`${err.name}: ${err.message}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (options.ssl) {
|
if (ssl && ssl.key && ssl.cert) {
|
||||||
const opts = {
|
const options = {
|
||||||
key: getValidPrivKeys(options.ssl.key),
|
key: ssl.key
|
||||||
cert: getValidCerts(options.ssl.cert),
|
.split(":")
|
||||||
|
.map(f => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
|
||||||
|
cert: ssl.cert
|
||||||
|
.split(":")
|
||||||
|
.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.on("connection", (socket: Socket) => {
|
if (keepAliveTimeout >= 0) server.keepAliveTimeout = keepAliveTimeout;
|
||||||
socketEndpoints.set(socket, {
|
server.listen(port, networkInterface);
|
||||||
localAddress: socket.localAddress,
|
|
||||||
localPort: socket.localPort,
|
|
||||||
remoteAddress: socket.remoteAddress,
|
|
||||||
remotePort: socket.remotePort,
|
|
||||||
remoteFamily: socket.remoteFamily as "IPv4" | "IPv6",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options.onClientError) {
|
|
||||||
server.on("clientError", (err, socket: Socket) => {
|
|
||||||
if (err["code"] !== "ECONNRESET" && socket.writable)
|
|
||||||
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
||||||
|
|
||||||
// As per Node docs: This event is guaranteed to be passed an instance
|
|
||||||
// of the <net.Socket> class
|
|
||||||
options.onClientError(err, socket as Socket);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
server.timeout = options.timeout || 0;
|
|
||||||
if (options.keepAliveTimeout != null)
|
|
||||||
server.keepAliveTimeout = options.keepAliveTimeout;
|
|
||||||
if (options.requestTimeout != null)
|
|
||||||
server.requestTimeout = options.requestTimeout;
|
|
||||||
|
|
||||||
server.listen({ port: options.port, host: options.host });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stop(terminateConnections = true): Promise<void> {
|
export function stop(): Promise<void> {
|
||||||
stopping = terminateConnections;
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reject(new Error("Could not close server in a timely manner"));
|
reject(new Error("Could not close server in a timely manner"));
|
||||||
@@ -168,8 +90,3 @@ export function stop(terminateConnections = true): Promise<void> {
|
|||||||
closeServer(20000, resolve);
|
closeServer(20000, resolve);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSocketEndpoints(socket: Socket): SocketEndpoint {
|
|
||||||
// TLSSocket keeps a reference to the raw TCP socket in _parent
|
|
||||||
return socketEndpoints.get(socket["_parent"] ?? socket);
|
|
||||||
}
|
|
||||||
+614
-827
File diff suppressed because it is too large.
Load diff
@@ -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)
|
||||||
|
};
|
||||||
|
}
|
||||||
+163
-407
@@ -1,36 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
import {
|
||||||
parseXml,
|
parseXml,
|
||||||
Element,
|
Element,
|
||||||
parseAttrs,
|
parseAttrs,
|
||||||
encodeEntities,
|
encodeEntities,
|
||||||
decodeEntities,
|
decodeEntities
|
||||||
} from "./xml-parser.ts";
|
} from "./xml-parser";
|
||||||
import memoize from "./common/memoize.ts";
|
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,
|
} from "./types";
|
||||||
type GetParameterNamesResponse,
|
|
||||||
type GetParameterValuesResponse,
|
|
||||||
type GetParameterAttributesResponse,
|
|
||||||
type SetParameterValuesResponse,
|
|
||||||
type SetParameterAttributesResponse,
|
|
||||||
type AddObjectResponse,
|
|
||||||
type DeleteObject,
|
|
||||||
type DeleteObjectResponse,
|
|
||||||
type RebootResponse,
|
|
||||||
type FactoryResetResponse,
|
|
||||||
type DownloadResponse,
|
|
||||||
GetRPCMethodsRequest,
|
|
||||||
RequestDownloadRequest,
|
|
||||||
AcsResponse,
|
|
||||||
} from "./types.ts";
|
|
||||||
import Path from "./common/path.ts";
|
|
||||||
|
|
||||||
const SERVER_NAME = `GenieACS/${VERSION}`;
|
const SERVER_NAME = `GenieACS/${VERSION}`;
|
||||||
|
|
||||||
@@ -40,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) {
|
||||||
@@ -106,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) {
|
||||||
@@ -156,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;
|
||||||
}
|
}
|
||||||
@@ -182,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;
|
||||||
}
|
}
|
||||||
@@ -192,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;
|
||||||
@@ -234,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 {
|
||||||
@@ -268,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")
|
||||||
),
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,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")
|
||||||
),
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,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();
|
||||||
@@ -330,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>${
|
||||||
@@ -375,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>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -384,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);
|
||||||
@@ -411,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"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,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>${methodRequest.delaySeconds ||
|
||||||
)}</TargetFileName><DelaySeconds>${
|
"0"}</DelaySeconds><SuccessURL>${encodeEntities(
|
||||||
methodRequest.delaySeconds || "0"
|
methodRequest.successUrl || ""
|
||||||
}</DelaySeconds><SuccessURL>${encodeEntities(
|
|
||||||
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":
|
||||||
@@ -522,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) {
|
||||||
@@ -584,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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,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" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,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":
|
||||||
@@ -655,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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,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":
|
||||||
@@ -740,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":
|
||||||
@@ -757,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":
|
||||||
@@ -792,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;
|
||||||
@@ -870,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;
|
||||||
@@ -878,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;
|
||||||
}
|
}
|
||||||
@@ -953,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;
|
||||||
@@ -975,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: "" };
|
||||||
@@ -1008,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) {
|
||||||
@@ -1047,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"';
|
||||||
@@ -1063,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>`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+99
-225
@@ -1,9 +1,27 @@
|
|||||||
import { IncomingMessage, ServerResponse } from "node:http";
|
/**
|
||||||
import { Script } from "node:vm";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import Path from "./common/path.ts";
|
*
|
||||||
import PathSet from "./common/path-set.ts";
|
* This file is part of GenieACS.
|
||||||
import VersionedMap from "./versioned-map.ts";
|
*
|
||||||
import InstanceSet from "./instance-set.ts";
|
* 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 Path from "./common/path";
|
||||||
|
import PathSet from "./common/path-set";
|
||||||
|
import VersionedMap from "./versioned-map";
|
||||||
|
import InstanceSet from "./instance-set";
|
||||||
|
import { IncomingMessage, ServerResponse } from "http";
|
||||||
|
|
||||||
export type Expression = string | number | boolean | null | any[];
|
export type Expression = string | number | boolean | null | any[];
|
||||||
|
|
||||||
@@ -78,7 +96,7 @@ export type VirtualParameterDeclaration = [
|
|||||||
value?: [string | number | boolean, string?];
|
value?: [string | number | boolean, string?];
|
||||||
notification?: number;
|
notification?: number;
|
||||||
accessList?: string[];
|
accessList?: string[];
|
||||||
}?,
|
}?
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface SyncState {
|
export interface SyncState {
|
||||||
@@ -120,7 +138,7 @@ export interface SessionContext {
|
|||||||
AttributeTimestamps,
|
AttributeTimestamps,
|
||||||
AttributeValues,
|
AttributeValues,
|
||||||
AttributeTimestamps,
|
AttributeTimestamps,
|
||||||
AttributeValues,
|
AttributeValues
|
||||||
][][];
|
][][];
|
||||||
revisions: number[];
|
revisions: number[];
|
||||||
rpcCount: number;
|
rpcCount: number;
|
||||||
@@ -131,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 };
|
||||||
@@ -138,12 +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;
|
||||||
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[];
|
||||||
@@ -151,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?][];
|
||||||
@@ -160,7 +178,7 @@ export interface Task {
|
|||||||
fileName?: string;
|
fileName?: string;
|
||||||
targetFileName?: string;
|
targetFileName?: string;
|
||||||
expiry?: number;
|
expiry?: number;
|
||||||
provisions?: [string, ...Expression[]][];
|
provisions?: (string | number | boolean)[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Operation {
|
export interface Operation {
|
||||||
@@ -177,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 {
|
||||||
@@ -270,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;
|
||||||
@@ -353,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;
|
||||||
@@ -364,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 {
|
||||||
@@ -408,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;
|
||||||
@@ -436,7 +354,7 @@ export type Clear = [
|
|||||||
notification?: number;
|
notification?: number;
|
||||||
accessList?: number;
|
accessList?: number;
|
||||||
}?,
|
}?,
|
||||||
number?,
|
number?
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface Preset {
|
export interface Preset {
|
||||||
@@ -444,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 = {
|
||||||
@@ -484,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;
|
||||||
@@ -506,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 {
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
import { constants } from "node:zlib";
|
/**
|
||||||
|
* 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 { 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";
|
||||||
@@ -6,17 +25,15 @@ import koaStatic from "koa-static";
|
|||||||
import koaCompress from "koa-compress";
|
import koaCompress from "koa-compress";
|
||||||
import koaBodyParser from "koa-bodyparser";
|
import koaBodyParser from "koa-bodyparser";
|
||||||
import koaJwt from "koa-jwt";
|
import koaJwt from "koa-jwt";
|
||||||
import * as config from "./config.ts";
|
import * as config from "./config";
|
||||||
import api from "./ui/api.ts";
|
import api from "./ui/api";
|
||||||
import Authorizer from "./common/authorizer.ts";
|
import Authorizer from "./common/authorizer";
|
||||||
import * as logger from "./logger.ts";
|
import * as logger from "./logger";
|
||||||
import * as localCache from "./ui/local-cache.ts";
|
import * as localCache from "./local-cache";
|
||||||
import { PermissionSet } from "./types.ts";
|
import { PermissionSet } from "./types";
|
||||||
import { authLocal } from "./api-functions.ts";
|
import { authLocal } from "./ui/api-functions";
|
||||||
import * as init from "./init.ts";
|
import * as init from "./init";
|
||||||
import { version as VERSION } from "../package.json";
|
import { version as VERSION } from "../package.json";
|
||||||
import memoize from "./common/memoize.ts";
|
|
||||||
import { APP_JS, APP_CSS, FAVICON_PNG } from "../build/assets.ts";
|
|
||||||
|
|
||||||
declare module "koa" {
|
declare module "koa" {
|
||||||
interface Request {
|
interface Request {
|
||||||
@@ -30,31 +47,12 @@ 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", (err, ctx) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// Ignored errors resulting from aborted requests
|
|
||||||
if (ctx?.req.aborted) return;
|
|
||||||
|
|
||||||
// Ignore client errors (e.g. malicious path)
|
|
||||||
if (err.status === 400) return;
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
koa.use(async (ctx, next) => {
|
koa.use(async (ctx, next) => {
|
||||||
const configSnapshot = await localCache.getRevision();
|
const configSnapshot = await localCache.getCurrentSnapshot();
|
||||||
ctx.state.configSnapshot = configSnapshot;
|
ctx.state.configSnapshot = configSnapshot;
|
||||||
ctx.set("X-Config-Snapshot", configSnapshot);
|
ctx.set("X-Config-Snapshot", configSnapshot);
|
||||||
ctx.set("GenieACS-Version", VERSION);
|
ctx.set("GenieACS-Version", VERSION);
|
||||||
@@ -74,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)[
|
||||||
@@ -90,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";
|
||||||
@@ -116,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 {
|
||||||
@@ -140,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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,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;
|
||||||
@@ -185,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;
|
||||||
@@ -206,39 +205,34 @@ router.post("/init", async (ctx) => {
|
|||||||
ctx.body = "";
|
ctx.body = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/", async (ctx) => {
|
router.get("/", async ctx => {
|
||||||
// koa-router seems to tolerate double slashes in the URL but that can
|
const permissionSets = ctx.state.authorizer.getPermissionSets();
|
||||||
// be problematic when using relatives asset paths in HTML
|
|
||||||
if (ctx.path.endsWith("//")) return;
|
|
||||||
|
|
||||||
const permissionSets: PermissionSet[] =
|
|
||||||
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)
|
||||||
wizard = '<script>window.location.hash = "#!/wizard";</script>';
|
wizard = '<script>window.location.hash = "#!/wizard";</script>';
|
||||||
|
|
||||||
ctx.body = `<!DOCTYPE html>
|
ctx.body = `
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>GenieACS</title>
|
<title>GenieACS</title>
|
||||||
<link rel="shortcut icon" type="image/png" href="${FAVICON_PNG}" />
|
<link rel="shortcut icon" type="image/png" href="favicon.png" />
|
||||||
<link rel="stylesheet" href="${APP_CSS}">
|
<link rel="stylesheet" href="app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<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)};
|
||||||
window.username = ${JSON.stringify(
|
window.username = ${JSON.stringify(
|
||||||
ctx.state.user ? ctx.state.user.username : "",
|
ctx.state.user ? ctx.state.user.username : ""
|
||||||
)};
|
)};
|
||||||
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>
|
||||||
`;
|
`;
|
||||||
@@ -246,19 +240,8 @@ 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: {
|
|
||||||
flush: constants.BROTLI_OPERATION_FLUSH,
|
|
||||||
params: {
|
|
||||||
[constants.BROTLI_PARAM_QUALITY]: 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
koa.use(router.routes());
|
koa.use(router.routes());
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ObjectID } from "mongodb";
|
||||||
|
import * as db from "./db";
|
||||||
|
import { del } from "../cache";
|
||||||
|
import { getUsers } from "../local-cache";
|
||||||
|
import { hashPassword } from "../auth";
|
||||||
|
import {
|
||||||
|
insertTasks,
|
||||||
|
watchTask,
|
||||||
|
connectionRequest,
|
||||||
|
deleteDevice
|
||||||
|
} from "../api-functions";
|
||||||
|
|
||||||
|
async function deleteFault(id): Promise<void> {
|
||||||
|
const deviceId = id.split(":", 1)[0];
|
||||||
|
const channel = id.slice(deviceId.length + 1);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
db.deleteFault(id),
|
||||||
|
channel.startsWith("task_")
|
||||||
|
? db.deleteTask(new ObjectID(channel.slice(5)))
|
||||||
|
: null
|
||||||
|
]);
|
||||||
|
|
||||||
|
await del(`${deviceId}_tasks_faults_operations`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteResource(resource, id): Promise<void> {
|
||||||
|
switch (resource) {
|
||||||
|
case "devices":
|
||||||
|
await deleteDevice(id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "files":
|
||||||
|
await db.deleteFile(id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "faults":
|
||||||
|
await deleteFault(id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "provisions":
|
||||||
|
await db.deleteProvision(id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "presets":
|
||||||
|
await db.deletePreset(id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "virtualParameters":
|
||||||
|
await db.deleteVirtualParameter(id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "config":
|
||||||
|
await db.deleteConfig(id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "permissions":
|
||||||
|
await db.deletePermission(id);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "users":
|
||||||
|
await db.deleteUser(id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await del("presets_hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postTasks(
|
||||||
|
deviceId,
|
||||||
|
tasks,
|
||||||
|
timeout
|
||||||
|
): Promise<{ connectionRequest: string; tasks: any[] }> {
|
||||||
|
for (const task of tasks) {
|
||||||
|
delete task._id;
|
||||||
|
task.device = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks = await insertTasks(tasks);
|
||||||
|
const statuses = tasks.map(t => {
|
||||||
|
return { _id: t._id, status: "pending" };
|
||||||
|
});
|
||||||
|
|
||||||
|
await del(`${deviceId}_tasks_faults_operations`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connectionRequest(deviceId);
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
connectionRequest: err.message,
|
||||||
|
tasks: statuses
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sample = tasks[tasks.length - 1];
|
||||||
|
|
||||||
|
// Waiting for session to finish or timeout
|
||||||
|
await watchTask(deviceId, sample._id, timeout);
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
for (const s of statuses) {
|
||||||
|
promises.push(db.query("tasks", ["=", ["PARAM", "_id"], s._id]));
|
||||||
|
promises.push(
|
||||||
|
db.query("faults", ["=", ["PARAM", "_id"], `${deviceId}:task_${s._id}`])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await Promise.all(promises);
|
||||||
|
for (const [i, r] of statuses.entries()) {
|
||||||
|
if (res[i * 2].length === 0) {
|
||||||
|
r.status = "done";
|
||||||
|
} else if (res[i * 2 + 1].length === 1) {
|
||||||
|
r.status = "fault";
|
||||||
|
r.fault = res[i * 2 + 1][0];
|
||||||
|
}
|
||||||
|
db.deleteTask(r._id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { connectionRequest: "OK", tasks: statuses };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PingResponse {
|
||||||
|
packetsTransmitted: number;
|
||||||
|
packetsReceived: number;
|
||||||
|
packetLoss: string;
|
||||||
|
min: number;
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
mdev: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putResource(resource, id, data): Promise<void> {
|
||||||
|
if (resource === "presets") {
|
||||||
|
await db.putPreset(id, data);
|
||||||
|
} else if (resource === "provisions") {
|
||||||
|
await db.putProvision(id, data);
|
||||||
|
} else if (resource === "virtualParameters") {
|
||||||
|
await db.putVirtualParameter(id, data);
|
||||||
|
} else if (resource === "config") {
|
||||||
|
await db.putConfig(id, data);
|
||||||
|
} else if (resource === "permissions") {
|
||||||
|
await db.putPermission(id, data);
|
||||||
|
} else if (resource === "users") {
|
||||||
|
delete data.password;
|
||||||
|
delete data.salt;
|
||||||
|
await db.putUser(id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
await del("presets_hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authLocal(snapshot, username, password): Promise<boolean> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const users = getUsers(snapshot);
|
||||||
|
const user = users[username];
|
||||||
|
if (!user || !user.password) return void resolve(null);
|
||||||
|
hashPassword(password, user.salt)
|
||||||
|
.then(hash => {
|
||||||
|
if (hash === user.password) resolve(true);
|
||||||
|
else resolve(false);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
+194
-337
@@ -1,22 +1,35 @@
|
|||||||
import { PassThrough } from "node:stream";
|
/**
|
||||||
|
* 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 stream from "stream";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import { ObjectId } from "mongodb";
|
import * as db from "./db";
|
||||||
import * as db from "./db.ts";
|
import * as apiFunctions from "./api-functions";
|
||||||
import * as apiFunctions from "../api-functions.ts";
|
import { evaluate, and, extractParams } from "../common/expression";
|
||||||
import { evaluate, and, extractParams } from "../common/expression/util.ts";
|
import { parse } from "../common/expression-parser";
|
||||||
import { parse } from "../common/expression/parser.ts";
|
import * as logger from "../logger";
|
||||||
import * as logger from "../logger.ts";
|
import { getConfig } from "../local-cache";
|
||||||
import { getConfig } from "../ui/local-cache.ts";
|
import { QueryOptions, Expression } from "../types";
|
||||||
import { Expression, Task } from "../types.ts";
|
import { generateSalt, hashPassword } from "../auth";
|
||||||
import { generateSalt, hashPassword } from "../auth.ts";
|
import { del } from "../cache";
|
||||||
import { del } from "../cache.ts";
|
import Authorizer from "../common/authorizer";
|
||||||
import Authorizer from "../common/authorizer.ts";
|
import { ping } from "../ping";
|
||||||
import { ping } from "../ping.ts";
|
|
||||||
import { decodeTag } from "../util.ts";
|
|
||||||
import { stringify as yamlStringify } from "../common/yaml.ts";
|
|
||||||
import { ResourceLockedError } from "../common/errors.ts";
|
|
||||||
import { acquireLock, releaseLock } from "../lock.ts";
|
|
||||||
import { collections } from "../db/db.ts";
|
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
export default router;
|
export default router;
|
||||||
@@ -39,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 = {
|
||||||
@@ -52,103 +65,89 @@ 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 { value: device } = await db.query("devices", filter).next();
|
const res = await db.query("devices", filter);
|
||||||
if (!device) return void (ctx.status = 404);
|
if (!res[0]) return void (ctx.status = 404);
|
||||||
|
|
||||||
ctx.type = "text/csv";
|
ctx.type = "text/csv";
|
||||||
ctx.attachment(
|
ctx.attachment(
|
||||||
`device-${ctx.params.id}-${new Date()
|
`device-${ctx.params.id}-${new Date()
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.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(device).sort()) {
|
for (const k of Object.keys(res[0]).sort()) {
|
||||||
const p = device[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,
|
||||||
`"${String(value).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"]],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,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: Parameters<typeof db.query>[2] = {};
|
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 }), {});
|
||||||
}
|
}
|
||||||
@@ -184,46 +183,46 @@ 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");
|
||||||
for await (const obj of db.query(resource, filter, options))
|
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("]");
|
||||||
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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: Parameters<typeof db.query>[2] = { 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)`,
|
||||||
@@ -231,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)) {
|
||||||
@@ -254,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"
|
||||||
);
|
);
|
||||||
for await (const obj of db.query(resource, filter, options)) {
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,102 +295,92 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
return v;
|
return v;
|
||||||
});
|
});
|
||||||
ctx.body.write(arr.join(",") + "\n");
|
ctx.body.write(arr.join(",") + "\n");
|
||||||
}
|
});
|
||||||
ctx.body.end();
|
ctx.body.end();
|
||||||
|
|
||||||
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 count = await db.count(resource, filter);
|
const res = await db.query(resource, filter);
|
||||||
|
|
||||||
if (!count) 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 { value: res } = await db.query(resource, filter).next();
|
const res = await db.query(resource, filter);
|
||||||
|
|
||||||
if (!res) return void (ctx.status = 404);
|
if (!res.length) return void next();
|
||||||
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
ctx.body = res;
|
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 { value: res } = await db.query(resource, filter).next();
|
const res = await db.query(resource, filter);
|
||||||
if (!res) return void (ctx.status = 404);
|
if (!res.length) return void next();
|
||||||
|
|
||||||
const validate = authorizer.getValidator(resource, res);
|
const validate = authorizer.getValidator(resource, res[0]);
|
||||||
if (!validate("delete")) {
|
if (!validate("delete")) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void (ctx.status = 403);
|
return void (ctx.status = 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await apiFunctions.deleteResource(resource, ctx.params.id);
|
||||||
await apiFunctions.deleteResource(resource, ctx.params.id);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof ResourceLockedError) {
|
|
||||||
log.message += " failed";
|
|
||||||
logger.accessWarn(log);
|
|
||||||
ctx.status = 503;
|
|
||||||
ctx.body = err.message;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
|
|
||||||
@@ -415,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;
|
||||||
@@ -454,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;
|
||||||
@@ -493,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);
|
||||||
@@ -515,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);
|
||||||
@@ -528,156 +465,77 @@ 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 deviceId = ctx.params.id;
|
|
||||||
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: deviceId,
|
|
||||||
tasks: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!authorizer.hasAccess("devices", 3)) {
|
|
||||||
logUnauthorizedWarning(log);
|
|
||||||
return void (ctx.status = 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
const socketTimeout: number = ctx.socket.timeout;
|
|
||||||
|
|
||||||
// Extend socket timeout while waiting for session
|
|
||||||
if (socketTimeout) ctx.socket.setTimeout(300000);
|
|
||||||
|
|
||||||
const token = await acquireLock(`cwmp_session_${deviceId}`, 5000, 30000);
|
|
||||||
if (!token) {
|
|
||||||
log.message += " failed";
|
|
||||||
logger.accessWarn(log);
|
|
||||||
ctx.body = "Device is in session";
|
|
||||||
ctx.status = 503;
|
|
||||||
// Restore socket timeout
|
|
||||||
if (socketTimeout) ctx.socket.setTimeout(socketTimeout);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let device;
|
|
||||||
|
|
||||||
let statuses: { _id: string; status: string }[];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filter = and(authorizer.getFilter("devices", 3), [
|
|
||||||
"=",
|
|
||||||
["PARAM", "DeviceID.ID"],
|
|
||||||
deviceId,
|
|
||||||
]);
|
|
||||||
device = (await db.query("devices", filter).next()).value;
|
|
||||||
if (!device) return void (ctx.status = 404);
|
|
||||||
|
|
||||||
const validate = authorizer.getValidator("devices", device);
|
|
||||||
for (const t of ctx.request.body) {
|
|
||||||
if (!validate("task", t)) {
|
|
||||||
logUnauthorizedWarning(log);
|
|
||||||
return void (ctx.status = 403);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tasks = ctx.request.body as Task[];
|
|
||||||
|
|
||||||
for (const task of tasks) {
|
|
||||||
delete task._id;
|
|
||||||
task["device"] = deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks = await apiFunctions.insertTasks(tasks);
|
|
||||||
|
|
||||||
statuses = tasks.map((t) => ({ _id: t._id, status: "pending" }));
|
|
||||||
} finally {
|
|
||||||
await releaseLock(`cwmp_session_${deviceId}`, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onlineThreshold = getConfig(
|
|
||||||
ctx.state.configSnapshot,
|
|
||||||
"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 lastInform = device["Events.Inform"].value[0] as number;
|
|
||||||
|
|
||||||
let status = await apiFunctions.connectionRequest(deviceId, device);
|
|
||||||
if (!status) {
|
|
||||||
const sessionStarted = await apiFunctions.awaitSessionStart(
|
|
||||||
deviceId,
|
|
||||||
lastInform,
|
|
||||||
onlineThreshold,
|
|
||||||
);
|
|
||||||
if (!sessionStarted) {
|
|
||||||
status = "No contact from CPE";
|
|
||||||
} else {
|
|
||||||
const sessionEnded = await apiFunctions.awaitSessionEnd(deviceId, 120000);
|
|
||||||
if (!sessionEnded) status = "Session took too long to complete";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!status) {
|
|
||||||
const promises = statuses.map((t) =>
|
|
||||||
collections.faults.count({ _id: `${deviceId}:task_${t._id}` }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = await Promise.all(promises);
|
|
||||||
for (const [i, r] of statuses.entries())
|
|
||||||
r.status = res[i] ? "fault" : "done";
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
|
||||||
|
|
||||||
// Restore socket timeout
|
|
||||||
if (socketTimeout) ctx.socket.setTimeout(socketTimeout);
|
|
||||||
|
|
||||||
log.tasks = statuses.map((t) => t._id).join(",");
|
|
||||||
logger.accessInfo(log);
|
|
||||||
|
|
||||||
ctx.set("Connection-Request", status || "OK");
|
|
||||||
ctx.body = statuses;
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/devices/:id/tags", async (ctx) => {
|
|
||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
|
||||||
const log = {
|
|
||||||
message: "Update tags",
|
|
||||||
context: ctx,
|
|
||||||
deviceId: ctx.params.id,
|
deviceId: ctx.params.id,
|
||||||
tags: ctx.request.body,
|
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 { value: res } = await db.query("devices", filter).next();
|
const devices = await db.query("devices", filter);
|
||||||
if (!res) return void (ctx.status = 404);
|
if (!devices.length) return void next();
|
||||||
|
|
||||||
const validate = authorizer.getValidator("devices", res);
|
const validate = authorizer.getValidator("devices", devices[0]);
|
||||||
|
for (const t of ctx.request.body) {
|
||||||
|
if (!validate("task", t)) {
|
||||||
|
logUnauthorizedWarning(log);
|
||||||
|
return void (ctx.status = 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Pass context and timestamp
|
||||||
|
const onlineThreshold = getConfig(
|
||||||
|
ctx.state.configSnapshot,
|
||||||
|
"cwmp.deviceOnlineThreshold"
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await apiFunctions.postTasks(
|
||||||
|
ctx.params.id,
|
||||||
|
ctx.request.body,
|
||||||
|
onlineThreshold
|
||||||
|
);
|
||||||
|
|
||||||
|
log.tasks = res.tasks.map(t => t._id).join(",");
|
||||||
|
|
||||||
|
logger.accessInfo(log);
|
||||||
|
|
||||||
|
ctx.set("Connection-Request", res.connectionRequest);
|
||||||
|
ctx.body = res.tasks;
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/devices/:id/tags", async (ctx, next) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
|
const log = {
|
||||||
|
message: "Update tags",
|
||||||
|
context: ctx,
|
||||||
|
deviceId: ctx.params.id,
|
||||||
|
tags: ctx.request.body
|
||||||
|
};
|
||||||
|
|
||||||
|
const filter = and(authorizer.getFilter("devices", 3), [
|
||||||
|
"=",
|
||||||
|
["PARAM", "DeviceID.ID"],
|
||||||
|
ctx.params.id
|
||||||
|
]);
|
||||||
|
if (!authorizer.hasAccess("devices", 3)) {
|
||||||
|
logUnauthorizedWarning(log);
|
||||||
|
return void next();
|
||||||
|
}
|
||||||
|
const res = await db.query("devices", filter);
|
||||||
|
if (!res.length) return void next();
|
||||||
|
|
||||||
|
const validate = authorizer.getValidator("devices", res[0]);
|
||||||
if (!validate("tags", ctx.request.body)) {
|
if (!validate("tags", ctx.request.body)) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void (ctx.status = 403);
|
return void (ctx.status = 403);
|
||||||
@@ -697,28 +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 => {
|
||||||
if (!ctx.state.user) return void (ctx.status = 401);
|
return new Promise(resolve => {
|
||||||
return new Promise<void>((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) {
|
||||||
@@ -727,7 +584,7 @@ router.put("/users/:id/password", async (ctx) => {
|
|||||||
!(await apiFunctions.authLocal(
|
!(await apiFunctions.authLocal(
|
||||||
ctx.state.configSnapshot,
|
ctx.state.configSnapshot,
|
||||||
username,
|
username,
|
||||||
ctx.request.body.authPassword,
|
ctx.request.body.authPassword
|
||||||
))
|
))
|
||||||
) {
|
) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
@@ -737,20 +594,20 @@ 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 newPassword = ctx.request.body.newPassword;
|
const filter = and(authorizer.getFilter("users", 3), [
|
||||||
|
"=",
|
||||||
|
["PARAM", RESOURCE_IDS.users],
|
||||||
|
username
|
||||||
|
]);
|
||||||
|
const res = await db.query("users", filter);
|
||||||
|
if (!res.length) return void next();
|
||||||
|
|
||||||
|
const newPassword = ctx.request.body.newPassword;
|
||||||
if (ctx.state.user) {
|
if (ctx.state.user) {
|
||||||
const filter = and(authorizer.getFilter("users", 3), [
|
const validate = authorizer.getValidator("users", res[0]);
|
||||||
"=",
|
|
||||||
["PARAM", RESOURCE_IDS.users],
|
|
||||||
username,
|
|
||||||
]);
|
|
||||||
const { value: res } = await db.query("users", filter).next();
|
|
||||||
if (!res) return void (ctx.status = 404);
|
|
||||||
const validate = authorizer.getValidator("users", res);
|
|
||||||
if (!validate("password", { password: newPassword })) {
|
if (!validate("password", { password: newPassword })) {
|
||||||
logUnauthorizedWarning(log);
|
logUnauthorizedWarning(log);
|
||||||
return void (ctx.status = 403);
|
return void (ctx.status = 403);
|
||||||
@@ -761,7 +618,7 @@ router.put("/users/:id/password", async (ctx) => {
|
|||||||
const password = await hashPassword(newPassword, salt);
|
const password = await hashPassword(newPassword, salt);
|
||||||
await db.putUser(username, { password, salt });
|
await db.putUser(username, { password, salt });
|
||||||
|
|
||||||
await del("ui-local-cache-hash");
|
await del("presets_hash");
|
||||||
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
ctx.body = "";
|
ctx.body = "";
|
||||||
|
|||||||
+391
-574
@@ -1,593 +1,410 @@
|
|||||||
import { Script } from "node:vm";
|
/**
|
||||||
import { Readable } from "node:stream";
|
* Copyright 2013-2019 GenieACS Inc.
|
||||||
import { Collection, ObjectId, WithoutId } from "mongodb";
|
*
|
||||||
import { encodeTag } from "../util.ts";
|
* This file is part of GenieACS.
|
||||||
import { evaluate } from "../common/expression/util.ts";
|
*
|
||||||
import { Expression, Fault, Task } from "../types.ts";
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
import { collections, filesBucket } from "../db/db.ts";
|
* it under the terms of the GNU Affero General Public License as
|
||||||
import { convertOldPrecondition, optimizeProjection } from "../db/util.ts";
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
import * as MongoTypes from "../db/types.ts";
|
* License, or (at your option) any later version.
|
||||||
import { parse, parseList, stringify } from "../common/expression/parser.ts";
|
*
|
||||||
import { toMongoQuery } from "../db/synth.ts";
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
function processDeviceProjection(
|
import { MongoClient, GridFSBucket } from "mongodb";
|
||||||
projection: Record<string, 1>,
|
import { Script } from "vm";
|
||||||
): Record<string, 1> {
|
import * as config from "../config";
|
||||||
if (!projection) return projection;
|
import * as mongodbFunctions from "../mongodb-functions";
|
||||||
const p = {};
|
import * as expression from "../common/expression";
|
||||||
for (const [k, v] of Object.entries(projection)) {
|
import { QueryOptions } from "../types";
|
||||||
if (k === "DeviceID.ID") {
|
|
||||||
p["_id"] = 1;
|
|
||||||
} else if (k.startsWith("DeviceID")) {
|
|
||||||
p["_deviceId._SerialNumber"] = v;
|
|
||||||
p["_deviceId._OUI"] = v;
|
|
||||||
p["_deviceId._ProductClass"] = v;
|
|
||||||
p["_deviceId._Manufacturer"] = v;
|
|
||||||
} else if (k.startsWith("Tags")) {
|
|
||||||
p["_tags"] = v;
|
|
||||||
} else if (k.startsWith("Events")) {
|
|
||||||
p["_lastInform"] = v;
|
|
||||||
p["_registered"] = v;
|
|
||||||
p["_lastBoot"] = v;
|
|
||||||
p["_lastBootstrap"] = v;
|
|
||||||
} else {
|
|
||||||
p[k] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p;
|
const CACHE_TTL = 300000;
|
||||||
|
|
||||||
|
let clientPromise: Promise<MongoClient>;
|
||||||
|
|
||||||
|
const RESOURCE_COLLECTION = {
|
||||||
|
files: "fs.files"
|
||||||
|
};
|
||||||
|
|
||||||
|
function ensureIndexes(client): void {
|
||||||
|
client
|
||||||
|
.db()
|
||||||
|
.collection("cache")
|
||||||
|
.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function processDeviceSort(
|
function getClient(): Promise<MongoClient> {
|
||||||
sort: Record<string, number>,
|
if (!clientPromise) {
|
||||||
): Record<string, number> {
|
clientPromise = new Promise((resolve, reject) => {
|
||||||
if (!sort) return sort;
|
const CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
|
||||||
const s = {};
|
MongoClient.connect(
|
||||||
for (const [k, v] of Object.entries(sort)) {
|
CONNECTION_URL,
|
||||||
if (k === "DeviceID.ID") s["_id"] = v;
|
{ useNewUrlParser: true },
|
||||||
else if (k.startsWith("DeviceID.")) s[`_deviceId._${k.slice(9)}`] = v;
|
(err, client) => {
|
||||||
else if (k === "Events.Inform") s["_lastInform"] = v;
|
if (err) return void reject(err);
|
||||||
else if (k === "Events.Registered") s["_registered"] = v;
|
ensureIndexes(client);
|
||||||
else if (k === "Events.1_BOOT") s["_lastBoot"] = v;
|
resolve(client);
|
||||||
else if (k === "Events.0_BOOTSTRAP") s["_lastBootstrap"] = v;
|
|
||||||
else s[`${k}._value`] = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseDate(d: Date): number | string {
|
|
||||||
const n = +d;
|
|
||||||
return isNaN(n) ? "" + d : n;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FlatAttributes {
|
|
||||||
object?: boolean;
|
|
||||||
objectTimestamp?: number;
|
|
||||||
writable?: boolean;
|
|
||||||
writableTimestamp?: number;
|
|
||||||
value?: [string | number | boolean, string];
|
|
||||||
valueTimestamp?: number;
|
|
||||||
notification?: number;
|
|
||||||
notificationTimestamp?: number;
|
|
||||||
accessList?: string[];
|
|
||||||
accessListTimestamp?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FlatDevice {
|
|
||||||
[param: string]: FlatAttributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function flattenDevice(device: Record<string, unknown>): FlatDevice {
|
|
||||||
function recursive(
|
|
||||||
input,
|
|
||||||
root: string,
|
|
||||||
output: FlatDevice,
|
|
||||||
timestamp: number,
|
|
||||||
): void {
|
|
||||||
for (const [name, tree] of Object.entries(input)) {
|
|
||||||
if (!root) {
|
|
||||||
if (name === "_lastInform") {
|
|
||||||
output["Events.Inform"] = {
|
|
||||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_registered") {
|
|
||||||
output["Events.Registered"] = {
|
|
||||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_lastBoot") {
|
|
||||||
output["Events.1_BOOT"] = {
|
|
||||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_lastBootstrap") {
|
|
||||||
output["Events.0_BOOTSTRAP"] = {
|
|
||||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_id") {
|
|
||||||
output["DeviceID.ID"] = {
|
|
||||||
value: [tree as string, "xsd:string"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_deviceId") {
|
|
||||||
output["DeviceID.Manufacturer"] = {
|
|
||||||
value: [tree["_Manufacturer"], "xsd:string"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
output["DeviceID.OUI"] = {
|
|
||||||
value: [tree["_OUI"], "xsd:string"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
output["DeviceID.ProductClass"] = {
|
|
||||||
value: [tree["_ProductClass"], "xsd:string"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
output["DeviceID.SerialNumber"] = {
|
|
||||||
value: [tree["_SerialNumber"], "xsd:string"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
} else if (name === "_tags") {
|
|
||||||
output["Tags"] = {
|
|
||||||
writable: false,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: true,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const t of tree as string[]) {
|
|
||||||
output[`Tags.${encodeTag(t)}`] = {
|
|
||||||
value: [true, "xsd:boolean"],
|
|
||||||
valueTimestamp: timestamp,
|
|
||||||
writable: true,
|
|
||||||
writableTimestamp: timestamp,
|
|
||||||
object: false,
|
|
||||||
objectTimestamp: timestamp,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (name.startsWith("_")) continue;
|
|
||||||
|
|
||||||
let childrenTimestamp = timestamp;
|
|
||||||
|
|
||||||
if (!root) childrenTimestamp = +(input["_timestamp"] || 1);
|
|
||||||
else if (+input["_timestamp"] > timestamp)
|
|
||||||
childrenTimestamp = +input["_timestamp"];
|
|
||||||
|
|
||||||
const attrs: FlatAttributes = {};
|
|
||||||
if (tree["_value"] != null) {
|
|
||||||
attrs.value = [
|
|
||||||
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
|
|
||||||
tree["_type"],
|
|
||||||
];
|
|
||||||
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp);
|
|
||||||
attrs.object = false;
|
|
||||||
attrs.objectTimestamp = childrenTimestamp;
|
|
||||||
} else if (tree["_object"] != null) {
|
|
||||||
attrs.object = tree["_object"];
|
|
||||||
attrs.objectTimestamp = childrenTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree["_writable"] != null) {
|
|
||||||
attrs.writable = tree["_writable"];
|
|
||||||
attrs.writableTimestamp = childrenTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree["_notification"] != null) {
|
|
||||||
attrs.notification = tree["_notification"];
|
|
||||||
attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tree["_accessList"] != null) {
|
|
||||||
attrs.accessList = tree["_accessList"];
|
|
||||||
attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = root ? `${root}.${name}` : name;
|
|
||||||
output[r] = attrs;
|
|
||||||
|
|
||||||
if (attrs.object || tree["object"] == null)
|
|
||||||
recursive(tree, r, output, childrenTimestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDevice: FlatDevice = {};
|
|
||||||
const timestamp = new Date((device["_lastInform"] as Date) || 1).getTime();
|
|
||||||
recursive(device, "", newDevice, timestamp);
|
|
||||||
return newDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenFault(fault: unknown): Fault {
|
|
||||||
const f = Object.assign({}, fault) as Fault;
|
|
||||||
if (f.timestamp) f.timestamp = +f.timestamp;
|
|
||||||
if (f["expiry"]) f["expiry"] = +f["expiry"];
|
|
||||||
return f as Fault;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenTask(task: unknown): Task {
|
|
||||||
const t = Object.assign({}, task) as Task;
|
|
||||||
t._id = "" + t._id;
|
|
||||||
if (t["timestamp"]) t["timestamp"] = +t["timestamp"];
|
|
||||||
if (t.expiry) t.expiry = +t.expiry;
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenPreset(
|
|
||||||
preset: Record<string, unknown>,
|
|
||||||
): Record<string, unknown> {
|
|
||||||
const p = Object.assign({}, preset);
|
|
||||||
if (p.precondition) {
|
|
||||||
try {
|
|
||||||
// Try parse to check expression validity
|
|
||||||
parse(p.precondition as string);
|
|
||||||
} catch (error) {
|
|
||||||
p.precondition = convertOldPrecondition(
|
|
||||||
JSON.parse(p.precondition as string),
|
|
||||||
);
|
);
|
||||||
p.precondition = (p.precondition as string).length
|
|
||||||
? stringify(p.precondition as Expression)
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.events) {
|
|
||||||
const e = [];
|
|
||||||
for (const [k, v] of Object.entries(p.events)) e.push(v ? k : `-${k}`);
|
|
||||||
p.events = e.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
const provision = p.configurations[0];
|
|
||||||
if (
|
|
||||||
(p.configurations as any[]).length === 1 &&
|
|
||||||
provision.type === "provision" &&
|
|
||||||
provision.name &&
|
|
||||||
provision.name.length
|
|
||||||
) {
|
|
||||||
p.provision = provision.name;
|
|
||||||
p.provisionArgs = provision.args
|
|
||||||
? provision.args.map((a) => stringify(a)).join(", ")
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
delete p.configurations;
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenFile(file: Record<string, unknown>): Record<string, unknown> {
|
|
||||||
const f = {};
|
|
||||||
f["_id"] = file["_id"];
|
|
||||||
if (file.metadata) {
|
|
||||||
f["metadata.fileType"] = file["metadata"]["fileType"] || "";
|
|
||||||
f["metadata.oui"] = file["metadata"]["oui"] || "";
|
|
||||||
f["metadata.productClass"] = file["metadata"]["productClass"] || "";
|
|
||||||
f["metadata.version"] = file["metadata"]["version"] || "";
|
|
||||||
}
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
function preProcessPreset(data: Record<string, unknown>): MongoTypes.Preset {
|
|
||||||
const preset = Object.assign({}, data);
|
|
||||||
|
|
||||||
if (!preset.precondition) preset.precondition = "";
|
|
||||||
// Try parse to check expression validity
|
|
||||||
parse(preset.precondition as string);
|
|
||||||
|
|
||||||
preset.weight = parseInt(preset.weight as string) || 0;
|
|
||||||
|
|
||||||
const events = {};
|
|
||||||
if (preset.events) {
|
|
||||||
for (let e of (preset.events as string).split(",")) {
|
|
||||||
let v = true;
|
|
||||||
e = e.trim();
|
|
||||||
if (e.startsWith("-")) {
|
|
||||||
v = false;
|
|
||||||
e = e.slice(1).trim();
|
|
||||||
}
|
|
||||||
if (e) events[e] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preset.events = events;
|
|
||||||
|
|
||||||
if (!preset.provision) throw new Error("Invalid preset provision");
|
|
||||||
|
|
||||||
const configuration = {
|
|
||||||
type: "provision",
|
|
||||||
name: preset.provision,
|
|
||||||
args: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (preset.provisionArgs)
|
|
||||||
configuration.args = parseList(preset.provisionArgs as string);
|
|
||||||
|
|
||||||
delete preset.provision;
|
|
||||||
delete preset.provisionArgs;
|
|
||||||
preset.configurations = [configuration];
|
|
||||||
return preset as unknown as MongoTypes.Preset;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface QueryOptions {
|
|
||||||
projection?: any;
|
|
||||||
skip?: number;
|
|
||||||
limit?: number;
|
|
||||||
sort?: {
|
|
||||||
[param: string]: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function* query(
|
|
||||||
resource: string,
|
|
||||||
filter: Expression,
|
|
||||||
options?: QueryOptions,
|
|
||||||
): AsyncGenerator<any, void, undefined> {
|
|
||||||
options = options || {};
|
|
||||||
filter = evaluate(filter, null, Date.now());
|
|
||||||
const q = toMongoQuery(filter, resource);
|
|
||||||
if (!q) return;
|
|
||||||
|
|
||||||
const collection = collections[resource] as Collection<any>;
|
|
||||||
const cursor = collection.find(q);
|
|
||||||
if (options.projection) {
|
|
||||||
let projection = options.projection;
|
|
||||||
if (resource === "devices")
|
|
||||||
projection = processDeviceProjection(options.projection);
|
|
||||||
|
|
||||||
if (resource === "presets") projection.configurations = 1;
|
|
||||||
projection = optimizeProjection(projection);
|
|
||||||
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.sort) {
|
|
||||||
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 (resource === "devices") s = processDeviceSort(s);
|
|
||||||
cursor.sort(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
for await (let doc of cursor) {
|
|
||||||
if (resource === "devices") doc = flattenDevice(doc);
|
|
||||||
else if (resource === "faults") doc = flattenFault(doc);
|
|
||||||
else if (resource === "tasks") doc = flattenTask(doc);
|
|
||||||
else if (resource === "presets") doc = flattenPreset(doc);
|
|
||||||
else if (resource === "files") doc = flattenFile(doc);
|
|
||||||
|
|
||||||
yield doc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function count(resource: string, filter: Expression): Promise<number> {
|
|
||||||
const collection = collections[resource] as Collection<any>;
|
|
||||||
filter = evaluate(filter, null, Date.now());
|
|
||||||
const q = toMongoQuery(filter, resource);
|
|
||||||
if (!q) return Promise.resolve(0);
|
|
||||||
return collection.countDocuments(q);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateDeviceTags(
|
|
||||||
deviceId: string,
|
|
||||||
tags: Record<string, boolean>,
|
|
||||||
): Promise<void> {
|
|
||||||
const add = [];
|
|
||||||
const pull = [];
|
|
||||||
|
|
||||||
for (let [tag, onOff] of Object.entries(tags)) {
|
|
||||||
tag = tag.trim();
|
|
||||||
if (onOff) add.push(tag);
|
|
||||||
else pull.push(tag);
|
|
||||||
}
|
|
||||||
const object = {};
|
|
||||||
|
|
||||||
if (add?.length) object["$addToSet"] = { _tags: { $each: add } };
|
|
||||||
if (pull?.length) object["$pullAll"] = { _tags: pull };
|
|
||||||
|
|
||||||
await collections.devices.updateOne({ _id: deviceId }, object);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function putPreset(
|
|
||||||
id: string,
|
|
||||||
object: Record<string, unknown>,
|
|
||||||
): Promise<void> {
|
|
||||||
const p = preProcessPreset(object);
|
|
||||||
await collections.presets.replaceOne({ _id: id }, p, { upsert: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deletePreset(id: string): Promise<void> {
|
|
||||||
await collections.presets.deleteOne({ _id: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function putProvision(
|
|
||||||
id: string,
|
|
||||||
object: { script: string },
|
|
||||||
): Promise<void> {
|
|
||||||
if (!object.script) object.script = "";
|
|
||||||
try {
|
|
||||||
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
|
|
||||||
filename: id,
|
|
||||||
lineOffset: -1,
|
|
||||||
});
|
});
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
await collections.provisions.replaceOne({ _id: id }, object, {
|
|
||||||
upsert: true,
|
return clientPromise;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProvision(id: string): Promise<void> {
|
export function cache<T>(key, valueGetter: () => Promise<T>, ttl): Promise<T> {
|
||||||
await collections.provisions.deleteOne({ _id: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function putVirtualParameter(
|
|
||||||
id: string,
|
|
||||||
object: { script: string },
|
|
||||||
): Promise<void> {
|
|
||||||
if (!object.script) object.script = "";
|
|
||||||
try {
|
|
||||||
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
|
|
||||||
filename: id,
|
|
||||||
lineOffset: -1,
|
|
||||||
});
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
await collections.virtualParameters.replaceOne({ _id: id }, object, {
|
|
||||||
upsert: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteVirtualParameter(id: string): Promise<void> {
|
|
||||||
await collections.virtualParameters.deleteOne({ _id: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function putConfig(
|
|
||||||
id: string,
|
|
||||||
object: WithoutId<MongoTypes.Config>,
|
|
||||||
): Promise<void> {
|
|
||||||
await collections.config.replaceOne({ _id: id }, object, { upsert: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteConfig(id: string): Promise<void> {
|
|
||||||
await collections.config.deleteOne({ _id: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function putPermission(
|
|
||||||
id: string,
|
|
||||||
object: WithoutId<MongoTypes.Permission>,
|
|
||||||
): Promise<void> {
|
|
||||||
await collections.permissions.replaceOne({ _id: id }, object, {
|
|
||||||
upsert: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deletePermission(id: string): Promise<void> {
|
|
||||||
await collections.permissions.deleteOne({ _id: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function putUser(
|
|
||||||
id: string,
|
|
||||||
object: Partial<WithoutId<MongoTypes.User>>,
|
|
||||||
): Promise<void> {
|
|
||||||
// update instead of replace to keep the password if not set by user
|
|
||||||
await collections.users.updateOne(
|
|
||||||
{ _id: id },
|
|
||||||
{ $set: object },
|
|
||||||
{ upsert: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteUser(id: string): Promise<void> {
|
|
||||||
await collections.users.deleteOne({ _id: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function downloadFile(filename: string): Readable {
|
|
||||||
return filesBucket.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 uploadStream = filesBucket.openUploadStreamWithId(
|
getClient()
|
||||||
filename as unknown as ObjectId,
|
.then(client => {
|
||||||
filename,
|
const collection = client.db().collection("cache");
|
||||||
{
|
collection.findOne({ _id: key }, (err, doc) => {
|
||||||
metadata: metadata,
|
if (err) return void reject(err);
|
||||||
},
|
if (doc != null) return void resolve(JSON.parse(doc.value) as T);
|
||||||
);
|
valueGetter()
|
||||||
|
.then(res => {
|
||||||
let readableEnded = false;
|
const expire = Date.now() + (ttl || CACHE_TTL);
|
||||||
contentStream.on("end", () => {
|
const cacheDoc = {
|
||||||
readableEnded = true;
|
_id: key,
|
||||||
});
|
value: JSON.stringify(res),
|
||||||
contentStream.on("close", () => {
|
expire: new Date(expire)
|
||||||
// In Node versions prior to 15, the stream will not emit an error if the
|
};
|
||||||
// connection is closed before the stream is finished.
|
collection.updateOne(
|
||||||
// For Node 12.9+ we can just use stream.readableEnded
|
{ _id: key },
|
||||||
if (!readableEnded)
|
{ $set: cacheDoc },
|
||||||
uploadStream.destroy(new Error("Stream closed prematurely"));
|
{ upsert: true },
|
||||||
});
|
err => {
|
||||||
|
if (err) reject(err);
|
||||||
contentStream.on("error", (err) => {
|
else resolve(res);
|
||||||
uploadStream.destroy(err);
|
}
|
||||||
});
|
);
|
||||||
|
})
|
||||||
uploadStream.on("error", reject);
|
.catch(reject);
|
||||||
uploadStream.on("finish", resolve);
|
});
|
||||||
contentStream.pipe(uploadStream);
|
})
|
||||||
|
.catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteFile(filename: string): Promise<void> {
|
export function query(
|
||||||
await filesBucket.delete(filename as any);
|
resource,
|
||||||
|
filter,
|
||||||
|
options?: QueryOptions,
|
||||||
|
callback?: (doc: any) => void
|
||||||
|
): Promise<any[]> {
|
||||||
|
options = options || {};
|
||||||
|
let q;
|
||||||
|
filter = expression.evaluate(filter, null, Date.now());
|
||||||
|
|
||||||
|
if (Array.isArray(filter)) {
|
||||||
|
if (resource === "devices") {
|
||||||
|
filter = mongodbFunctions.processDeviceFilter(filter);
|
||||||
|
} else if (resource === "tasks") {
|
||||||
|
filter = mongodbFunctions.processTasksFilter(filter);
|
||||||
|
} else if (resource === "faults") {
|
||||||
|
filter = mongodbFunctions.processFaultsFilter(filter);
|
||||||
|
} else if (resource === "users") {
|
||||||
|
// Protect against brute force, and dictionary attacks
|
||||||
|
const params = expression.extractParams(filter);
|
||||||
|
if (params.includes("password") || params.includes("salt"))
|
||||||
|
return Promise.reject(new Error("Invalid users filter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
q = mongodbFunctions.filterToMongoQuery(filter);
|
||||||
|
} else if (filter != null && !filter) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getClient().then(client => {
|
||||||
|
const collection = client
|
||||||
|
.db()
|
||||||
|
.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||||
|
const cursor = collection.find(q);
|
||||||
|
if (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 === "users") cursor.project({ password: 0, salt: 0 });
|
||||||
|
|
||||||
|
if (options.skip) cursor.skip(options.skip);
|
||||||
|
if (options.limit) cursor.limit(options.limit);
|
||||||
|
|
||||||
|
if (options.sort) {
|
||||||
|
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 (resource === "devices") s = mongodbFunctions.processDeviceSort(s);
|
||||||
|
cursor.sort(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callback) {
|
||||||
|
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")
|
||||||
|
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 async function deleteFault(id: string): Promise<void> {
|
export function count(resource, filter): Promise<number> {
|
||||||
await collections.faults.deleteOne({ _id: id });
|
let q;
|
||||||
|
filter = expression.evaluate(filter, null, Date.now());
|
||||||
|
|
||||||
|
if (Array.isArray(filter)) {
|
||||||
|
if (resource === "devices")
|
||||||
|
filter = mongodbFunctions.processDeviceFilter(filter);
|
||||||
|
else if (resource === "tasks")
|
||||||
|
filter = mongodbFunctions.processTasksFilter(filter);
|
||||||
|
else if (resource === "faults")
|
||||||
|
filter = mongodbFunctions.processFaultsFilter(filter);
|
||||||
|
q = mongodbFunctions.filterToMongoQuery(filter);
|
||||||
|
} else if (filter != null && !filter) {
|
||||||
|
return Promise.resolve(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getClient().then(client => {
|
||||||
|
const collection = client
|
||||||
|
.db()
|
||||||
|
.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||||
|
collection.find(q).count((err, c) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(c);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteTask(id: ObjectId): Promise<void> {
|
export function updateDeviceTags(deviceId, tags): Promise<void> {
|
||||||
await collections.tasks.deleteOne({ _id: id });
|
return new Promise((resolve, reject) => {
|
||||||
|
const add = [];
|
||||||
|
const pull = [];
|
||||||
|
|
||||||
|
const regex = /^[0-9a-zA-Z_]+$/;
|
||||||
|
for (let [tag, onOff] of Object.entries(tags)) {
|
||||||
|
tag = tag.trim();
|
||||||
|
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 object = {};
|
||||||
|
if (add && add.length) object["$addToSet"] = { _tags: { $each: add } };
|
||||||
|
if (pull && pull.length) object["$pullAll"] = { _tags: pull };
|
||||||
|
collection.updateOne({ _id: deviceId }, object, err => {
|
||||||
|
if (err) return void reject(err);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function putResource(resource, id, object): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getClient()
|
||||||
|
.then(client => {
|
||||||
|
const collection = client
|
||||||
|
.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(resource, id): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getClient()
|
||||||
|
.then(client => {
|
||||||
|
const collection = client
|
||||||
|
.db()
|
||||||
|
.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||||
|
collection.deleteOne({ _id: id }, err => {
|
||||||
|
if (err) return void reject(err);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putPreset(id, object): Promise<void> {
|
||||||
|
object = mongodbFunctions.preProcessPreset(object);
|
||||||
|
return putResource("presets", id, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePreset(id): Promise<void> {
|
||||||
|
return deleteResource("presets", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putProvision(id, object): Promise<void> {
|
||||||
|
if (!object.script) object.script = "";
|
||||||
|
try {
|
||||||
|
new Script(`"use strict";(function(){\n${object.script}\n})();`);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
return putResource("provisions", id, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProvision(id): Promise<void> {
|
||||||
|
return deleteResource("provisions", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putVirtualParameter(id, object): Promise<void> {
|
||||||
|
if (!object.script) object.script = "";
|
||||||
|
try {
|
||||||
|
new Script(`"use strict";(function(){\n${object.script}\n})();`);
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
return putResource("virtualParameters", id, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteVirtualParameter(id): Promise<void> {
|
||||||
|
return deleteResource("virtualParameters", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putConfig(id, object): Promise<void> {
|
||||||
|
return putResource("config", id, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteConfig(id): Promise<void> {
|
||||||
|
return deleteResource("config", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putPermission(id, object): Promise<void> {
|
||||||
|
return putResource("permissions", id, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePermission(id): Promise<void> {
|
||||||
|
return deleteResource("permissions", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putUser(id, object): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getClient()
|
||||||
|
.then(client => {
|
||||||
|
const collection = client.db().collection("users");
|
||||||
|
// update instead of replace to keep the password if not set by user
|
||||||
|
collection.updateOne(
|
||||||
|
{ _id: id },
|
||||||
|
{ $set: object },
|
||||||
|
{ upsert: true },
|
||||||
|
err => {
|
||||||
|
if (err) return void reject(err);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteUser(id): Promise<void> {
|
||||||
|
return deleteResource("users", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function putFile(filename, metadata, contentStream): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getClient()
|
||||||
|
.then(client => {
|
||||||
|
const bucket = new GridFSBucket(client.db());
|
||||||
|
const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
|
||||||
|
metadata: metadata
|
||||||
|
});
|
||||||
|
uploadStream.on("error", reject);
|
||||||
|
contentStream.on("error", reject);
|
||||||
|
uploadStream.on("finish", resolve);
|
||||||
|
contentStream.pipe(uploadStream);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteFile(filename): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getClient()
|
||||||
|
.then(client => {
|
||||||
|
const bucket = new GridFSBucket(client.db());
|
||||||
|
bucket.delete(filename, err => {
|
||||||
|
if (err) return void reject(err);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteFault(id): Promise<void> {
|
||||||
|
return deleteResource("faults", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTask(id): Promise<void> {
|
||||||
|
return deleteResource("tasks", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disconnect(): Promise<void> {
|
||||||
|
if (clientPromise) await (await clientPromise).close();
|
||||||
}
|
}
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
import * as crypto from "node:crypto";
|
|
||||||
import * as config from "../config.ts";
|
|
||||||
import { collections } from "../db/db.ts";
|
|
||||||
import * as expression from "../common/expression/util.ts";
|
|
||||||
import { parse } from "../common/expression/parser.ts";
|
|
||||||
import { Expression, Users, Permissions, Config, UiConfig } from "../types.ts";
|
|
||||||
import { LocalCache } from "../local-cache.ts";
|
|
||||||
|
|
||||||
interface Snapshot {
|
|
||||||
permissions: Permissions;
|
|
||||||
users: Users;
|
|
||||||
config: Config;
|
|
||||||
ui: UiConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchPermissions(): Promise<[string, Permissions]> {
|
|
||||||
const perms = await collections.permissions.find().toArray();
|
|
||||||
perms.sort((a, b) => (a._id > b._id ? 1 : -1));
|
|
||||||
const h = crypto
|
|
||||||
.createHash("md5")
|
|
||||||
.update(JSON.stringify(perms))
|
|
||||||
.digest("hex");
|
|
||||||
const permissions: Permissions = {};
|
|
||||||
|
|
||||||
for (const p of perms) {
|
|
||||||
if (!permissions[p.role]) permissions[p.role] = {};
|
|
||||||
if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {};
|
|
||||||
|
|
||||||
permissions[p.role][p.access][p.resource] = {
|
|
||||||
access: p.access,
|
|
||||||
filter: parse(p.filter || "true"),
|
|
||||||
};
|
|
||||||
if (p.validate)
|
|
||||||
permissions[p.role][p.access][p.resource].validate = parse(p.validate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [h, permissions];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchUsers(): Promise<[string, Users]> {
|
|
||||||
const _users = await collections.users.find().toArray();
|
|
||||||
_users.sort((a, b) => (a._id > b._id ? 1 : -1));
|
|
||||||
const h = crypto
|
|
||||||
.createHash("md5")
|
|
||||||
.update(JSON.stringify(_users))
|
|
||||||
.digest("hex");
|
|
||||||
const users = {};
|
|
||||||
|
|
||||||
for (const user of _users) {
|
|
||||||
users[user._id] = {
|
|
||||||
password: user.password,
|
|
||||||
salt: user.salt,
|
|
||||||
roles: user.roles.split(",").map((s) => s.trim()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return [h, users];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchConfig(): Promise<[string, Config, UiConfig]> {
|
|
||||||
const conf = await collections.config.find().toArray();
|
|
||||||
conf.sort((a, b) => (a._id > b._id ? 1 : -1));
|
|
||||||
const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex");
|
|
||||||
|
|
||||||
const ui = {
|
|
||||||
filters: {},
|
|
||||||
device: {},
|
|
||||||
index: {},
|
|
||||||
overview: {},
|
|
||||||
pageSize: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const _config = {};
|
|
||||||
|
|
||||||
for (const c of conf) {
|
|
||||||
// Evaluate expressions to simplify them
|
|
||||||
const val = expression.evaluate(parse(c.value));
|
|
||||||
_config[c._id] = val;
|
|
||||||
if (c._id.startsWith("ui.")) {
|
|
||||||
const keys = c._id.split(".");
|
|
||||||
if (!(keys[1] in ui)) continue;
|
|
||||||
// remove the first key(ui)
|
|
||||||
keys.shift();
|
|
||||||
let ref = ui;
|
|
||||||
while (keys.length > 1) {
|
|
||||||
const k = keys.shift();
|
|
||||||
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
|
|
||||||
ref = ref[k];
|
|
||||||
}
|
|
||||||
ref[keys[0]] = val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [h, _config, ui];
|
|
||||||
}
|
|
||||||
|
|
||||||
const localCache = new LocalCache<Snapshot>("ui-local-cache-hash", refresh);
|
|
||||||
|
|
||||||
async function refresh(): Promise<[string, Snapshot]> {
|
|
||||||
const res = await Promise.all([
|
|
||||||
fetchPermissions(),
|
|
||||||
fetchUsers(),
|
|
||||||
fetchConfig(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const h = crypto.createHash("md5");
|
|
||||||
for (const r of res) h.update(r[0]);
|
|
||||||
|
|
||||||
const snapshot = {
|
|
||||||
permissions: res[0][1],
|
|
||||||
users: res[1][1],
|
|
||||||
config: res[2][1],
|
|
||||||
ui: res[2][2],
|
|
||||||
};
|
|
||||||
|
|
||||||
return [h.digest("hex"), snapshot];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getRevision(): Promise<string> {
|
|
||||||
return await localCache.getRevision();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConfig(
|
|
||||||
revision: string,
|
|
||||||
key: string,
|
|
||||||
context: Record<string, unknown>,
|
|
||||||
now: number,
|
|
||||||
cb?: (e: Expression) => Expression,
|
|
||||||
): string | number | boolean | null {
|
|
||||||
const snapshot = localCache.get(revision);
|
|
||||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
|
||||||
|
|
||||||
const oldOpts = {
|
|
||||||
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
|
|
||||||
"cwmp.debug": "DEBUG",
|
|
||||||
"cwmp.retryDelay": "RETRY_DELAY",
|
|
||||||
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
|
|
||||||
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
|
|
||||||
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
|
|
||||||
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
|
|
||||||
"cwmp.cookiesPath": "COOKIES_PATH",
|
|
||||||
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
|
|
||||||
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
|
|
||||||
"cwmp.connectionRequestAllowBasicAuth":
|
|
||||||
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
|
|
||||||
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
|
|
||||||
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
|
|
||||||
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!(key in snapshot.config)) {
|
|
||||||
if (key in oldOpts) {
|
|
||||||
let id;
|
|
||||||
if (context?.["id"]) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = expression.evaluate(snapshot.config[key], context, now, cb);
|
|
||||||
return Array.isArray(v) ? null : v;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConfigExpression(revision: string, key: string): Expression {
|
|
||||||
const snapshot = localCache.get(revision);
|
|
||||||
return snapshot.config[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUsers(revision: string): Users {
|
|
||||||
const snapshot = localCache.get(revision);
|
|
||||||
return snapshot.users;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPermissions(revision: string): Permissions {
|
|
||||||
const snapshot = localCache.get(revision);
|
|
||||||
return snapshot.permissions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUiConfig(revision: string): UiConfig {
|
|
||||||
const snapshot = localCache.get(revision);
|
|
||||||
return snapshot.ui;
|
|
||||||
}
|
|
||||||
-70
@@ -1,70 +0,0 @@
|
|||||||
import { EventEmitter } from "node:events";
|
|
||||||
|
|
||||||
export function generateDeviceId(
|
|
||||||
deviceIdStruct: Record<string, string>,
|
|
||||||
): string {
|
|
||||||
// Escapes everything except alphanumerics and underscore
|
|
||||||
function esc(str): string {
|
|
||||||
return str.replace(/[^A-Za-z0-9_]/g, (chr) => {
|
|
||||||
const buf = Buffer.from(chr, "utf8");
|
|
||||||
let rep = "";
|
|
||||||
for (const b of buf) rep += "%" + b.toString(16).toUpperCase();
|
|
||||||
return rep;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guaranteeing globally unique id as defined in TR-069
|
|
||||||
if (deviceIdStruct["ProductClass"]) {
|
|
||||||
return (
|
|
||||||
esc(deviceIdStruct["OUI"]) +
|
|
||||||
"-" +
|
|
||||||
esc(deviceIdStruct["ProductClass"]) +
|
|
||||||
"-" +
|
|
||||||
esc(deviceIdStruct["SerialNumber"])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source: http://stackoverflow.com/a/6969486
|
|
||||||
export function escapeRegExp(str: string): string {
|
|
||||||
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, "%"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function once(
|
|
||||||
emitter: EventEmitter,
|
|
||||||
event: string,
|
|
||||||
timeout: number,
|
|
||||||
): Promise<unknown[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
reject(new Error(`Event ${event} timed out after ${timeout} ms`));
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
emitter.once(event, (...args: unknown[]) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolve(args);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setTimeoutPromise(delay: number, ref = true): Promise<void> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const timerId = setTimeout(resolve, delay);
|
|
||||||
if (!ref) timerId.unref();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+26
-8
@@ -1,3 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
const NONEXISTENT = Symbol();
|
const NONEXISTENT = Symbol();
|
||||||
const UNDEFINED = undefined;
|
const UNDEFINED = undefined;
|
||||||
|
|
||||||
@@ -7,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];
|
||||||
@@ -27,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];
|
||||||
|
|
||||||
@@ -119,7 +138,7 @@ export default class VersionedMap<K, V> {
|
|||||||
const del = revisionsObj.delete || 0;
|
const del = revisionsObj.delete || 0;
|
||||||
const mutations = Object.keys(revisionsObj).reduce(
|
const mutations = Object.keys(revisionsObj).reduce(
|
||||||
(acc, cur) => (cur === "delete" ? acc : acc | (1 << +cur)),
|
(acc, cur) => (cur === "delete" ? acc : acc | (1 << +cur)),
|
||||||
del,
|
del
|
||||||
);
|
);
|
||||||
|
|
||||||
const revisions = [];
|
const revisions = [];
|
||||||
@@ -149,7 +168,6 @@ export default class VersionedMap<K, V> {
|
|||||||
|
|
||||||
public *diff(): IterableIterator<[K, V, V]> {
|
public *diff(): IterableIterator<[K, V, V]> {
|
||||||
for (const [key, revisions] of this.map) {
|
for (const [key, revisions] of this.map) {
|
||||||
if (revisions.length === 1) continue;
|
|
||||||
let first = revisions[0];
|
let first = revisions[0];
|
||||||
let last = revisions[revisions.length - 1];
|
let last = revisions[revisions.length - 1];
|
||||||
if (first === NONEXISTENT && last === NONEXISTENT) continue;
|
if (first === NONEXISTENT && last === NONEXISTENT) continue;
|
||||||
@@ -175,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-14
@@ -1,3 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
const CHAR_SINGLE_QUOTE = 39;
|
const CHAR_SINGLE_QUOTE = 39;
|
||||||
const CHAR_DOUBLE_QUOTE = 34;
|
const CHAR_DOUBLE_QUOTE = 34;
|
||||||
const CHAR_LESS_THAN = 60;
|
const CHAR_LESS_THAN = 60;
|
||||||
@@ -34,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
|
||||||
}
|
}
|
||||||
@@ -72,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 = "";
|
||||||
@@ -85,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
|
||||||
@@ -112,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 """:
|
case """:
|
||||||
return '"';
|
return '"';
|
||||||
@@ -145,15 +161,15 @@ export function decodeEntities(string: string): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodeEntities(string: string): string {
|
export function encodeEntities(string): string {
|
||||||
const entities = {
|
const entities = {
|
||||||
"&": "&",
|
"&": "&",
|
||||||
'"': """,
|
'"': """,
|
||||||
"'": "'",
|
"'": "'",
|
||||||
"<": "<",
|
"<": "<",
|
||||||
">": ">",
|
">": ">"
|
||||||
};
|
};
|
||||||
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 {
|
||||||
@@ -170,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];
|
||||||
@@ -314,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);
|
||||||
|
|||||||
@@ -1,466 +0,0 @@
|
|||||||
import * as net from "node:net";
|
|
||||||
import * as tls from "node:tls";
|
|
||||||
import { EventEmitter } from "node:events";
|
|
||||||
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
||||||
import { parseXml, Element, parseAttrs } from "./xml-parser.ts";
|
|
||||||
|
|
||||||
function encodeBase64(str: string): string {
|
|
||||||
return Buffer.from(str).toString("base64");
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeBase64(str: string): string {
|
|
||||||
return Buffer.from(str, "base64").toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectStreamTag(data: string): number {
|
|
||||||
const i1 = data.indexOf("<stream:stream");
|
|
||||||
if (i1 < 0) throw new Error("Cannot detect opening stream tag");
|
|
||||||
const i2 = data.indexOf(">", i1);
|
|
||||||
if (i2 < 0) throw new Error("Cannot detect opening stream tag");
|
|
||||||
return i2 + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function xmppStream<T>(
|
|
||||||
socket: net.Socket,
|
|
||||||
callback: Generator<void, T, Element>,
|
|
||||||
): Promise<T> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const onError = (err: Error): void => {
|
|
||||||
socket.removeListener("error", onError);
|
|
||||||
reject(err);
|
|
||||||
};
|
|
||||||
socket.on("error", onError);
|
|
||||||
|
|
||||||
const onData = (chunk: Buffer): void => {
|
|
||||||
try {
|
|
||||||
const str = chunk.toString("utf8");
|
|
||||||
const xml = parseXml(str);
|
|
||||||
const { value, done } = callback.next(xml.children[0]);
|
|
||||||
if (done) {
|
|
||||||
socket.removeListener("error", onError);
|
|
||||||
socket.removeListener("data", onData);
|
|
||||||
resolve(value as T);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
socket.removeListener("error", onError);
|
|
||||||
socket.removeListener("data", onData);
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.once("data", (chunk: Buffer) => {
|
|
||||||
try {
|
|
||||||
const str = chunk.toString("utf8");
|
|
||||||
const i = detectStreamTag(str);
|
|
||||||
const streamTagStr = str.slice(0, i);
|
|
||||||
const xml = parseXml(streamTagStr + "</stream:stream>");
|
|
||||||
callback.next(xml.children[0]);
|
|
||||||
chunk = chunk.slice(Buffer.byteLength(streamTagStr));
|
|
||||||
if (chunk.length) onData(chunk);
|
|
||||||
socket.on("data", onData);
|
|
||||||
} catch (err) {
|
|
||||||
socket.removeListener("error", onError);
|
|
||||||
socket.removeListener("data", onData);
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
callback.next();
|
|
||||||
} catch (err) {
|
|
||||||
socket.removeListener("error", onError);
|
|
||||||
socket.removeListener("data", onData);
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const INT_1 = Buffer.from([0, 0, 0, 1]);
|
|
||||||
const saltedPasswordCache = {
|
|
||||||
password: "",
|
|
||||||
iterationCount: 0,
|
|
||||||
saltBase64: "",
|
|
||||||
salted: Buffer.allocUnsafe(0),
|
|
||||||
};
|
|
||||||
|
|
||||||
function saltPassword(
|
|
||||||
password: string,
|
|
||||||
saltBase64: string,
|
|
||||||
iteractionCount: number,
|
|
||||||
): Buffer {
|
|
||||||
if (
|
|
||||||
password === saltedPasswordCache.password &&
|
|
||||||
saltBase64 === saltedPasswordCache.saltBase64 &&
|
|
||||||
iteractionCount === saltedPasswordCache.iterationCount
|
|
||||||
)
|
|
||||||
return saltedPasswordCache.salted;
|
|
||||||
|
|
||||||
const hi = createHmac("sha1", password)
|
|
||||||
.update(Buffer.concat([Buffer.from(saltBase64, "base64"), INT_1]))
|
|
||||||
.digest();
|
|
||||||
let hi2: Buffer = hi;
|
|
||||||
for (let i = 1; i < iteractionCount; ++i) {
|
|
||||||
hi2 = createHmac("sha1", password).update(hi2).digest();
|
|
||||||
for (const [j, b] of hi2.entries()) hi[j] ^= b;
|
|
||||||
}
|
|
||||||
|
|
||||||
saltedPasswordCache.saltBase64 = saltBase64;
|
|
||||||
saltedPasswordCache.password = password;
|
|
||||||
saltedPasswordCache.iterationCount = iteractionCount;
|
|
||||||
saltedPasswordCache.salted = hi;
|
|
||||||
|
|
||||||
return hi;
|
|
||||||
}
|
|
||||||
|
|
||||||
function* loginPlain(
|
|
||||||
socket: net.Socket,
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
): Generator<void, void, Element> {
|
|
||||||
socket.write(
|
|
||||||
`<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">${encodeBase64(
|
|
||||||
`\x00${username}\x00${password}`,
|
|
||||||
)}</auth>`,
|
|
||||||
);
|
|
||||||
const res1 = yield;
|
|
||||||
|
|
||||||
if (
|
|
||||||
res1.name === "failure" &&
|
|
||||||
res1.children.some((c) => c.name === "not-authorized")
|
|
||||||
)
|
|
||||||
throw new Error("Not authorized");
|
|
||||||
|
|
||||||
if (res1.name !== "success")
|
|
||||||
throw new Error(`Unexpected response ${res1.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function* loginScram(
|
|
||||||
socket: net.Socket,
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
): Generator<void, void, Element> {
|
|
||||||
const cnonce = randomBytes(8).toString("base64");
|
|
||||||
const gs2Header = "n,,";
|
|
||||||
const clientFirstMessageBare = `n=${username},r=${cnonce}`;
|
|
||||||
const clientFirstMessage = gs2Header + clientFirstMessageBare;
|
|
||||||
socket.write(
|
|
||||||
`<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="SCRAM-SHA-1">${encodeBase64(
|
|
||||||
clientFirstMessage,
|
|
||||||
)}</auth>`,
|
|
||||||
);
|
|
||||||
const res1 = yield;
|
|
||||||
if (res1.name !== "challenge")
|
|
||||||
throw new Error(`Unexpected element ${res1.name}`);
|
|
||||||
const serverFirstMessage = decodeBase64(res1.text);
|
|
||||||
|
|
||||||
let iterationCount: number;
|
|
||||||
let saltBase64: string;
|
|
||||||
let nonce: string;
|
|
||||||
for (const s of serverFirstMessage.split(",")) {
|
|
||||||
if (s.startsWith("i=")) iterationCount = parseInt(s.slice(2));
|
|
||||||
else if (s.startsWith("s=")) saltBase64 = s.slice(2);
|
|
||||||
else if (s.startsWith("r=")) nonce = s.slice(2);
|
|
||||||
}
|
|
||||||
if (iterationCount == null || isNaN(iterationCount))
|
|
||||||
throw new Error("Invalid iteration count");
|
|
||||||
if (saltBase64 == null) throw new Error("Missing salt");
|
|
||||||
if (nonce == null) throw new Error("Missing nonce");
|
|
||||||
|
|
||||||
const saltedPassword = saltPassword(password, saltBase64, iterationCount);
|
|
||||||
const clientKey = createHmac("sha1", saltedPassword)
|
|
||||||
.update("Client Key")
|
|
||||||
.digest();
|
|
||||||
const storedKey = createHash("sha1").update(clientKey).digest();
|
|
||||||
|
|
||||||
const clientFinalMessageWithoutProof = `c=${encodeBase64(
|
|
||||||
gs2Header,
|
|
||||||
)},r=${nonce}`;
|
|
||||||
const authMessage = `${clientFirstMessageBare},${serverFirstMessage},${clientFinalMessageWithoutProof}`;
|
|
||||||
const clientSignature = createHmac("sha1", storedKey)
|
|
||||||
.update(authMessage)
|
|
||||||
.digest();
|
|
||||||
|
|
||||||
const clientProof = Buffer.from(clientKey);
|
|
||||||
for (const [i, b] of clientSignature.entries()) clientProof[i] ^= b;
|
|
||||||
|
|
||||||
const clientFinalMessage = `${clientFinalMessageWithoutProof},p=${clientProof.toString(
|
|
||||||
"base64",
|
|
||||||
)}`;
|
|
||||||
socket.write(
|
|
||||||
`<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">${encodeBase64(
|
|
||||||
clientFinalMessage,
|
|
||||||
)}</response>`,
|
|
||||||
);
|
|
||||||
const res2 = yield;
|
|
||||||
|
|
||||||
if (
|
|
||||||
res2.name === "failure" &&
|
|
||||||
res2.children.some((c) => c.name === "not-authorized")
|
|
||||||
)
|
|
||||||
throw new Error("Not authorized");
|
|
||||||
|
|
||||||
if (res2.name !== "success")
|
|
||||||
throw new Error(`Unexpected response ${res2.name}`);
|
|
||||||
|
|
||||||
const serverKey = createHmac("sha1", saltedPassword)
|
|
||||||
.update("Server Key")
|
|
||||||
.digest();
|
|
||||||
const serverSignature = createHmac("sha1", serverKey)
|
|
||||||
.update(authMessage)
|
|
||||||
.digest("base64");
|
|
||||||
|
|
||||||
if (!decodeBase64(res2.text).endsWith(serverSignature))
|
|
||||||
throw new Error("Invalid server signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
function* starttls(socket: net.Socket): Generator<void, void, Element> {
|
|
||||||
socket.write("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");
|
|
||||||
const res1 = yield;
|
|
||||||
if (res1.name !== "proceed") throw new Error("Failed to initiate STARTTLS");
|
|
||||||
}
|
|
||||||
|
|
||||||
function* bind(
|
|
||||||
socket: net.Socket,
|
|
||||||
resource: string,
|
|
||||||
): Generator<void, void, Element> {
|
|
||||||
const id = randomBytes(8).toString("base64");
|
|
||||||
socket.write(
|
|
||||||
`<iq id='${id}' type='set'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>${resource}</resource></bind></iq>`,
|
|
||||||
);
|
|
||||||
const res1 = yield;
|
|
||||||
if (res1.name !== "iq") throw new Error(`Unexpected element ${res1.name}`);
|
|
||||||
const attrs1 = parseAttrs(res1.attrs);
|
|
||||||
const idAttr = attrs1.find((a) => a.name === "id");
|
|
||||||
if (!idAttr || idAttr.value !== id) throw new Error("Invalid ID");
|
|
||||||
const typeAttr = attrs1.find((a) => a.name === "type");
|
|
||||||
if (!typeAttr) throw new Error("Missing type attribute");
|
|
||||||
if (typeAttr.value !== "result") throw new Error("Cannot bind to resource");
|
|
||||||
}
|
|
||||||
|
|
||||||
const STATUS_RESTART_STREAM = 1;
|
|
||||||
const STATUS_STARTTLS = 2;
|
|
||||||
|
|
||||||
function* init(
|
|
||||||
socket: net.Socket,
|
|
||||||
host: string,
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
resource: string,
|
|
||||||
): Generator<void, number, Element> {
|
|
||||||
socket.write(
|
|
||||||
`<?xml version='1.0'?><stream:stream from='${username}@${host}' to='${host}' version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>`,
|
|
||||||
);
|
|
||||||
const open = yield;
|
|
||||||
if (open.name !== "stream:stream")
|
|
||||||
throw new Error(`Unexpected element ${open.name}`);
|
|
||||||
const features = yield;
|
|
||||||
if (features.name !== "stream:features")
|
|
||||||
throw new Error(`Unexpected element ${features.name}`);
|
|
||||||
for (const feature of features.children) {
|
|
||||||
if (feature.name === "starttls") {
|
|
||||||
if (feature.children.some((c) => c.name === "required")) {
|
|
||||||
yield* starttls(socket);
|
|
||||||
return STATUS_STARTTLS;
|
|
||||||
}
|
|
||||||
} else if (feature.name === "mechanisms") {
|
|
||||||
const mechanisms: Set<string> = new Set(
|
|
||||||
feature.children.map((c) => c.text),
|
|
||||||
);
|
|
||||||
if (mechanisms.has("PLAIN")) {
|
|
||||||
yield* loginPlain(socket, username, password);
|
|
||||||
return STATUS_RESTART_STREAM;
|
|
||||||
} else if (mechanisms.has("SCRAM-SHA-1")) {
|
|
||||||
yield* loginScram(socket, username, password);
|
|
||||||
return STATUS_RESTART_STREAM;
|
|
||||||
} else {
|
|
||||||
throw new Error("No supported SASL method");
|
|
||||||
}
|
|
||||||
} else if (feature.name === "bind") {
|
|
||||||
yield* bind(socket, resource);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function upgradeTls(socket: net.Socket, host: string): Promise<tls.TLSSocket> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
socket.on("error", reject);
|
|
||||||
const newSocket = tls.connect({ socket, host }, () => {
|
|
||||||
socket.removeListener("error", reject);
|
|
||||||
resolve(newSocket);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface XmppClientOptions {
|
|
||||||
host: string;
|
|
||||||
port?: number;
|
|
||||||
username?: string;
|
|
||||||
password?: string;
|
|
||||||
resource?: string;
|
|
||||||
timeout?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class XmppClient extends EventEmitter {
|
|
||||||
private _socket: net.Socket;
|
|
||||||
private _host: string;
|
|
||||||
private _username: string;
|
|
||||||
private _resource: string;
|
|
||||||
private _iqStanzaCallbacks: Map<
|
|
||||||
string,
|
|
||||||
(err: Error, r?: { rawRes: string; res: Element }) => void
|
|
||||||
>;
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
super();
|
|
||||||
this._socket = null;
|
|
||||||
this._host = null;
|
|
||||||
this._username = null;
|
|
||||||
this._resource = null;
|
|
||||||
this._iqStanzaCallbacks = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
static connect(opts: XmppClientOptions): Promise<XmppClient> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let socket = new net.Socket();
|
|
||||||
socket.on("error", reject);
|
|
||||||
|
|
||||||
socket.connect(
|
|
||||||
opts.port || 5222,
|
|
||||||
opts.host,
|
|
||||||
void async function () {
|
|
||||||
socket.removeListener("error", reject);
|
|
||||||
try {
|
|
||||||
let status = 1;
|
|
||||||
while (status) {
|
|
||||||
if (status === STATUS_STARTTLS)
|
|
||||||
socket = await upgradeTls(socket, opts.host);
|
|
||||||
status = await xmppStream(
|
|
||||||
socket,
|
|
||||||
init(
|
|
||||||
socket,
|
|
||||||
opts.host,
|
|
||||||
opts.username,
|
|
||||||
opts.password,
|
|
||||||
opts.resource,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new XmppClient();
|
|
||||||
client._socket = socket;
|
|
||||||
client._host = opts.host;
|
|
||||||
client._username = opts.username;
|
|
||||||
client._resource = opts.resource;
|
|
||||||
socket.on("data", client._onData.bind(client));
|
|
||||||
socket.on("error", client._onError.bind(client));
|
|
||||||
if (opts.timeout)
|
|
||||||
socket.setTimeout(opts.timeout, client.close.bind(client));
|
|
||||||
resolve(client);
|
|
||||||
} catch (err) {
|
|
||||||
socket.destroy();
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
close(): void {
|
|
||||||
this._socket.end("</stream:stream>");
|
|
||||||
this._socket.removeAllListeners("data");
|
|
||||||
this._socket.removeAllListeners("error");
|
|
||||||
this.emit("close");
|
|
||||||
}
|
|
||||||
|
|
||||||
ref(): void {
|
|
||||||
this._socket.ref();
|
|
||||||
}
|
|
||||||
|
|
||||||
unref(): void {
|
|
||||||
this._socket.unref();
|
|
||||||
}
|
|
||||||
|
|
||||||
get host(): string {
|
|
||||||
return this._host;
|
|
||||||
}
|
|
||||||
|
|
||||||
get username(): string {
|
|
||||||
return this._username;
|
|
||||||
}
|
|
||||||
|
|
||||||
get resource(): string {
|
|
||||||
return this._resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onData(chunk: Buffer): void {
|
|
||||||
try {
|
|
||||||
let close = false;
|
|
||||||
let str = chunk.toString("utf8");
|
|
||||||
if (str.endsWith("</stream:stream>")) {
|
|
||||||
str = str.slice(0, -16);
|
|
||||||
close = true;
|
|
||||||
}
|
|
||||||
const xml = parseXml(str);
|
|
||||||
const idx = xml.children.map((c) => c.bodyIndex);
|
|
||||||
for (const [i, c] of xml.children.entries()) {
|
|
||||||
const s = str.slice(idx[i], idx[i + 1]);
|
|
||||||
if (c.name === "iq") {
|
|
||||||
const attrs = parseAttrs(c.attrs);
|
|
||||||
const id = attrs.find((a) => a.name === "id");
|
|
||||||
if (id) {
|
|
||||||
const cb = this._iqStanzaCallbacks.get(id.value);
|
|
||||||
if (cb) cb(null, { rawRes: s, res: c });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.emit("stanza", c, s);
|
|
||||||
}
|
|
||||||
if (close) {
|
|
||||||
this._socket.removeAllListeners("data");
|
|
||||||
this._socket.removeAllListeners("error");
|
|
||||||
this.emit("close");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this._socket.removeAllListeners("data");
|
|
||||||
this._socket.removeAllListeners("error");
|
|
||||||
this.emit("error", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onError(err: Error): void {
|
|
||||||
this._socket.end();
|
|
||||||
this.emit("error", err);
|
|
||||||
for (const cb of this._iqStanzaCallbacks.values()) cb(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
send(msg: string): void {
|
|
||||||
this._socket.write(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendIqStanza(
|
|
||||||
from: string,
|
|
||||||
to: string,
|
|
||||||
type: string,
|
|
||||||
body: string,
|
|
||||||
timeout = 3000,
|
|
||||||
): Promise<{ rawReq: string; rawRes: string; res: Element }> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const id = randomBytes(8).toString("base64");
|
|
||||||
const rawReq = `<iq from="${from}" to="${to}" id="${id}" type="${type}">${body}</iq>`;
|
|
||||||
this.send(rawReq);
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
this._iqStanzaCallbacks.delete(id);
|
|
||||||
reject(
|
|
||||||
new Error("Did not receive IQ stanza response in a timely manner"),
|
|
||||||
);
|
|
||||||
}, timeout);
|
|
||||||
this._iqStanzaCallbacks.set(id, (err, r) => {
|
|
||||||
this._iqStanzaCallbacks.delete(id);
|
|
||||||
clearTimeout(t);
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(Object.assign(r, { rawReq }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
+9548
-7858
File diff suppressed because it is too large.
Load diff
+63
-51
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "genieacs",
|
"name": "genieacs",
|
||||||
"version": "1.2.10",
|
"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",
|
||||||
@@ -12,65 +12,77 @@
|
|||||||
"CWMP",
|
"CWMP",
|
||||||
"ACS"
|
"ACS"
|
||||||
],
|
],
|
||||||
"author": {
|
"author": "Zaid Abdulla <zaid@genieacs.com>",
|
||||||
"name": "GenieACS Inc.",
|
|
||||||
"url": "https://genieacs.com"
|
|
||||||
},
|
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"preferGlobal": true,
|
||||||
"bin": {
|
"directories": {
|
||||||
"genieacs-cwmp": "bin/genieacs-cwmp",
|
"bin": "bin",
|
||||||
"genieacs-fs": "bin/genieacs-fs",
|
"tools": "tools"
|
||||||
"genieacs-nbi": "bin/genieacs-nbi",
|
|
||||||
"genieacs-ui": "bin/genieacs-ui"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breejs/later": "^4.2.0",
|
"iconv-lite": "^0.5.0",
|
||||||
"bson": "^4.7.2",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"espresso-iisojs": "^1.0.8",
|
"koa": "^2.7.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"koa-bodyparser": "^4.2.1",
|
||||||
"ipaddr.js": "^2.0.1",
|
"koa-compress": "^3.0.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"koa-jwt": "^3.6.0",
|
||||||
"koa": "^2.15.1",
|
"koa-router": "^7.4.0",
|
||||||
"koa-bodyparser": "^4.3.0",
|
|
||||||
"koa-compress": "^5.1.0",
|
|
||||||
"koa-jwt": "^4.0.3",
|
|
||||||
"koa-router": "^12.0.0",
|
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"mongodb": "^4.16.0",
|
"later": "^1.2.0",
|
||||||
"parsimmon": "^1.18.1",
|
"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": {
|
||||||
"@types/codemirror": "^5.60.15",
|
"@types/jsonwebtoken": "^8.3.2",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/koa": "^2.0.49",
|
||||||
"@types/koa": "^2.15.0",
|
"@types/koa-compress": "^2.0.9",
|
||||||
"@types/koa-compress": "^4.0.6",
|
"@types/koa-router": "^7.0.42",
|
||||||
"@types/koa-router": "^7.4.8",
|
"@types/koa-static": "^4.0.1",
|
||||||
"@types/koa-static": "^4.0.4",
|
"@types/libxmljs": "^0.18.3",
|
||||||
"@types/mithril": "^2.2.6",
|
"@types/mithril": "^2.0.0",
|
||||||
"@types/node": "^20.11.29",
|
"@types/mongodb": "^3.1.31",
|
||||||
"@types/parsimmon": "^1.10.9",
|
"@types/parsimmon": "^1.10.0",
|
||||||
"@types/seedrandom": "^3.0.8",
|
"@types/seedrandom": "^2.4.28",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
"@types/svgo": "^1.2.1",
|
||||||
"@typescript-eslint/parser": "^7.3.1",
|
"@typescript-eslint/eslint-plugin": "^1.13.0",
|
||||||
"codemirror": "^5.65.16",
|
"@typescript-eslint/parser": "^1.13.0",
|
||||||
"esbuild": "^0.20.2",
|
"ava": "^2.2.0",
|
||||||
"eslint": "^8.57.0",
|
"codemirror": "^5.48.2",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"cssnano": "^4.1.10",
|
||||||
"mithril": "^2.2.2",
|
"eslint": "^6.1.0",
|
||||||
"prettier": "^3.2.5",
|
"eslint-config-prettier": "^6.0.0",
|
||||||
"sql.js": "^1.10.2",
|
"esm": "^3.2.25",
|
||||||
"svgo": "^3.2.0",
|
"mithril": "^2.0.3",
|
||||||
"typescript": "^5.4.2",
|
"postcss": "^7.0.17",
|
||||||
"yaml": "^1.10.2"
|
"postcss-import": "^12.0.1",
|
||||||
|
"postcss-preset-env": "^6.7.0",
|
||||||
|
"prettier": "^1.18.2",
|
||||||
|
"rollup": "^1.17.0",
|
||||||
|
"rollup-plugin-json": "^4.0.0",
|
||||||
|
"rollup-plugin-replace": "^2.2.0",
|
||||||
|
"rollup-plugin-terser": "^5.1.1",
|
||||||
|
"rollup-plugin-typescript": "^1.0.1",
|
||||||
|
"source-map-loader": "^0.2.4",
|
||||||
|
"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": "esbuild --log-level=warning --bundle --platform=node --target=node18 --packages=external --sourcemap=inline --outdir=test test/*.ts && node --test --enable-source-maps test/*.js && rm test/*.js",
|
"test": "ava --verbose --color",
|
||||||
"lint": "esbuild build/lint.ts --bundle --platform=node --target=node16 --packages=external | node",
|
"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\"' _ {} \\;",
|
||||||
"build": "esbuild build/build.ts --bundle --platform=node --target=node12 --packages=external | node"
|
"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": "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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-10
@@ -1,9 +1,8 @@
|
|||||||
import test from "node:test";
|
import ava from "ava";
|
||||||
import assert from "node:assert";
|
import { randomBytes } from "crypto";
|
||||||
import { randomBytes } from "node:crypto";
|
import * as auth from "../lib/auth";
|
||||||
import * as auth from "../lib/auth.ts";
|
|
||||||
|
|
||||||
void test("digest", () => {
|
ava("digest", t => {
|
||||||
const username = "test";
|
const username = "test";
|
||||||
const password = "test";
|
const password = "test";
|
||||||
const uri = "/";
|
const uri = "/";
|
||||||
@@ -15,9 +14,11 @@ void test("digest", () => {
|
|||||||
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);
|
||||||
|
|
||||||
for (const challenge of challenges) {
|
for (const challenge of challenges) {
|
||||||
const wwwAuthHeader = auth.parseWwwAuthenticateHeader(challenge);
|
const wwwAuthHeader = auth.parseWwwAuthenticateHeader(challenge);
|
||||||
const solution = auth.solveDigest(
|
const solution = auth.solveDigest(
|
||||||
@@ -26,10 +27,10 @@ void test("digest", () => {
|
|||||||
uri,
|
uri,
|
||||||
method,
|
method,
|
||||||
body,
|
body,
|
||||||
wwwAuthHeader,
|
wwwAuthHeader
|
||||||
);
|
);
|
||||||
const authHeader = auth.parseAuthorizationHeader(solution);
|
const authHeader = auth.parseAuthorizationHeader(solution);
|
||||||
assert.strictEqual(
|
t.is(
|
||||||
authHeader["response"],
|
authHeader["response"],
|
||||||
auth.digest(
|
auth.digest(
|
||||||
username,
|
username,
|
||||||
@@ -41,8 +42,8 @@ void test("digest", () => {
|
|||||||
authHeader["qop"],
|
authHeader["qop"],
|
||||||
body,
|
body,
|
||||||
authHeader["cnonce"],
|
authHeader["cnonce"],
|
||||||
authHeader["nc"],
|
authHeader["nc"]
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import ava from "ava";
|
||||||
|
import * as common from "../lib/common";
|
||||||
|
|
||||||
|
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 special = [";", "%3B"];
|
||||||
|
const cases = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ProductClass: "TestProductClass",
|
||||||
|
OUI: "TestOUI",
|
||||||
|
SerialNumber: "TestSerialNumber"
|
||||||
|
},
|
||||||
|
"TestOUI-TestProductClass-TestSerialNumber"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
OUI: "TestOUI",
|
||||||
|
SerialNumber: "TestSerialNumber"
|
||||||
|
},
|
||||||
|
"TestOUI-TestSerialNumber"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
OUI: `TestOUIWith${space[0]}_${special[0]}2912`,
|
||||||
|
SerialNumber: `TestSerialNumberWith${space[0]}_${special[0]}2912`
|
||||||
|
},
|
||||||
|
`TestOUIWith${space[1]}_${special[1]}2912-TestSerialNumberWith${space[1]}_${special[1]}2912`
|
||||||
|
]
|
||||||
|
];
|
||||||
|
t.plan(cases.length);
|
||||||
|
for (const c of cases) t.is(common.generateDeviceId(c[0]), c[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
ava("escapeRegExp", t => {
|
||||||
|
t.is(
|
||||||
|
common.escapeRegExp("\\ ^ $ * + ? . ( ) | { } [ ]"),
|
||||||
|
"\\\\ \\^ \\$ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]"
|
||||||
|
);
|
||||||
|
});
|
||||||
-168
@@ -1,168 +0,0 @@
|
|||||||
import test from "node:test";
|
|
||||||
import assert from "node:assert";
|
|
||||||
import { EJSON } from "bson";
|
|
||||||
import { Filter } from "mongodb";
|
|
||||||
import { stringify, parse } from "../lib/common/expression/parser.ts";
|
|
||||||
import { convertOldPrecondition } from "../lib/db/util.ts";
|
|
||||||
import { toMongoQuery } from "../lib/db/synth.ts";
|
|
||||||
|
|
||||||
void test("convertOldPrecondition", () => {
|
|
||||||
const tests = [
|
|
||||||
[{}, "true"],
|
|
||||||
[{ test: "test" }, 'test = "test"'],
|
|
||||||
[{ test: { $eq: "test" } }, 'test = "test"'],
|
|
||||||
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'],
|
|
||||||
[{ test: { $gte: "test" } }, 'test >= "test"'],
|
|
||||||
[{ "Tags.test": true }, "Tags.test IS NOT NULL"],
|
|
||||||
[{ "Tags.test": false }, "Tags.test IS NULL"],
|
|
||||||
[{ "Tags.test": { $exists: true } }, "Tags.test IS NOT NULL"],
|
|
||||||
[{ "Tags.test": { $exists: false } }, "Tags.test IS NULL"],
|
|
||||||
[{ "Tags.test": { $ne: true } }, "Tags.test IS NULL"],
|
|
||||||
[{ "Tags.test": { $ne: false } }, "Tags.test IS NOT NULL"],
|
|
||||||
[{ "Tags.test": { $eq: true } }, "Tags.test IS NOT NULL"],
|
|
||||||
[{ "Tags.test": { $eq: false } }, "Tags.test IS NULL"],
|
|
||||||
[{ _tags: "test" }, "Tags.test IS NOT NULL"],
|
|
||||||
[{ _tags: { $ne: "test" } }, "Tags.test IS NULL"],
|
|
||||||
[{ _tags: { $eq: "test" } }, "Tags.test IS NOT NULL"],
|
|
||||||
[
|
|
||||||
{ $and: [{ test: "test" }, { test: { $ne: "test" } }] },
|
|
||||||
'test = "test" AND (test <> "test" OR test IS NULL)',
|
|
||||||
],
|
|
||||||
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
|
|
||||||
[
|
|
||||||
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
|
|
||||||
'test = "test" OR (test <> "test" OR test IS NULL)',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ test: { $gte: "test1", $ne: "test2" } },
|
|
||||||
'test >= "test1" AND (test <> "test2" OR test IS NULL)',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{ test: "test", test2: { $ne: "test2" } },
|
|
||||||
'test = "test" AND (test2 <> "test2" OR test2 IS NULL)',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
const shouldFailTests = [
|
|
||||||
[{ test: { $gee: "test" } }, "Operator $gee not supported"],
|
|
||||||
[{ test: [] }, "Invalid type"],
|
|
||||||
[{ "Tags.test": { $gt: true } }, "Invalid tag query"],
|
|
||||||
[{ _tags: [] }, "Invalid type"],
|
|
||||||
[{ _tags: { $gt: "test" } }, "Invalid tag query"],
|
|
||||||
[{ $nor: [] }, "Operator $nor not supported"],
|
|
||||||
] as [Filter<unknown>, string][];
|
|
||||||
|
|
||||||
for (const t of tests) {
|
|
||||||
assert.strictEqual(
|
|
||||||
stringify(convertOldPrecondition(t[0] as Record<string, unknown>)),
|
|
||||||
t[1],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const t of shouldFailTests) {
|
|
||||||
const func = (): void => {
|
|
||||||
convertOldPrecondition(t[0]);
|
|
||||||
};
|
|
||||||
assert.throws(func, new Error(t[1]));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
void test("toMongoQuery", async () => {
|
|
||||||
const queries: [string, Filter<unknown> | false][] = [
|
|
||||||
["true", {}],
|
|
||||||
["Tags.tag1 = true", { _tags: { $eq: "tag1" } }],
|
|
||||||
["Tags.tag1 <> false", { _tags: { $eq: "tag1" } }],
|
|
||||||
["Tags.tag1 IS NULL", { _tags: { $ne: "tag1" } }],
|
|
||||||
["Tags.tag1 = 123", false],
|
|
||||||
["Param1 = 'value1'", { "Param1._value": { $eq: "value1" } }],
|
|
||||||
[
|
|
||||||
"Param1 <> 'value1'",
|
|
||||||
{
|
|
||||||
"Param1._value": { $ne: "value1" },
|
|
||||||
$and: [{ "Param1._value": { $ne: null } }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Param1 <> 1657844103524",
|
|
||||||
{
|
|
||||||
"Param1._value": { $ne: 1657844103524 },
|
|
||||||
|
|
||||||
$and: [
|
|
||||||
{
|
|
||||||
"Param1._value": { $ne: { $date: "2022-07-15T00:15:03.524Z" } },
|
|
||||||
},
|
|
||||||
{ "Param1._value": { $ne: null } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Param1 = 1657844103524",
|
|
||||||
{
|
|
||||||
$or: [
|
|
||||||
{ "Param1._value": { $eq: { $date: "2022-07-15T00:15:03.524Z" } } },
|
|
||||||
{ "Param1._value": { $eq: 1657844103524 } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
["Param1 > 'value'", { "Param1._value": { $gt: "value" } }],
|
|
||||||
["Param1 IS NOT NULL", { "Param1._value": { $ne: null } }],
|
|
||||||
[
|
|
||||||
"Param1 LIKE 'value'",
|
|
||||||
{
|
|
||||||
"Param1._value": {
|
|
||||||
$regularExpression: { options: "s", pattern: "^value$" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
[
|
|
||||||
"LOWER(Param1) LIKE 'value'",
|
|
||||||
{
|
|
||||||
"Param1._value": {
|
|
||||||
$regularExpression: { options: "is", pattern: "^value$" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Param1 <> 'value2' OR NOT (Param2 = 'value1' OR Param1 < 'value2')",
|
|
||||||
{
|
|
||||||
$or: [
|
|
||||||
{
|
|
||||||
"Param2._value": { $ne: null },
|
|
||||||
$and: [{ "Param2._value": { $ne: "value1" } }],
|
|
||||||
"Param1._value": { $eq: "value2" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Param1._value": { $ne: "value2" },
|
|
||||||
$and: [{ "Param1._value": { $ne: null } }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Param1 <> 'value2' OR Param1 IS NULL",
|
|
||||||
{ "Param1._value": { $ne: "value2" } },
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [expStr, expect] of queries) {
|
|
||||||
const exp = parse(expStr);
|
|
||||||
let query = toMongoQuery(exp, "devices");
|
|
||||||
if (query) query = EJSON.serialize(query);
|
|
||||||
assert.deepStrictEqual(query, expect);
|
|
||||||
}
|
|
||||||
|
|
||||||
const failQueries: [any, string][] = [
|
|
||||||
["Param1 = Param2", "Right-hand operand must be a literal value"],
|
|
||||||
["Param1 LIKE Param2", "Right-hand operand of 'LIKE' must be a string"],
|
|
||||||
["NOW() = 1", "Left-hand operand must be a parameter"],
|
|
||||||
["param{param2} = 1", "Left-hand operand must be a parameter"],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [expStr, err] of failQueries) {
|
|
||||||
const exp = parse(expStr);
|
|
||||||
assert.throws(() => toMongoQuery(exp, "devices"), {
|
|
||||||
message: err,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
+38
-38
@@ -1,97 +1,97 @@
|
|||||||
import test from "node:test";
|
import ava from "ava";
|
||||||
import assert from "node:assert";
|
import Path from "../lib/common/path";
|
||||||
import Path from "../lib/common/path.ts";
|
import * as device from "../lib/device";
|
||||||
import * as device from "../lib/device.ts";
|
import PathSet from "../lib/common/path-set";
|
||||||
import PathSet from "../lib/common/path-set.ts";
|
import VersionedMap from "../lib/versioned-map";
|
||||||
import VersionedMap from "../lib/versioned-map.ts";
|
import { Attributes } from "../lib/types";
|
||||||
import { Attributes } from "../lib/types.ts";
|
|
||||||
|
|
||||||
void test("getAliasDeclarations", () => {
|
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);
|
||||||
|
|
||||||
const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"];
|
const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"];
|
||||||
|
|
||||||
|
t.plan(24);
|
||||||
for (const [i, d] of decs.entries()) {
|
for (const [i, d] of decs.entries()) {
|
||||||
assert.strictEqual(d.path.toString(), expected[i]);
|
t.is(d.path.toString(), expected[i]);
|
||||||
assert.strictEqual(d.pathGet, 99);
|
t.is(d.pathGet, 99);
|
||||||
assert.strictEqual(d.pathSet, null);
|
t.is(d.pathSet, null);
|
||||||
assert.deepStrictEqual(d.attrGet, i ? { value: 99 } : null);
|
t.deepEqual(d.attrGet, i ? { value: 99 } : null);
|
||||||
assert.strictEqual(d.attrSet, null);
|
t.is(d.attrSet, null);
|
||||||
assert.strictEqual(d.defer, true);
|
t.is(d.defer, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
void test("unpack", () => {
|
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[];
|
||||||
unpacked = device.unpack(
|
unpacked = device.unpack(
|
||||||
deviceData,
|
deviceData,
|
||||||
Path.parse("a.[b:b,c:c].a.[b:b1,c:c1].a"),
|
Path.parse("a.[b:b,c:c].a.[b:b1,c:c1].a")
|
||||||
);
|
);
|
||||||
assert.strictEqual(unpacked.length, 2);
|
t.is(unpacked.length, 2);
|
||||||
assert.strictEqual(unpacked[0].toString(), "a.1.a.1.a");
|
t.is(unpacked[0].toString(), "a.1.a.1.a");
|
||||||
assert.strictEqual(unpacked[1].toString(), "a.2.a.1.a");
|
t.is(unpacked[1].toString(), "a.2.a.1.a");
|
||||||
|
|
||||||
unpacked = device.unpack(deviceData, Path.parse("a.*.a.[b:c1,c:b1].a"));
|
unpacked = device.unpack(deviceData, Path.parse("a.*.a.[b:c1,c:b1].a"));
|
||||||
assert.strictEqual(unpacked.length, 1);
|
t.is(unpacked.length, 1);
|
||||||
assert.strictEqual(unpacked[0].toString(), "a.2.a.2.a");
|
t.is(unpacked[0].toString(), "a.2.a.2.a");
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import ava from "ava";
|
||||||
|
import * as mongodbFunctions from "../lib/mongodb-functions";
|
||||||
|
import { stringify } from "../lib/common/expression-parser";
|
||||||
|
|
||||||
|
ava("mongoQueryToFilter", t => {
|
||||||
|
const tests = [
|
||||||
|
[{}, "true"],
|
||||||
|
[{ test: "test" }, 'test = "test"'],
|
||||||
|
[{ test: { $eq: "test" } }, 'test = "test"'],
|
||||||
|
[{ test: { $ne: "test" } }, 'test <> "test"'],
|
||||||
|
[{ test: { $gte: "test" } }, 'test >= "test"'],
|
||||||
|
[{ "Tags.test": true }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ "Tags.test": false }, "Tags.test IS NULL"],
|
||||||
|
[{ "Tags.test": { $exists: true } }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ "Tags.test": { $exists: false } }, "Tags.test IS NULL"],
|
||||||
|
[{ "Tags.test": { $ne: true } }, "Tags.test IS NULL"],
|
||||||
|
[{ "Tags.test": { $ne: false } }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ "Tags.test": { $eq: true } }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ "Tags.test": { $eq: false } }, "Tags.test IS NULL"],
|
||||||
|
[{ _tags: "test" }, "Tags.test IS NOT NULL"],
|
||||||
|
[{ _tags: { $ne: "test" } }, "Tags.test IS NULL"],
|
||||||
|
[{ _tags: { $eq: "test" } }, "Tags.test IS NOT NULL"],
|
||||||
|
[
|
||||||
|
{ $and: [{ test: "test" }, { test: { $ne: "test" } }] },
|
||||||
|
'test = "test" AND test <> "test"'
|
||||||
|
],
|
||||||
|
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
|
||||||
|
[
|
||||||
|
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
|
||||||
|
'test = "test" OR test <> "test"'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ test: { $gte: "test1", $ne: "test2" } },
|
||||||
|
'test >= "test1" AND test <> "test2"'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ test: "test", test2: { $ne: "test2" } },
|
||||||
|
'test = "test" AND test2 <> "test2"'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
const shouldFailTests = [
|
||||||
|
[{ test: { $gee: "test" } }, "Operator $gee not supported"],
|
||||||
|
[{ test: [] }, "Invalid type"],
|
||||||
|
[{ "Tags.test": { $gt: true } }, "Invalid tag query"],
|
||||||
|
[{ _tags: [] }, "Invalid type"],
|
||||||
|
[{ _tags: { $gt: "test" } }, "Invalid tag query"],
|
||||||
|
[{ $nor: [] }, "Operator $nor not supported"]
|
||||||
|
];
|
||||||
|
|
||||||
|
t.plan(tests.length + 2 * shouldFailTests.length);
|
||||||
|
for (const test of tests)
|
||||||
|
t.is(stringify(mongodbFunctions.mongoQueryToFilter(test[0])), test[1]);
|
||||||
|
|
||||||
|
for (const test of shouldFailTests) {
|
||||||
|
const func = (): void => {
|
||||||
|
mongodbFunctions.mongoQueryToFilter(test[0]);
|
||||||
|
};
|
||||||
|
const error = t.throws(func, Error);
|
||||||
|
t.is(error.message, test[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import test from "node:test";
|
|
||||||
import assert from "node:assert";
|
|
||||||
import initSqlJs from "sql.js/dist/sql-asm.js";
|
|
||||||
import { parse, stringify } from "../lib/common/expression/parser.ts";
|
|
||||||
import {
|
|
||||||
bookmarkToExpression,
|
|
||||||
paginate,
|
|
||||||
toBookmark,
|
|
||||||
} from "../lib/common/expression/pagination.ts";
|
|
||||||
import { Expression } from "../lib/types.ts";
|
|
||||||
import { covers, minimize } from "../lib/common/expression/synth.ts";
|
|
||||||
|
|
||||||
const VALUES = [null, -1, false, "a"];
|
|
||||||
const PARAMS = ["param1", "param2"];
|
|
||||||
|
|
||||||
let db;
|
|
||||||
|
|
||||||
async function query(filter: string): Promise<{ id: string }[]> {
|
|
||||||
if (!db) {
|
|
||||||
const sql = await initSqlJs();
|
|
||||||
db = new sql.Database();
|
|
||||||
|
|
||||||
db.run(`CREATE TABLE test (id INTEGER PRIMARY KEY, ${PARAMS.join(", ")})`);
|
|
||||||
|
|
||||||
const stmt = db.prepare(
|
|
||||||
`INSERT INTO test (${PARAMS.join(", ")}) VALUES (${PARAMS.map(
|
|
||||||
() => "?",
|
|
||||||
).join(", ")})`,
|
|
||||||
);
|
|
||||||
const count = VALUES.length ** PARAMS.length;
|
|
||||||
for (let i = 0; i < count; ++i) {
|
|
||||||
const values: (boolean | number | string)[] = [];
|
|
||||||
for (let j = 0; j < PARAMS.length; ++j)
|
|
||||||
values.push(VALUES[Math.trunc(i / VALUES.length ** j) % VALUES.length]);
|
|
||||||
stmt.run(values);
|
|
||||||
}
|
|
||||||
stmt.free();
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = db.exec(`SELECT * FROM test WHERE ${filter}`);
|
|
||||||
if (!res.length) return [];
|
|
||||||
return res[0].values.map((row) =>
|
|
||||||
Object.fromEntries(row.map((v, i) => [res[0].columns[i], v])),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAllSortOrders(columns: string[]): Array<Record<string, number>> {
|
|
||||||
const sortOrders: Array<Record<string, number>> = [];
|
|
||||||
|
|
||||||
function generateOrders(
|
|
||||||
remaining: string[],
|
|
||||||
current: Record<string, number>,
|
|
||||||
): void {
|
|
||||||
if (remaining.length === 0) {
|
|
||||||
sortOrders.push({ ...current });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const column of remaining) {
|
|
||||||
const newRemaining = remaining.filter((c) => c !== column);
|
|
||||||
current[column] = -1;
|
|
||||||
generateOrders(newRemaining, current);
|
|
||||||
current[column] = 1;
|
|
||||||
generateOrders(newRemaining, current);
|
|
||||||
delete current[column];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateOrders(columns, {});
|
|
||||||
return sortOrders;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testPaginate(
|
|
||||||
q1: Expression,
|
|
||||||
q2: Expression,
|
|
||||||
sort: Record<string, number>,
|
|
||||||
): Promise<void> {
|
|
||||||
const orderBy = Object.entries(sort)
|
|
||||||
.map(([k, v]) => `${k} ${v > 0 ? "ASC" : "DESC"}`)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
const allMatches = await query(`${stringify(q2)} ORDER BY ${orderBy}`);
|
|
||||||
const [fulfilled, diff] = paginate(q1, q2, sort);
|
|
||||||
assert.ok(covers(q1, fulfilled));
|
|
||||||
|
|
||||||
const fulfilledMatches = await query(
|
|
||||||
`${stringify(fulfilled)} ORDER BY ${orderBy}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const diffMatches = await query(`${stringify(diff)} ORDER BY ${orderBy}`);
|
|
||||||
assert.deepStrictEqual(allMatches, [...fulfilledMatches, ...diffMatches]);
|
|
||||||
|
|
||||||
if (allMatches.length === fulfilledMatches.length) return;
|
|
||||||
|
|
||||||
const nextMatches = allMatches.slice(
|
|
||||||
fulfilledMatches.length,
|
|
||||||
fulfilledMatches.length + 1,
|
|
||||||
);
|
|
||||||
const bookmark = toBookmark(sort, nextMatches[nextMatches.length - 1]);
|
|
||||||
const bookmarkFilter = bookmarkToExpression(bookmark, sort);
|
|
||||||
const capped = ["AND", q2, bookmarkFilter];
|
|
||||||
|
|
||||||
assert.ok(!covers(q1, capped));
|
|
||||||
const cappedMatches = await query(
|
|
||||||
`(${stringify(capped)}) ORDER BY ${orderBy}`,
|
|
||||||
);
|
|
||||||
assert.deepStrictEqual(cappedMatches, [...fulfilledMatches, ...nextMatches]);
|
|
||||||
const min = minimize(["OR", q1, capped]);
|
|
||||||
await testPaginate(min, q2, sort);
|
|
||||||
}
|
|
||||||
|
|
||||||
void test("paginate", async () => {
|
|
||||||
const cases: [string, string][] = [["param2 < 'a'", "param1 >= 'a'"]];
|
|
||||||
const params = ["id", ...PARAMS];
|
|
||||||
const sortOrders = getAllSortOrders(params);
|
|
||||||
|
|
||||||
for (const [q1, q2] of cases)
|
|
||||||
for (const sort of sortOrders)
|
|
||||||
await testPaginate(parse(q1), parse(q2), sort);
|
|
||||||
});
|
|
||||||
+31
-32
@@ -1,35 +1,34 @@
|
|||||||
import test from "node:test";
|
import ava from "ava";
|
||||||
import assert from "node:assert";
|
import Path from "../lib/common/path";
|
||||||
import Path from "../lib/common/path.ts";
|
import PathSet from "../lib/common/path-set";
|
||||||
import PathSet from "../lib/common/path-set.ts";
|
|
||||||
|
|
||||||
void test("depth", () => {
|
ava("depth", t => {
|
||||||
const pathSet = new PathSet();
|
const pathSet = new PathSet();
|
||||||
assert.strictEqual(pathSet.depth, 0);
|
t.is(pathSet.depth, 0);
|
||||||
pathSet.add(Path.parse(""));
|
pathSet.add(Path.parse(""));
|
||||||
assert.strictEqual(pathSet.depth, 1);
|
t.is(pathSet.depth, 1);
|
||||||
pathSet.add(Path.parse("a"));
|
pathSet.add(Path.parse("a"));
|
||||||
assert.strictEqual(pathSet.depth, 2);
|
t.is(pathSet.depth, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
void test("add", () => {
|
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"));
|
||||||
assert.strictEqual(pathSet.find(Path.parse("a"), true, true, 99).length, 1);
|
t.is(pathSet.find(Path.parse("a"), true, true, 99).length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
void test("get", () => {
|
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"));
|
||||||
pathSet.add(Path.parse("*.*"));
|
pathSet.add(Path.parse("*.*"));
|
||||||
|
|
||||||
assert.strictEqual(pathSet.get(Path.parse("a.*")).toString(), "a.*");
|
t.is(pathSet.get(Path.parse("a.*")).toString(), "a.*");
|
||||||
assert.strictEqual(pathSet.get(Path.parse("*.a")), null);
|
t.is(pathSet.get(Path.parse("*.a")), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
void test("find", () => {
|
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,33 +36,33 @@ void test("find", () => {
|
|||||||
pathSet.add(Path.parse("*.a"));
|
pathSet.add(Path.parse("*.a"));
|
||||||
pathSet.add(Path.parse("*.*"));
|
pathSet.add(Path.parse("*.*"));
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
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"]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
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", "*.*"]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
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.*"]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
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"]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
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.*", "*.*"]
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepStrictEqual(
|
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", "*.*"]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user