Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
444183da0a | ||
|
|
c02f667623 | ||
|
|
25ea0c8b64 | ||
|
|
2789961596 | ||
|
|
d91be02599 | ||
|
|
5806fe2df6 | ||
|
|
eb5caa6b5a | ||
|
|
ffbcf2bbc0 | ||
|
|
6f29267266 | ||
|
|
b0d50b07e7 | ||
|
|
c06538f9cc | ||
|
|
3d0ac9b415 | ||
|
|
dabf1b2f43 | ||
|
|
7941d929d2 | ||
|
|
7ccdbace70 | ||
|
|
344f0ec5e0 | ||
|
|
4b533d0aad | ||
|
|
2b37f11e2e | ||
|
|
adaf801d6e | ||
|
|
94756e463f | ||
|
|
3c55306e22 | ||
|
|
aa12b7c724 | ||
|
|
8f827997cd | ||
|
|
c9a4be0475 | ||
|
|
fa95d7a310 | ||
|
|
d299ef3a59 | ||
|
|
2c606b268e | ||
|
|
1b86f41ad7 | ||
|
|
c4754a9b55 | ||
|
|
f24111ea9f | ||
|
|
8f662fb152 | ||
|
|
dd722ba6e0 | ||
|
|
88a84788a5 | ||
|
|
73baffc3b9 | ||
|
|
0188ac860b | ||
|
|
7f295beeec | ||
|
|
2ac536bf8f | ||
|
|
f63e357b29 | ||
|
|
ceec3f2cc8 | ||
|
|
2b60feb2f4 | ||
|
|
9203417686 | ||
|
|
8477cd1cfa | ||
|
|
12c958dc03 | ||
|
|
fdb81b7a43 | ||
|
|
529386a9b6 | ||
|
|
98698b775c | ||
|
|
9706026ea5 | ||
|
|
9e1d7eca9b | ||
|
|
d48deaaf47 | ||
|
|
ec770b4e1b | ||
|
|
c3c7ab72b2 | ||
|
|
ca83704958 | ||
|
|
bc2d258f9c | ||
|
|
7ae3bcdc0f | ||
|
|
53bb4d3552 | ||
|
|
595f0eddf1 | ||
|
|
0861b2db40 | ||
|
|
f4692faed6 | ||
|
|
cc885e7d04 | ||
|
|
cd8d264f03 | ||
|
|
1e10d394bb | ||
|
|
359eca7bbc | ||
|
|
088e25cbb0 | ||
|
|
1d02893692 | ||
|
|
da01793ac5 | ||
|
|
cce85e48ed | ||
|
|
be90214bf9 | ||
|
|
6ce91df127 | ||
|
|
1a53a320da | ||
|
|
cfe279b511 | ||
|
|
08cfe4171c | ||
|
|
6c37e88ab1 | ||
|
|
8b27794cc7 | ||
|
|
9cf18503ee | ||
|
|
3b36cefbca | ||
|
|
02e09dfdab | ||
|
|
63a75cc765 | ||
|
|
630697a0cb | ||
|
|
f6aa444a23 | ||
|
|
139dc5410e | ||
|
|
e4582695b7 | ||
|
|
0064c2b951 | ||
|
|
47c2d6ee6f | ||
|
|
f290ce696f | ||
|
|
556424d4d6 | ||
|
|
7e507125c4 | ||
|
|
2ca0caa258 | ||
|
|
478a0af781 | ||
|
|
9eab36cd73 | ||
|
|
8f69b74a89 | ||
|
|
fccda8d726 | ||
|
|
111cb106d0 | ||
|
|
5b17d1b2ca | ||
|
|
02dfe4b554 | ||
|
|
688811317c | ||
|
|
4871613c67 | ||
|
|
540898de21 | ||
|
|
e9a4676f5c | ||
|
|
12197f3b2a | ||
|
|
bdad2f9a32 | ||
|
|
2e32b5d2a6 | ||
|
|
51e160560c | ||
|
|
a60a212a10 | ||
|
|
a692dddd50 | ||
|
|
f62a42ada4 | ||
|
|
97ed9797c5 | ||
|
|
405f9d6a0f | ||
|
|
995a232387 | ||
|
|
1e6ab38ede | ||
|
|
ca36bf1b64 | ||
|
|
79cb0b02d0 | ||
|
|
e3acaf925d | ||
|
|
44748e5769 | ||
|
|
6cca59c666 | ||
|
|
7594883404 | ||
|
|
cc89fd92d7 | ||
|
|
134ca46dde | ||
|
|
03c3c456eb | ||
|
|
edcee33780 | ||
|
|
5571ce96b4 | ||
|
|
a40e4b863b | ||
|
|
80269ec6bd | ||
|
|
5f44f6597c | ||
|
|
13ff0724e7 | ||
|
|
f613ba25c2 | ||
|
|
c950c73e76 | ||
|
|
8d8e122633 |
No files matched your search
@@ -1,2 +0,0 @@
|
|||||||
!/bin/.*.ts
|
|
||||||
!/tools/.*.ts
|
|
||||||
+15
-5
@@ -8,19 +8,29 @@
|
|||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"plugins": ["@typescript-eslint"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "prettier/@typescript-eslint"],
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-shadow": ["error", { "allow": ["err"] }],
|
"no-shadow": ["error", { "allow": ["err"] }],
|
||||||
"handle-callback-err": "error",
|
"handle-callback-err": "error",
|
||||||
"prefer-arrow-callback": "error",
|
"prefer-arrow-callback": "error",
|
||||||
"curly": ["error", "multi-or-nest", "consistent"],
|
"curly": ["error", "multi-or-nest", "consistent"],
|
||||||
"no-buffer-constructor": "error",
|
"no-buffer-constructor": "error",
|
||||||
"prefer-const": ["error", {"destructuring": "all"}],
|
"prefer-const": ["error", { "destructuring": "all" }],
|
||||||
"eqeqeq": ["error", "always", {"null": "ignore"}],
|
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
||||||
"no-return-await": "error",
|
"no-return-await": "error",
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
"@typescript-eslint/no-use-before-define": ["error", { "functions": false }],
|
"@typescript-eslint/no-use-before-define": [
|
||||||
"@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }],
|
"error",
|
||||||
|
{ "functions": false }
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": [
|
||||||
|
"error",
|
||||||
|
{ "allowExpressions": true }
|
||||||
|
],
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
"@typescript-eslint/no-floating-promises": "error",
|
||||||
"require-atomic-updates": 0,
|
"require-atomic-updates": 0,
|
||||||
"no-prototype-builtins": 0
|
"no-prototype-builtins": 0
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
*~
|
*~
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
bin/.*.ts
|
|
||||||
tools/.*.ts
|
|
||||||
docs/_build
|
docs/_build
|
||||||
-25
@@ -1,25 +0,0 @@
|
|||||||
*~
|
|
||||||
.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/.*
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
dist
|
||||||
|
npm-shrinkwrap.json
|
||||||
+191
-72
@@ -1,20 +1,138 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 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)
|
## 1.2.3 (2020-10-26)
|
||||||
|
|
||||||
- New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly
|
- New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly
|
||||||
report writable parameters as non-writable. When set to true, the scripts will
|
report writable parameters as non-writable. When set to true, the scripts will
|
||||||
no longer respect the 'writable' attribute of the CPE parameters and will send
|
no longer respect the 'writable' attribute of the CPE parameters and will send
|
||||||
a SetParamteerValues, AddObject, or DeleteObject request anyway.
|
a SetParamteerValues, AddObject, or DeleteObject request anyway.
|
||||||
- Tags no longer restrict what characters are allowed. Any character other than
|
- Tags no longer restrict what characters are allowed. Any character other than
|
||||||
alphanumeric characters, hyphen, or underscore is now encoded in the data
|
alphanumeric characters, hyphen, or underscore is now encoded in the data
|
||||||
model (i.e. Tags.\<tag>) using its hex value preceded by "0x".
|
model (i.e. Tags.\<tag>) using its hex value preceded by "0x".
|
||||||
- Ask for a confirmation before closing a pop-up dialog with unsaved changes.
|
- Ask for a confirmation before closing a pop-up dialog with unsaved changes.
|
||||||
- Better XML validation to avoid crashes caused by invalid CPE requests.
|
- Better XML validation to avoid crashes caused by invalid CPE requests.
|
||||||
- Fix confusing 404 error message when the user attempts to modify a resource
|
- Fix confusing 404 error message when the user attempts to modify a resource
|
||||||
when they don't have the necessary permissions.
|
when they don't have the necessary permissions.
|
||||||
- Fix a rare issue where genieacs-cwmp stops accepting new connections after
|
- Fix a rare issue where genieacs-cwmp stops accepting new connections after
|
||||||
running for a few weeks.
|
running for a few weeks.
|
||||||
- Fix exception when IS NULL operator is used in certain situations.
|
- Fix exception when IS NULL operator is used in certain situations.
|
||||||
|
|
||||||
## 1.2.2 (2020-10-03)
|
## 1.2.2 (2020-10-03)
|
||||||
@@ -24,81 +142,80 @@ running for a few weeks.
|
|||||||
- Fix exception that can happen and persist after a Download request.
|
- Fix exception that can happen and persist after a Download request.
|
||||||
- Fix validation bug preventing running refreshObject task on data model root.
|
- Fix validation bug preventing running refreshObject task on data model root.
|
||||||
- Fix invalid arguments fault in refresh preset configuration when upgrading
|
- Fix invalid arguments fault in refresh preset configuration when upgrading
|
||||||
from v1.1.
|
from v1.1.
|
||||||
|
|
||||||
## 1.2.1 (2020-09-08)
|
## 1.2.1 (2020-09-08)
|
||||||
|
|
||||||
- Fix bug causing faults to not be displayed in the UI.
|
- Fix bug causing faults to not be displayed in the UI.
|
||||||
- Fix bug where deleting objects does not get reflected immediately in the UI.
|
- Fix bug where deleting objects does not get reflected immediately in the UI.
|
||||||
- Improve conversion between filters written in the expression format and
|
- Improve conversion between filters written in the expression format and
|
||||||
MongoDB queries. There should now be fewer edge cases where the two are not
|
MongoDB queries. There should now be fewer edge cases where the two are not
|
||||||
equisatisfiable.
|
equisatisfiable.
|
||||||
|
|
||||||
## 1.2.0 (2020-09-01)
|
## 1.2.0 (2020-09-01)
|
||||||
|
|
||||||
- Support GetParameterAttributes and SetParameterAttributes TR-069 methods.
|
- Support GetParameterAttributes and SetParameterAttributes TR-069 methods.
|
||||||
- Support CASE statement and COALESCE function in expressions.
|
- Support CASE statement and COALESCE function in expressions.
|
||||||
- Provision arguments can now be a list of expressions that are dynamically
|
- Provision arguments can now be a list of expressions that are dynamically
|
||||||
evaluated.
|
evaluated.
|
||||||
- Support Forwarded HTTP header to display in the logs the correct IP of CPEs
|
- Support Forwarded HTTP header to display in the logs the correct IP of CPEs
|
||||||
behind a reverse proxy. Must be configured using FORWARDED_HEADER option.
|
behind a reverse proxy. Must be configured using FORWARDED_HEADER option.
|
||||||
- Config expressions can now access all available device parameters, not only
|
- Config expressions can now access all available device parameters, not only
|
||||||
serial number, product class, and OUI.
|
serial number, product class, and OUI.
|
||||||
- Use relative URLs throughout the UI to allow serving from a subdirectory
|
- Use relative URLs throughout the UI to allow serving from a subdirectory using
|
||||||
using a reverse proxy.
|
a reverse proxy.
|
||||||
- Make Date.parse() and Date.UTC() available to provision scripts.
|
- Make Date.parse() and Date.UTC() available to provision scripts.
|
||||||
- libxmljs has been entirely removed in favor of our bespoke XML parser.
|
- libxmljs has been entirely removed in favor of our bespoke XML parser.
|
||||||
- Removed the config option CWMP_KEEP_ALIVE_TIMEOUT. SESSION_TIMEOUT is now
|
- Removed the config option CWMP_KEEP_ALIVE_TIMEOUT. SESSION_TIMEOUT is now used
|
||||||
used to determine the TCP connection timeout.
|
to determine the TCP connection timeout.
|
||||||
- The all-parameters component now limits the number of parameters displayed
|
- The all-parameters component now limits the number of parameters displayed for
|
||||||
for better performance.
|
better performance.
|
||||||
- The process genieacs-cwmp is now much less likely to throw exceptions as a
|
- The process genieacs-cwmp is now much less likely to throw exceptions as a
|
||||||
result of invalid requests from CPE.
|
result of invalid requests from CPE.
|
||||||
- A large number of bug fixes and stability improvements.
|
- A large number of bug fixes and stability improvements.
|
||||||
|
|
||||||
## 1.2.0-beta.0 (2019-07-30)
|
## 1.2.0-beta.0 (2019-07-30)
|
||||||
|
|
||||||
- A brand new UI superseding genieacs-gui.
|
- A brand new UI superseding genieacs-gui.
|
||||||
- New initialization wizard on first run.
|
- New initialization wizard on first run.
|
||||||
- New expression/query language used in search filters and preset
|
- New expression/query language used in search filters and preset preconditions.
|
||||||
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 as
|
variables. Other general configuration options are stored in the database so
|
||||||
to not require service restart for changes to take effect.
|
as 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 NodeJs and MongoDB are 10.x and up and 2.6 and up
|
- Supported versions of Node.js 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.
|
||||||
@@ -108,66 +225,68 @@ of returning incorrect results.
|
|||||||
- 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: {declare
|
'TIMESTAMPS' and 'VALUES' variables. The content of the args array is:
|
||||||
timestamps}, {declare values}, {current timestamps}, {current values}.
|
{declare timestamps}, {declare values}, {current timestamps}, {current
|
||||||
|
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 entities.
|
and virtual parameter scripts to facilitate integration with external
|
||||||
- Support for UDP/STUN based connection requests for reaching devices behind
|
entities.
|
||||||
NAT (TR-069 Annex G).
|
- Support for UDP/STUN based connection requests for reaching devices behind NAT
|
||||||
|
(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.
|
||||||
@@ -176,19 +295,19 @@ presets in that channel.
|
|||||||
- 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 than
|
access log (stdout). Also added config options to dump logs to files rather
|
||||||
standard streams.
|
than 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 now
|
data model if available. config/auth.js is still supported as a fallback and
|
||||||
supports an optional callback argument.
|
now 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
|
||||||
+10
-9
@@ -5,8 +5,8 @@ tracker is strictly for bug reports.
|
|||||||
|
|
||||||
# Issues
|
# Issues
|
||||||
|
|
||||||
We use GitHub issues to track bugs. Please ensure your bug description is
|
We use GitHub issues to track bugs. Please ensure your bug description is clear
|
||||||
clear and has sufficient instructions to be able to reproduce the issue.
|
and has sufficient instructions to be able to reproduce the issue.
|
||||||
|
|
||||||
If you face interoperability issues with your CPE, more often than not it's a
|
If you face interoperability issues with your CPE, more often than not it's a
|
||||||
device related issue. Please consult the [forum] before submitting an Issue.
|
device related issue. Please consult the [forum] before submitting an Issue.
|
||||||
@@ -26,13 +26,14 @@ 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
|
distribute your contributions and such derivative works under the AGPLv3 license
|
||||||
license or any other license terms, including, but not limited to, proprietary
|
or any other license terms, including, but not limited to, proprietary or
|
||||||
or commercial license terms.
|
commercial license terms.
|
||||||
|
|
||||||
You confirm that you own or have rights to distribute and sublicense the
|
You confirm that you own or have rights to distribute and sublicense the source
|
||||||
source code contained therein, and that your content does not infringe upon
|
code contained therein, and that your content does not infringe upon the
|
||||||
the intellectual property rights of a third party.
|
intellectual property rights of a third party.
|
||||||
|
|
||||||
[commit-guidelines]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
[commit-guidelines]:
|
||||||
|
http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
||||||
[forum]: https://forum.genieacs.com
|
[forum]: https://forum.genieacs.com
|
||||||
@@ -17,10 +17,10 @@ supported versions are:
|
|||||||
|
|
||||||
Install GenieACS from NPM:
|
Install GenieACS from NPM:
|
||||||
|
|
||||||
sudo npm install -g --unsafe-perm genieacs
|
sudo npm install -g genieacs
|
||||||
|
|
||||||
To build from source instead, clone this repo or download the source archive
|
To build from source instead, clone this repo or download the source archive
|
||||||
then *cd* into the source directory then run:
|
then _cd_ into the source directory then run:
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
@@ -30,8 +30,8 @@ source):
|
|||||||
|
|
||||||
### genieacs-cwmp
|
### genieacs-cwmp
|
||||||
|
|
||||||
This is the service that the CPEs will communicate with. It listens on port
|
This is the service that the CPEs will communicate with. It listens on port 7547
|
||||||
7547 by default. Configure the ACS URL in your devices accordingly.
|
by default. Configure the ACS URL in your devices accordingly.
|
||||||
|
|
||||||
You may optionally use [genieacs-sim](https://github.com/genieacs/genieacs-sim)
|
You may optionally use [genieacs-sim](https://github.com/genieacs/genieacs-sim)
|
||||||
as a dummy TR-069 simulator if you don't have a CPE at hand.
|
as a dummy TR-069 simulator if you don't have a CPE at hand.
|
||||||
@@ -50,7 +50,7 @@ such. It listens on port 7567 by default.
|
|||||||
### genieacs-ui
|
### genieacs-ui
|
||||||
|
|
||||||
This serves the web based user interface. It listens on port 3000 by default.
|
This serves the web based user interface. It listens on port 3000 by default.
|
||||||
You must pass *--ui-jwt-secret* argument to supply the secret key used for
|
You must pass _--ui-jwt-secret_ argument to supply the secret key used for
|
||||||
signing browser cookies:
|
signing browser cookies:
|
||||||
|
|
||||||
genieacs-ui --ui-jwt-secret secret
|
genieacs-ui --ui-jwt-secret secret
|
||||||
@@ -64,14 +64,13 @@ a complete installation guide for production deployments.
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
The [forum](https://forum.genieacs.com) is a good place to get guidance and
|
The [forum](https://forum.genieacs.com) is a good place to get guidance and help
|
||||||
help from the community. Head on over and join the conversation!
|
from the community. Head on over and join the conversation!
|
||||||
|
|
||||||
For commercial support options, please visit
|
For commercial support options, please visit
|
||||||
[genieacs.com](https://genieacs.com/support/).
|
[genieacs.com](https://genieacs.com/support/).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2013-2020 GenieACS Inc. GenieACS is released under the [AGPLv3
|
Copyright 2013-2021 GenieACS Inc. GenieACS is released under the
|
||||||
license
|
[AGPLv3 license terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
|
||||||
terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
|
|
||||||
+2
-3
@@ -1,5 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
"extensions": ["ts"],
|
extensions: ["ts"],
|
||||||
"require": ["esm", "ts-node/register/transpile-only"]
|
require: ["esm", "ts-node/register/transpile-only"],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,6 +83,7 @@ if (!cluster.worker) {
|
|||||||
host: SERVICE_ADDRESS,
|
host: SERVICE_ADDRESS,
|
||||||
ssl: key && cert ? { key, cert } : null,
|
ssl: key && cert ? { key, cert } : null,
|
||||||
onConnection: cwmp.onConnection,
|
onConnection: cwmp.onConnection,
|
||||||
|
onClientError: cwmp.onClientError,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
keepAliveTimeout: 0,
|
keepAliveTimeout: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ if (!cluster.worker) {
|
|||||||
|
|
||||||
const _listener = (req, res): void => {
|
const _listener = (req, res): void => {
|
||||||
if (stopping) res.setHeader("Connection", "close");
|
if (stopping) res.setHeader("Connection", "close");
|
||||||
listener(req, res);
|
void listener(req, res);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initPromise = db
|
const initPromise = db
|
||||||
|
|||||||
+57
-114
@@ -25,12 +25,12 @@ import typescript from "@rollup/plugin-typescript";
|
|||||||
import { terser } from "rollup-plugin-terser";
|
import { terser } from "rollup-plugin-terser";
|
||||||
import nodeResolve from "@rollup/plugin-node-resolve";
|
import nodeResolve from "@rollup/plugin-node-resolve";
|
||||||
import commonjs from "@rollup/plugin-commonjs";
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
import webpack, { Configuration } from "webpack";
|
|
||||||
import postcss from "postcss";
|
import postcss from "postcss";
|
||||||
import postcssImport from "postcss-import";
|
import postcssImport from "postcss-import";
|
||||||
import postcssPresetEnv from "postcss-preset-env";
|
import postcssPresetEnv from "postcss-preset-env";
|
||||||
|
import postcssColorMod from "postcss-color-mod-function";
|
||||||
import cssnano from "cssnano";
|
import cssnano from "cssnano";
|
||||||
import SVGO from "svgo";
|
import { optimize } from "svgo";
|
||||||
import * as xmlParser from "../lib/xml-parser";
|
import * as xmlParser from "../lib/xml-parser";
|
||||||
|
|
||||||
const MODE = process.env["NODE_ENV"] || "production";
|
const MODE = process.env["NODE_ENV"] || "production";
|
||||||
@@ -43,7 +43,7 @@ const BUILD_METADATA = new Date()
|
|||||||
const INPUT_DIR = path.resolve(__dirname, "..");
|
const INPUT_DIR = path.resolve(__dirname, "..");
|
||||||
const OUTPUT_DIR = path.resolve(__dirname, "../dist");
|
const OUTPUT_DIR = path.resolve(__dirname, "../dist");
|
||||||
|
|
||||||
const externals = [
|
const builtins = [
|
||||||
"path",
|
"path",
|
||||||
"fs",
|
"fs",
|
||||||
"cluster",
|
"cluster",
|
||||||
@@ -54,33 +54,13 @@ const externals = [
|
|||||||
"zlib",
|
"zlib",
|
||||||
"crypto",
|
"crypto",
|
||||||
"util",
|
"util",
|
||||||
"mongodb",
|
|
||||||
"vm",
|
"vm",
|
||||||
"later",
|
|
||||||
"parsimmon",
|
|
||||||
"seedrandom",
|
|
||||||
"querystring",
|
"querystring",
|
||||||
"child_process",
|
"child_process",
|
||||||
"dgram",
|
"dgram",
|
||||||
"url",
|
"url",
|
||||||
"iconv-lite",
|
"readline",
|
||||||
"koa",
|
|
||||||
"koa-router",
|
|
||||||
"koa-compress",
|
|
||||||
"koa-bodyparser",
|
|
||||||
"koa-jwt",
|
|
||||||
"koa-static",
|
|
||||||
"jsonwebtoken",
|
|
||||||
"stream",
|
"stream",
|
||||||
"mithril",
|
|
||||||
"parsimmon",
|
|
||||||
"yaml",
|
|
||||||
"codemirror",
|
|
||||||
"codemirror/mode/javascript/javascript",
|
|
||||||
"codemirror/mode/yaml/yaml",
|
|
||||||
"ipaddr.js",
|
|
||||||
"jsbi",
|
|
||||||
"espresso-iisojs",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function rmDirSync(dirPath): void {
|
function rmDirSync(dirPath): void {
|
||||||
@@ -95,6 +75,7 @@ function rmDirSync(dirPath): void {
|
|||||||
fs.rmdirSync(dirPath);
|
fs.rmdirSync(dirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For lockfileVersion = 1
|
||||||
function stripDevDeps(deps): void {
|
function stripDevDeps(deps): void {
|
||||||
if (!deps["dependencies"]) return;
|
if (!deps["dependencies"]) return;
|
||||||
for (const [k, v] of Object.entries(deps["dependencies"])) {
|
for (const [k, v] of Object.entries(deps["dependencies"])) {
|
||||||
@@ -104,6 +85,15 @@ 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));
|
||||||
@@ -130,7 +120,7 @@ function generateSymbol(id: string, svgStr: string): string {
|
|||||||
return `<symbol id="icon-${id}" ${viewBox}>${symbolBody}</symbol>`;
|
return `<symbol id="icon-${id}" ${viewBox}>${symbolBody}</symbol>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<string[]> {
|
||||||
// Delete any old output directory
|
// Delete any old output directory
|
||||||
rmDirSync(OUTPUT_DIR);
|
rmDirSync(OUTPUT_DIR);
|
||||||
|
|
||||||
@@ -138,13 +128,13 @@ async function init(): Promise<void> {
|
|||||||
fs.mkdirSync(OUTPUT_DIR);
|
fs.mkdirSync(OUTPUT_DIR);
|
||||||
fs.mkdirSync(OUTPUT_DIR + "/bin");
|
fs.mkdirSync(OUTPUT_DIR + "/bin");
|
||||||
fs.mkdirSync(OUTPUT_DIR + "/public");
|
fs.mkdirSync(OUTPUT_DIR + "/public");
|
||||||
fs.mkdirSync(OUTPUT_DIR + "/tools");
|
|
||||||
|
|
||||||
// Create package.json
|
// Create package.json
|
||||||
const packageJson = JSON.parse(
|
const packageJson = JSON.parse(
|
||||||
fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString()
|
fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString()
|
||||||
);
|
);
|
||||||
delete packageJson["devDependencies"];
|
delete packageJson["devDependencies"];
|
||||||
|
delete packageJson["private"];
|
||||||
packageJson["scripts"] = {
|
packageJson["scripts"] = {
|
||||||
install: packageJson["scripts"].install,
|
install: packageJson["scripts"].install,
|
||||||
configure: packageJson["scripts"].configure,
|
configure: packageJson["scripts"].configure,
|
||||||
@@ -161,10 +151,12 @@ async function init(): Promise<void> {
|
|||||||
);
|
);
|
||||||
npmShrinkwrapJson["version"] = packageJson["version"];
|
npmShrinkwrapJson["version"] = packageJson["version"];
|
||||||
stripDevDeps(npmShrinkwrapJson);
|
stripDevDeps(npmShrinkwrapJson);
|
||||||
|
stripDevDeps2(npmShrinkwrapJson);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
|
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
|
||||||
JSON.stringify(npmShrinkwrapJson, null, 2)
|
JSON.stringify(npmShrinkwrapJson, null, 2)
|
||||||
);
|
);
|
||||||
|
return Object.keys(packageJson["dependencies"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyStatic(): Promise<void> {
|
async function copyStatic(): Promise<void> {
|
||||||
@@ -194,49 +186,15 @@ async function generateCss(): Promise<void> {
|
|||||||
stage: 3,
|
stage: 3,
|
||||||
features: {
|
features: {
|
||||||
"nesting-rules": true,
|
"nesting-rules": true,
|
||||||
"color-mod-function": true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
cssnano,
|
postcssColorMod,
|
||||||
|
...(MODE === "production" ? [cssnano] : []),
|
||||||
]).process(cssIn, { from: cssInPath, to: cssOutPath });
|
]).process(cssIn, { from: cssInPath, to: cssOutPath });
|
||||||
fs.writeFileSync(cssOutPath, cssOut.css);
|
fs.writeFileSync(cssOutPath, cssOut.css);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateToolsJs(): Promise<void> {
|
async function generateBackendJs(externals: string[]): Promise<void> {
|
||||||
for (const bin of ["dump-data-model"]) {
|
|
||||||
const inputFile = path.resolve(INPUT_DIR, `tools/${bin}.ts`);
|
|
||||||
const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`);
|
|
||||||
const bundle = await rollup({
|
|
||||||
input: inputFile,
|
|
||||||
external: externals,
|
|
||||||
acorn: {
|
|
||||||
allowHashBang: true,
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
typescript({
|
|
||||||
tsconfig: "./tsconfig.json",
|
|
||||||
include: [`tools/${bin}.ts`, "lib/**/*.ts"],
|
|
||||||
}),
|
|
||||||
MODE === "production" ? terser() : null,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
await bundle.write({
|
|
||||||
format: "cjs",
|
|
||||||
preferConst: true,
|
|
||||||
sourcemap: "inline",
|
|
||||||
sourcemapExcludeSources: true,
|
|
||||||
banner: "#!/usr/bin/env node",
|
|
||||||
file: outputFile,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mark as executable
|
|
||||||
const mode = fs.statSync(outputFile).mode;
|
|
||||||
fs.chmodSync(outputFile, mode | 73);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateBackendJs(): Promise<void> {
|
|
||||||
for (const bin of [
|
for (const bin of [
|
||||||
"genieacs-cwmp",
|
"genieacs-cwmp",
|
||||||
"genieacs-ext",
|
"genieacs-ext",
|
||||||
@@ -248,7 +206,7 @@ async function generateBackendJs(): Promise<void> {
|
|||||||
const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`);
|
const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`);
|
||||||
const bundle = await rollup({
|
const bundle = await rollup({
|
||||||
input: inputFile,
|
input: inputFile,
|
||||||
external: externals,
|
external: [...builtins, ...externals],
|
||||||
acorn: {
|
acorn: {
|
||||||
allowHashBang: true,
|
allowHashBang: true,
|
||||||
},
|
},
|
||||||
@@ -292,26 +250,26 @@ async function generateBackendJs(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateFrontendJs(): Promise<void> {
|
async function generateFrontendJs(externals: string[]): Promise<void> {
|
||||||
const inputFile = path.resolve(INPUT_DIR, "ui/app.ts");
|
const inputFile = path.resolve(INPUT_DIR, "ui/app.ts");
|
||||||
const outputFile = path.resolve(OUTPUT_DIR, "public/app.js");
|
const outputDir = path.resolve(OUTPUT_DIR, "public");
|
||||||
|
|
||||||
const inlineDeps = ["mithril", "parsimmon", "jsbi", "espresso-iisojs"];
|
const inlineDeps = ["parsimmon", "espresso-iisojs"];
|
||||||
const bundle = await rollup({
|
const bundle = await rollup({
|
||||||
input: inputFile,
|
input: inputFile,
|
||||||
external: externals.filter((e) => !inlineDeps.includes(e)),
|
external: [
|
||||||
|
...builtins,
|
||||||
|
...externals.filter((e) => !inlineDeps.includes(e)),
|
||||||
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
rollupJson({ preferConst: true }),
|
rollupJson({ preferConst: true }),
|
||||||
{
|
{
|
||||||
name: "",
|
name: "",
|
||||||
resolveId: function (importee, importer) {
|
resolveId: function (importee, importer) {
|
||||||
if (importee.endsWith("/bigint")) {
|
if (importee.endsWith("/package.json")) {
|
||||||
return this.resolve(importee + "-jsbi", importer);
|
const p = path.resolve(path.dirname(importer), importee);
|
||||||
} else if (importee === "espresso-iisojs") {
|
if (p === path.resolve(INPUT_DIR, "package.json"))
|
||||||
return this.resolve(
|
return path.resolve(OUTPUT_DIR, "package.json");
|
||||||
"espresso-iisojs/dist/espresso-iisojs-jsbi.mjs",
|
|
||||||
importer
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -319,8 +277,9 @@ async function generateFrontendJs(): Promise<void> {
|
|||||||
typescript({ tsconfig: "./tsconfig.json" }),
|
typescript({ tsconfig: "./tsconfig.json" }),
|
||||||
nodeResolve(),
|
nodeResolve(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
|
MODE === "production" ? terser() : null,
|
||||||
],
|
],
|
||||||
inlineDynamicImports: true,
|
preserveEntrySignatures: false,
|
||||||
treeshake: {
|
treeshake: {
|
||||||
propertyReadSideEffects: false,
|
propertyReadSideEffects: false,
|
||||||
moduleSideEffects: false,
|
moduleSideEffects: false,
|
||||||
@@ -332,52 +291,37 @@ async function generateFrontendJs(): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await bundle.write({
|
await bundle.write({
|
||||||
|
manualChunks: (id) => {
|
||||||
|
if (id.includes("node_modules/codemirror")) return "codemirror";
|
||||||
|
else if (id.includes("node_modules/yaml")) return "yaml";
|
||||||
|
return "app";
|
||||||
|
},
|
||||||
preferConst: true,
|
preferConst: true,
|
||||||
format: "es",
|
format: "es",
|
||||||
sourcemap: "inline",
|
sourcemap: true,
|
||||||
sourcemapExcludeSources: true,
|
sourcemapExcludeSources: true,
|
||||||
file: outputFile,
|
dir: outputDir,
|
||||||
});
|
|
||||||
|
|
||||||
const webpackConf: Configuration = {
|
|
||||||
mode: MODE as "development" | "production",
|
|
||||||
entry: outputFile,
|
|
||||||
resolve: {
|
|
||||||
aliasFields: ["module"],
|
|
||||||
},
|
|
||||||
devtool: "nosources-source-map",
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
use: ["source-map-loader"],
|
|
||||||
enforce: "pre",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.resolve(OUTPUT_DIR, "public"),
|
|
||||||
filename: "app.js",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
webpack(webpackConf, (err, stats) => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
process.stdout.write(stats.toString({ colors: true }) + "\n");
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateIconsSprite(): Promise<void> {
|
async function generateIconsSprite(): Promise<void> {
|
||||||
const svgo = new SVGO({ plugins: [{ removeViewBox: false }] });
|
|
||||||
const symbols = [];
|
const symbols = [];
|
||||||
const iconsDir = path.resolve(INPUT_DIR, "ui/icons");
|
const iconsDir = path.resolve(INPUT_DIR, "ui/icons");
|
||||||
for (const file of fs.readdirSync(iconsDir)) {
|
for (const file of fs.readdirSync(iconsDir)) {
|
||||||
const id = path.parse(file).name;
|
const id = path.parse(file).name;
|
||||||
const filePath = path.join(iconsDir, file);
|
const filePath = path.join(iconsDir, file);
|
||||||
const { data } = await svgo.optimize(fs.readFileSync(filePath).toString());
|
const { data } = await optimize(fs.readFileSync(filePath).toString(), {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: "preset-default",
|
||||||
|
params: {
|
||||||
|
overrides: {
|
||||||
|
removeViewBox: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
symbols.push(generateSymbol(id, data));
|
symbols.push(generateSymbol(id, data));
|
||||||
}
|
}
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -387,14 +331,13 @@ async function generateIconsSprite(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init()
|
init()
|
||||||
.then(() => {
|
.then((externals) => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
copyStatic(),
|
copyStatic(),
|
||||||
generateCss(),
|
generateCss(),
|
||||||
generateIconsSprite(),
|
generateIconsSprite(),
|
||||||
generateToolsJs(),
|
generateBackendJs(externals),
|
||||||
generateBackendJs(),
|
generateFrontendJs(externals),
|
||||||
generateFrontendJs(),
|
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Ignore
|
// Ignore
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
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
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#! /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 expoeses a rich RESTful API through its NBI component. This document
|
GenieACS exposes 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 pitfll is not properly percent-encoding special characters in the
|
A common pitfall 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-0000000"}
|
query = {"_id": "202BC1-BM632w-000000"}
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/devices/?query=%7B%22_id%22%3A%22202BC1-BM632w-0000000%22%7D'
|
curl -i 'http://localhost:7557/devices/?query=%7B%22_id%22%3A%22202BC1-BM632w-000000%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-0000000"}
|
query = {"device": "202BC1-BM632w-000000"}
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/tasks/?query=%7B%22device%22%3A%22202BC1-BM632w-0000000%22%7D'
|
curl -i 'http://localhost:7557/tasks/?query=%7B%22device%22%3A%22202BC1-BM632w-000000%22%7D'
|
||||||
|
|
||||||
- Return specific parameters for a given device:
|
- Return specific parameters for a given device:
|
||||||
|
|
||||||
.. code:: javascript
|
.. code:: javascript
|
||||||
|
|
||||||
query = {"_id": "202BC1-BM632w-0000000"}
|
query = {"_id": "202BC1-BM632w-000000"}
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
curl -i 'http://localhost:7557/devices?query=%7B%22_id%22%3A%22202BC1-BM632w-0000000%22%7D&projection=InternetGatewayDevice.DeviceInfo.ModelName,InternetGatewayDevice.DeviceInfo.Manufacturer'
|
curl -i 'http://localhost:7557/devices?query=%7B%22_id%22%3A%22202BC1-BM632w-000000%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 immediatly. Otherwise, the tasks will be queued and be
|
execute the tasks immediately. 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-0000000/tasks?connection_request' \
|
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/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-0000000/tasks?connection_request' \
|
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/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-0000000:default' -X DELETE
|
curl -i 'http://localhost:7557/faults/202BC1-BM632w-000000: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-0000000/tags/testing' -X POST
|
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/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-0000000/tags/testing' -X DELETE
|
curl -i 'http://localhost:7557/devices/202BC1-BM632w-000000/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 belogs to.
|
- ``oui``: The OUI of the device model that this file belongs 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 availabe tasks and their object structure.
|
Find the different available tasks and their object structure.
|
||||||
|
|
||||||
``getParameterValues``
|
``getParameterValues``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -3,4 +3,70 @@
|
|||||||
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: ``0.0.0.0``
|
Default: ``::``
|
||||||
|
|
||||||
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: unest
|
Default: unset
|
||||||
|
|
||||||
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: ``0.0.0.0``
|
Default: ``::``
|
||||||
|
|
||||||
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: unest
|
Default: unset
|
||||||
|
|
||||||
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: ``0.0.0.0``
|
Default: ``::``
|
||||||
|
|
||||||
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.
|
||||||
@@ -186,7 +186,7 @@ UI_PORT
|
|||||||
UI_INTERFACE
|
UI_INTERFACE
|
||||||
The network interface that genieacs-ui binds to.
|
The network interface that genieacs-ui binds to.
|
||||||
|
|
||||||
Default: ``0.0.0.0``
|
Default: ``::``
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
+10
-7
@@ -1,4 +1,4 @@
|
|||||||
// This is an exmaple GenieACS extension to get the current latitude/longitude
|
// This is an example 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,15 +10,17 @@ let cache = null;
|
|||||||
let cacheExpire = 0;
|
let cacheExpire = 0;
|
||||||
|
|
||||||
function latlong(args, callback) {
|
function latlong(args, callback) {
|
||||||
if (Date.now() < cacheExpire)
|
if (Date.now() < cacheExpire) return callback(null, cache);
|
||||||
return callback(null, cache);
|
|
||||||
|
|
||||||
http.get("http://api.open-notify.org/iss-now.json", (res) => {
|
http
|
||||||
|
.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"];
|
||||||
@@ -26,7 +28,8 @@ function latlong(args, callback) {
|
|||||||
cacheExpire = Date.now() + 10000;
|
cacheExpire = Date.now() + 10000;
|
||||||
callback(null, cache);
|
callback(null, cache);
|
||||||
});
|
});
|
||||||
}).on("error", (err) => {
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 independant of each other and may be
|
The various GenieACS services are independent 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 10.x and up. Refer to https://nodejs.org/ for
|
GenieACS requires Node.js 12.13 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 --unsafe-perm genieacs@\ |release|
|
sudo npm install -g genieacs@\ |release|
|
||||||
|
|
||||||
.. topic:: Installing from source
|
.. topic:: Installing from source
|
||||||
|
|
||||||
@@ -73,6 +73,7 @@ Configure systemd
|
|||||||
GENIEACS_FS_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-fs-access.log
|
GENIEACS_FS_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-fs-access.log
|
||||||
GENIEACS_UI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-ui-access.log
|
GENIEACS_UI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-ui-access.log
|
||||||
GENIEACS_DEBUG_FILE=/var/log/genieacs/genieacs-debug.yaml
|
GENIEACS_DEBUG_FILE=/var/log/genieacs/genieacs-debug.yaml
|
||||||
|
NODE_OPTIONS=--enable-source-maps
|
||||||
GENIEACS_EXT_DIR=/opt/genieacs/ext
|
GENIEACS_EXT_DIR=/opt/genieacs/ext
|
||||||
GENIEACS_UI_JWT_SECRET=secret
|
GENIEACS_UI_JWT_SECRET=secret
|
||||||
|
|
||||||
|
|||||||
+24
-10
@@ -17,7 +17,6 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { parse } from "url";
|
|
||||||
import * as db from "./db";
|
import * as db from "./db";
|
||||||
import * as common from "./common";
|
import * as common from "./common";
|
||||||
import * as cache from "./cache";
|
import * as cache from "./cache";
|
||||||
@@ -80,7 +79,7 @@ export async function connectionRequest(
|
|||||||
).value || [""])[0];
|
).value || [""])[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteAddress = parse(connectionRequestUrl).host;
|
const remoteAddress = new URL(connectionRequestUrl).hostname;
|
||||||
|
|
||||||
const evalCallback = (exp): Expression => {
|
const evalCallback = (exp): Expression => {
|
||||||
if (!Array.isArray(exp)) return exp;
|
if (!Array.isArray(exp)) return exp;
|
||||||
@@ -95,7 +94,7 @@ export async function connectionRequest(
|
|||||||
else if (name === "password") return password;
|
else if (name === "password") return password;
|
||||||
|
|
||||||
const p = device[name];
|
const p = device[name];
|
||||||
if (p && p.value) return p.value[0];
|
if (p?.value) return p.value[0];
|
||||||
} else if (exp[0] === "FUNC") {
|
} else if (exp[0] === "FUNC") {
|
||||||
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress;
|
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress;
|
||||||
else if (exp[1] === "USERNAME") return username;
|
else if (exp[1] === "USERNAME") return username;
|
||||||
@@ -145,15 +144,24 @@ export async function connectionRequest(
|
|||||||
|
|
||||||
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
|
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
|
||||||
|
|
||||||
let udpProm;
|
let udpProm = Promise.resolve(false);
|
||||||
if (udpConnectionRequestAddress) {
|
if (udpConnectionRequestAddress) {
|
||||||
|
try {
|
||||||
|
const u = new URL("udp://" + udpConnectionRequestAddress);
|
||||||
udpProm = udpConnectionRequest(
|
udpProm = udpConnectionRequest(
|
||||||
udpConnectionRequestAddress,
|
u.hostname,
|
||||||
|
parseInt(u.port || "80"),
|
||||||
authExp,
|
authExp,
|
||||||
UDP_CONNECTION_REQUEST_PORT,
|
UDP_CONNECTION_REQUEST_PORT,
|
||||||
debug,
|
debug,
|
||||||
deviceId
|
deviceId
|
||||||
|
).then(
|
||||||
|
() => true,
|
||||||
|
() => false
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore invalid address
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = await httpConnectionRequest(
|
const status = await httpConnectionRequest(
|
||||||
@@ -165,10 +173,7 @@ export async function connectionRequest(
|
|||||||
deviceId
|
deviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (udpProm) {
|
if (await udpProm) return "";
|
||||||
await udpProm;
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -296,6 +301,15 @@ function sanitizeTask(task): void {
|
|||||||
)
|
)
|
||||||
throw new Error("Invalid 'provisions' property");
|
throw new Error("Invalid 'provisions' property");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "reboot":
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "factoryReset":
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid task name");
|
||||||
}
|
}
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
@@ -303,7 +317,7 @@ function sanitizeTask(task): void {
|
|||||||
|
|
||||||
export async function insertTasks(tasks: any[]): Promise<Task[]> {
|
export async function insertTasks(tasks: any[]): Promise<Task[]> {
|
||||||
if (tasks && !Array.isArray(tasks)) tasks = [tasks];
|
if (tasks && !Array.isArray(tasks)) tasks = [tasks];
|
||||||
else if (!tasks || tasks.length === 0) return tasks || [];
|
else if (!tasks?.length) return tasks || [];
|
||||||
|
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
sanitizeTask(task);
|
sanitizeTask(task);
|
||||||
|
|||||||
+3
-3
@@ -52,9 +52,9 @@ function parseHeaderFeilds(str: string): Record<string, string> {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseAuthorizationHeader(
|
export function parseAuthorizationHeader(authHeader: string): {
|
||||||
authHeader: string
|
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 };
|
||||||
|
|||||||
+14
-27
@@ -20,11 +20,12 @@
|
|||||||
import { Collection } from "mongodb";
|
import { Collection } from "mongodb";
|
||||||
import { onConnect } from "./db";
|
import { onConnect } from "./db";
|
||||||
import * as config from "./config";
|
import * as config from "./config";
|
||||||
|
import { Cache } from "./mongodb-types";
|
||||||
|
|
||||||
const CLOCK_SKEW_TOLERANCE = 30000;
|
const CLOCK_SKEW_TOLERANCE = 30000;
|
||||||
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
|
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
|
||||||
|
|
||||||
let cacheCollection: Collection;
|
let cacheCollection: Collection<Cache>;
|
||||||
|
|
||||||
onConnect(async (db) => {
|
onConnect(async (db) => {
|
||||||
cacheCollection = db.collection("cache");
|
cacheCollection = db.collection("cache");
|
||||||
@@ -33,8 +34,7 @@ onConnect(async (db) => {
|
|||||||
|
|
||||||
export async function get(key: string): Promise<string> {
|
export async function get(key: string): Promise<string> {
|
||||||
const res = await cacheCollection.findOne({ _id: key });
|
const res = await cacheCollection.findOne({ _id: key });
|
||||||
if (res) return res["value"];
|
return res?.value;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function del(key: string): Promise<void> {
|
export async function del(key: string): Promise<void> {
|
||||||
@@ -52,15 +52,14 @@ export async function set(
|
|||||||
);
|
);
|
||||||
await cacheCollection.replaceOne(
|
await cacheCollection.replaceOne(
|
||||||
{ _id: key },
|
{ _id: key },
|
||||||
{ _id: key, value, expire, timestamp },
|
{ value, expire, timestamp },
|
||||||
{ upsert: true }
|
{ upsert: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pop(key: string): Promise<any> {
|
export async function pop(key: string): Promise<string> {
|
||||||
const res = await cacheCollection.findOneAndDelete({ _id: key });
|
const res = await cacheCollection.findOneAndDelete({ _id: key });
|
||||||
if (res && res["value"]) return res["value"]["value"];
|
return res.value?.value;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acquireLock(
|
export async function acquireLock(
|
||||||
@@ -72,35 +71,23 @@ export async function acquireLock(
|
|||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const r = await cacheCollection.findOneAndUpdate(
|
const r = await cacheCollection.findOneAndUpdate(
|
||||||
{ _id: lockName, token },
|
{ _id: lockName, value: token },
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
value: token,
|
|
||||||
expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE),
|
expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE),
|
||||||
},
|
},
|
||||||
$currentDate: { timestamp: true },
|
$currentDate: { timestamp: true },
|
||||||
},
|
},
|
||||||
{ upsert: true, returnOriginal: false }
|
{ upsert: true, returnDocument: "after" }
|
||||||
);
|
);
|
||||||
const v = r.value;
|
if (Math.abs(r.value.timestamp.getTime() - now) > CLOCK_SKEW_TOLERANCE)
|
||||||
if (Math.abs(v["timestamp"].getTime() - now) > CLOCK_SKEW_TOLERANCE)
|
|
||||||
throw new Error("Database clock skew too great");
|
throw new Error("Database clock skew too great");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 11000) {
|
if (err.code !== 11000) throw err;
|
||||||
if (timeout > 0) {
|
if (!(timeout > 0)) return null;
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const w = 50 + Math.random() * 50;
|
const w = 50 + Math.random() * 50;
|
||||||
setTimeout(() => {
|
await new Promise((resolve) => setTimeout(resolve, w));
|
||||||
acquireLock(lockName, ttl, timeout - w, token).then(
|
return acquireLock(lockName, ttl, timeout - w, token);
|
||||||
resolve,
|
|
||||||
reject
|
|
||||||
);
|
|
||||||
}, w);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw new Error("Failed to acquire lock");
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
@@ -114,5 +101,5 @@ export async function releaseLock(
|
|||||||
_id: lockName,
|
_id: lockName,
|
||||||
value: token,
|
value: token,
|
||||||
});
|
});
|
||||||
if (res["result"]["n"] !== 1) throw new Error("Lock expired");
|
if (res.deletedCount !== 1) throw new Error("Lock expired");
|
||||||
}
|
}
|
||||||
+2
-2
@@ -17,14 +17,14 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import cluster from "cluster";
|
import cluster, { Worker } from "cluster";
|
||||||
import { cpus } from "os";
|
import { cpus } from "os";
|
||||||
import * as logger from "./logger";
|
import * as logger from "./logger";
|
||||||
|
|
||||||
let respawnTimestamp = 0;
|
let respawnTimestamp = 0;
|
||||||
let crashes: number[] = [];
|
let crashes: number[] = [];
|
||||||
|
|
||||||
function fork(): cluster.Worker {
|
function fork(): 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
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ export function escapeRegExp(str: string): string {
|
|||||||
export function encodeTag(tag: string): string {
|
export function encodeTag(tag: string): string {
|
||||||
return encodeURIComponent(tag)
|
return encodeURIComponent(tag)
|
||||||
.replace(
|
.replace(
|
||||||
/[!~*'()]/g,
|
/[!~*'().]/g,
|
||||||
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
|
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
|
||||||
)
|
)
|
||||||
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
|
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ import { PermissionSet, Expression } from "../types";
|
|||||||
import { evaluate, or } from "./expression";
|
import { evaluate, or } from "./expression";
|
||||||
|
|
||||||
export default class Authorizer {
|
export default class Authorizer {
|
||||||
private permissionSets: PermissionSet[];
|
private declare permissionSets: PermissionSet[];
|
||||||
private validatorCache: WeakMap<
|
private declare validatorCache: WeakMap<
|
||||||
any,
|
any,
|
||||||
(mutationType, mutation, any) => boolean
|
(mutationType, mutation, any) => boolean
|
||||||
>;
|
>;
|
||||||
private hasAccessCache: Map<string, boolean>;
|
private declare hasAccessCache: Map<string, boolean>;
|
||||||
private getFilterCache: Map<string, Expression>;
|
private declare getFilterCache: Map<string, Expression>;
|
||||||
|
|
||||||
public constructor(permissionSets: PermissionSet[]) {
|
public constructor(permissionSets: PermissionSet[]) {
|
||||||
this.permissionSets = permissionSets;
|
this.permissionSets = permissionSets;
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright 2013-2020 GenieACS Inc.
|
|
||||||
*
|
|
||||||
* This file is part of GenieACS.
|
|
||||||
*
|
|
||||||
* GenieACS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* GenieACS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import JSBI from "jsbi";
|
|
||||||
|
|
||||||
export function and(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.bitwiseAnd(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function or(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.bitwiseOr(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function xor(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.bitwiseXor(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function not(a: JSBI): JSBI {
|
|
||||||
return JSBI.bitwiseNot(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function lshift(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.leftShift(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rshift(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.signedRightShift(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function add(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.add(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sub(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.subtract(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mul(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.multiply(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function div(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.divide(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function exp(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.exponentiate(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rem(a: JSBI, b: JSBI): JSBI {
|
|
||||||
return JSBI.remainder(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toNumber(a: JSBI): number {
|
|
||||||
return JSBI.toNumber(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function eq(a: JSBI, b: JSBI): boolean {
|
|
||||||
return JSBI.equal(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ne(a: JSBI, b: JSBI): boolean {
|
|
||||||
return JSBI.notEqual(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function lt(a: JSBI, b: JSBI): boolean {
|
|
||||||
return JSBI.lessThan(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function lte(a: JSBI, b: JSBI): boolean {
|
|
||||||
return JSBI.lessThanOrEqual(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function gt(a: JSBI, b: JSBI): boolean {
|
|
||||||
return JSBI.greaterThan(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function gte(a: JSBI, b: JSBI): boolean {
|
|
||||||
return JSBI.greaterThanOrEqual(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function asUintN(a: number, b: JSBI): JSBI {
|
|
||||||
return JSBI.asUintN(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function asIntN(a: number, b: JSBI): JSBI {
|
|
||||||
return JSBI.asIntN(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
const _BigInt = JSBI.BigInt;
|
|
||||||
type _bigint = JSBI;
|
|
||||||
export { _BigInt as BigInt, _bigint as bigint };
|
|
||||||
+267
-243
@@ -29,8 +29,8 @@ const TWO = BI.BigInt(2);
|
|||||||
const NEGATIVE_ONE = BI.BigInt(-1);
|
const NEGATIVE_ONE = BI.BigInt(-1);
|
||||||
|
|
||||||
class Indeterminates {
|
class Indeterminates {
|
||||||
public map: Map<string, number>;
|
public declare map: Map<string, number>;
|
||||||
public sortedKeys: string[];
|
public declare sortedKeys: string[];
|
||||||
|
|
||||||
public constructor(str?: string) {
|
public constructor(str?: string) {
|
||||||
this.map = new Map();
|
this.map = new Map();
|
||||||
@@ -118,7 +118,7 @@ function findGcd(a: BI.bigint, b: BI.bigint): BI.bigint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Polynomial {
|
class Polynomial {
|
||||||
public terms: Term[];
|
public declare terms: Term[];
|
||||||
|
|
||||||
public constructor(terms: Term[]) {
|
public constructor(terms: Term[]) {
|
||||||
this.terms = terms;
|
this.terms = terms;
|
||||||
@@ -298,12 +298,11 @@ class Polynomial {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Minterm = number[];
|
type Minterm = number[];
|
||||||
type Context = Map<string, number>;
|
|
||||||
|
|
||||||
abstract class BoolExprSynth {
|
abstract class BoolExprSynth {
|
||||||
public abstract true(context: Context): Minterm[];
|
public abstract true(context: SynthContext): Minterm[];
|
||||||
public abstract false(context: Context): Minterm[];
|
public abstract false(context: SynthContext): Minterm[];
|
||||||
public abstract null(context: Context): Minterm[];
|
public abstract null(context: SynthContext): Minterm[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class TrueSynth extends BoolExprSynth {
|
class TrueSynth extends BoolExprSynth {
|
||||||
@@ -343,8 +342,8 @@ class NullSynth extends BoolExprSynth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class VarSynth extends BoolExprSynth {
|
class VarSynth extends BoolExprSynth {
|
||||||
private expStr: string;
|
private declare exp: Expression;
|
||||||
private negate: boolean;
|
private declare negate: boolean;
|
||||||
public constructor(exp: Expression) {
|
public constructor(exp: Expression) {
|
||||||
super();
|
super();
|
||||||
this.negate = false;
|
this.negate = false;
|
||||||
@@ -368,52 +367,49 @@ class VarSynth extends BoolExprSynth {
|
|||||||
this.negate = true;
|
this.negate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.expStr = JSON.stringify(exp);
|
this.exp = exp;
|
||||||
}
|
}
|
||||||
public true(context: Context): Minterm[] {
|
public true(context: SynthContext): Minterm[] {
|
||||||
let v = context.get(this.expStr);
|
const v = context.getVariable(this.exp);
|
||||||
if (v == null) context.set(this.expStr, (v = context.size));
|
|
||||||
return [[(v << 2) ^ (this.negate ? 1 : 3)]];
|
return [[(v << 2) ^ (this.negate ? 1 : 3)]];
|
||||||
}
|
}
|
||||||
public false(context: Context): Minterm[] {
|
public false(context: SynthContext): Minterm[] {
|
||||||
let v = context.get(this.expStr);
|
const v = context.getVariable(this.exp);
|
||||||
if (v == null) context.set(this.expStr, (v = context.size));
|
|
||||||
return [[(v << 2) ^ (this.negate ? 3 : 1)]];
|
return [[(v << 2) ^ (this.negate ? 3 : 1)]];
|
||||||
}
|
}
|
||||||
public null(context: Context): Minterm[] {
|
public null(context: SynthContext): Minterm[] {
|
||||||
let v = context.get(this.expStr);
|
const v = context.getVariable(this.exp);
|
||||||
if (v == null) context.set(this.expStr, (v = context.size));
|
|
||||||
return [[v << 2, (v << 2) ^ 2]];
|
return [[v << 2, (v << 2) ^ 2]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotSynth extends BoolExprSynth {
|
class NotSynth extends BoolExprSynth {
|
||||||
private exprSynth: BoolExprSynth;
|
private declare exprSynth: BoolExprSynth;
|
||||||
public constructor(e: BoolExprSynth) {
|
public constructor(e: BoolExprSynth) {
|
||||||
super();
|
super();
|
||||||
this.exprSynth = e;
|
this.exprSynth = e;
|
||||||
}
|
}
|
||||||
public true(context: Context): Minterm[] {
|
public true(context: SynthContext): Minterm[] {
|
||||||
return this.exprSynth.false(context);
|
return this.exprSynth.false(context);
|
||||||
}
|
}
|
||||||
public false(context: Context): Minterm[] {
|
public false(context: SynthContext): Minterm[] {
|
||||||
return this.exprSynth.true(context);
|
return this.exprSynth.true(context);
|
||||||
}
|
}
|
||||||
public null(context: Context): Minterm[] {
|
public null(context: SynthContext): Minterm[] {
|
||||||
return this.exprSynth.null(context);
|
return this.exprSynth.null(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IsNullSynth extends BoolExprSynth {
|
class IsNullSynth extends BoolExprSynth {
|
||||||
private exprSynth: BoolExprSynth;
|
private declare exprSynth: BoolExprSynth;
|
||||||
public constructor(e: BoolExprSynth) {
|
public constructor(e: BoolExprSynth) {
|
||||||
super();
|
super();
|
||||||
this.exprSynth = e;
|
this.exprSynth = e;
|
||||||
}
|
}
|
||||||
public true(context: Context): Minterm[] {
|
public true(context: SynthContext): Minterm[] {
|
||||||
return this.exprSynth.null(context);
|
return this.exprSynth.null(context);
|
||||||
}
|
}
|
||||||
public false(context: Context): Minterm[] {
|
public false(context: SynthContext): Minterm[] {
|
||||||
return [...this.exprSynth.true(context), ...this.exprSynth.false(context)];
|
return [...this.exprSynth.true(context), ...this.exprSynth.false(context)];
|
||||||
}
|
}
|
||||||
public null(): Minterm[] {
|
public null(): Minterm[] {
|
||||||
@@ -422,7 +418,7 @@ class IsNullSynth extends BoolExprSynth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class OrSynth extends BoolExprSynth {
|
class OrSynth extends BoolExprSynth {
|
||||||
private exprSynths: BoolExprSynth[];
|
private declare exprSynths: BoolExprSynth[];
|
||||||
public constructor(...e: BoolExprSynth[]) {
|
public constructor(...e: BoolExprSynth[]) {
|
||||||
super();
|
super();
|
||||||
this.exprSynths = e.filter((ee) => !(ee instanceof FalseSynth));
|
this.exprSynths = e.filter((ee) => !(ee instanceof FalseSynth));
|
||||||
@@ -436,13 +432,13 @@ class OrSynth extends BoolExprSynth {
|
|||||||
});
|
});
|
||||||
this.exprSynths.push(...unpacked);
|
this.exprSynths.push(...unpacked);
|
||||||
}
|
}
|
||||||
public true(context: Context): Minterm[] {
|
public true(context: SynthContext): Minterm[] {
|
||||||
if (this.exprSynths.length === 0) return [];
|
if (this.exprSynths.length === 0) return [];
|
||||||
if (this.exprSynths.length === 1) return this.exprSynths[0].true(context);
|
if (this.exprSynths.length === 1) return this.exprSynths[0].true(context);
|
||||||
if (this.exprSynths.some((e) => e instanceof TrueSynth)) return [[]];
|
if (this.exprSynths.some((e) => e instanceof TrueSynth)) return [[]];
|
||||||
return this.exprSynths.map((e) => e.true(context)).flat();
|
return this.exprSynths.map((e) => e.true(context)).flat();
|
||||||
}
|
}
|
||||||
public false(context: Context): Minterm[] {
|
public false(context: SynthContext): Minterm[] {
|
||||||
if (this.exprSynths.length === 0) return [[]];
|
if (this.exprSynths.length === 0) return [[]];
|
||||||
if (this.exprSynths.length === 1) return this.exprSynths[0].false(context);
|
if (this.exprSynths.length === 1) return this.exprSynths[0].false(context);
|
||||||
if (
|
if (
|
||||||
@@ -455,7 +451,7 @@ class OrSynth extends BoolExprSynth {
|
|||||||
this.exprSynths.map((e) => complement(e.false(context))).flat()
|
this.exprSynths.map((e) => complement(e.false(context))).flat()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
public null(context: Context): Minterm[] {
|
public null(context: SynthContext): Minterm[] {
|
||||||
if (this.exprSynths.length === 0) return [];
|
if (this.exprSynths.length === 0) return [];
|
||||||
if (this.exprSynths.length === 1) return this.exprSynths[0].null(context);
|
if (this.exprSynths.length === 1) return this.exprSynths[0].null(context);
|
||||||
const n = this.exprSynths.map((e) => e.null(context)).flat();
|
const n = this.exprSynths.map((e) => e.null(context)).flat();
|
||||||
@@ -465,7 +461,7 @@ class OrSynth extends BoolExprSynth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AndSynth extends BoolExprSynth {
|
class AndSynth extends BoolExprSynth {
|
||||||
private exprSynths: BoolExprSynth[];
|
private declare exprSynths: BoolExprSynth[];
|
||||||
public constructor(...e: BoolExprSynth[]) {
|
public constructor(...e: BoolExprSynth[]) {
|
||||||
super();
|
super();
|
||||||
this.exprSynths = e.filter((ee) => !(ee instanceof TrueSynth));
|
this.exprSynths = e.filter((ee) => !(ee instanceof TrueSynth));
|
||||||
@@ -479,7 +475,7 @@ class AndSynth extends BoolExprSynth {
|
|||||||
});
|
});
|
||||||
this.exprSynths.push(...unpacked);
|
this.exprSynths.push(...unpacked);
|
||||||
}
|
}
|
||||||
public true(context: Context): Minterm[] {
|
public true(context: SynthContext): Minterm[] {
|
||||||
if (this.exprSynths.length === 0) return [[]];
|
if (this.exprSynths.length === 0) return [[]];
|
||||||
if (this.exprSynths.length === 1) return this.exprSynths[0].true(context);
|
if (this.exprSynths.length === 1) return this.exprSynths[0].true(context);
|
||||||
if (
|
if (
|
||||||
@@ -492,13 +488,13 @@ class AndSynth extends BoolExprSynth {
|
|||||||
this.exprSynths.map((e) => complement(e.true(context))).flat()
|
this.exprSynths.map((e) => complement(e.true(context))).flat()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
public false(context: Context): Minterm[] {
|
public false(context: SynthContext): Minterm[] {
|
||||||
if (this.exprSynths.length === 0) return [];
|
if (this.exprSynths.length === 0) return [];
|
||||||
if (this.exprSynths.length === 1) return this.exprSynths[0].false(context);
|
if (this.exprSynths.length === 1) return this.exprSynths[0].false(context);
|
||||||
if (this.exprSynths.some((e) => e instanceof FalseSynth)) return [[]];
|
if (this.exprSynths.some((e) => e instanceof FalseSynth)) return [[]];
|
||||||
return this.exprSynths.map((e) => e.false(context)).flat();
|
return this.exprSynths.map((e) => e.false(context)).flat();
|
||||||
}
|
}
|
||||||
public null(context: Context): Minterm[] {
|
public null(context: SynthContext): Minterm[] {
|
||||||
if (this.exprSynths.length === 0) return [];
|
if (this.exprSynths.length === 0) return [];
|
||||||
if (this.exprSynths.length === 1) return this.exprSynths[0].null(context);
|
if (this.exprSynths.length === 1) return this.exprSynths[0].null(context);
|
||||||
const n = this.exprSynths.map((e) => e.null(context)).flat();
|
const n = this.exprSynths.map((e) => e.null(context)).flat();
|
||||||
@@ -508,12 +504,12 @@ class AndSynth extends BoolExprSynth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CaseSynth extends BoolExprSynth {
|
class CaseSynth extends BoolExprSynth {
|
||||||
private exprSynths: BoolExprSynth[];
|
private declare exprSynths: BoolExprSynth[];
|
||||||
public constructor(e: BoolExprSynth[]) {
|
public constructor(e: BoolExprSynth[]) {
|
||||||
super();
|
super();
|
||||||
this.exprSynths = e;
|
this.exprSynths = e;
|
||||||
}
|
}
|
||||||
public true(context: Context): Minterm[] {
|
public true(context: SynthContext): Minterm[] {
|
||||||
const minterms: Minterm[] = [];
|
const minterms: Minterm[] = [];
|
||||||
const cumulative: Minterm[] = [];
|
const cumulative: Minterm[] = [];
|
||||||
for (let i = 0; i < this.exprSynths.length; i += 2) {
|
for (let i = 0; i < this.exprSynths.length; i += 2) {
|
||||||
@@ -533,7 +529,7 @@ class CaseSynth extends BoolExprSynth {
|
|||||||
}
|
}
|
||||||
return minterms;
|
return minterms;
|
||||||
}
|
}
|
||||||
public false(context: Context): Minterm[] {
|
public false(context: SynthContext): Minterm[] {
|
||||||
const minterms: Minterm[] = [];
|
const minterms: Minterm[] = [];
|
||||||
const cumulative: Minterm[] = [];
|
const cumulative: Minterm[] = [];
|
||||||
for (let i = 0; i < this.exprSynths.length; i += 2) {
|
for (let i = 0; i < this.exprSynths.length; i += 2) {
|
||||||
@@ -553,7 +549,7 @@ class CaseSynth extends BoolExprSynth {
|
|||||||
}
|
}
|
||||||
return minterms;
|
return minterms;
|
||||||
}
|
}
|
||||||
public null(context: Context): Minterm[] {
|
public null(context: SynthContext): Minterm[] {
|
||||||
const minterms: Minterm[] = [];
|
const minterms: Minterm[] = [];
|
||||||
const cumulative: Minterm[] = [];
|
const cumulative: Minterm[] = [];
|
||||||
for (let i = 0; i < this.exprSynths.length; i += 2) {
|
for (let i = 0; i < this.exprSynths.length; i += 2) {
|
||||||
@@ -666,10 +662,10 @@ function normalizeCallback(exp: Expression): Expression {
|
|||||||
if (p == null) return null;
|
if (p == null) return null;
|
||||||
args.push(p);
|
args.push(p);
|
||||||
}
|
}
|
||||||
return (args.reduce(
|
return args.reduce(
|
||||||
(previousValue, currentValue) => previousValue.add(currentValue),
|
(previousValue, currentValue) => previousValue.add(currentValue),
|
||||||
ADDITIVE_IDENTITY
|
ADDITIVE_IDENTITY
|
||||||
) as unknown) as Expression;
|
) as unknown as Expression;
|
||||||
} else if (op === "*") {
|
} else if (op === "*") {
|
||||||
const args: Polynomial[] = [];
|
const args: Polynomial[] = [];
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
for (let i = 1; i < exp.length; ++i) {
|
||||||
@@ -677,10 +673,10 @@ function normalizeCallback(exp: Expression): Expression {
|
|||||||
if (p == null) return null;
|
if (p == null) return null;
|
||||||
args.push(p);
|
args.push(p);
|
||||||
}
|
}
|
||||||
return (args.reduce(
|
return args.reduce(
|
||||||
(previousValue, currentValue) => previousValue.multiply(currentValue),
|
(previousValue, currentValue) => previousValue.multiply(currentValue),
|
||||||
MULTIPLICATIVE_IDENTITY
|
MULTIPLICATIVE_IDENTITY
|
||||||
) as unknown) as Expression;
|
) as unknown as Expression;
|
||||||
} else if (op === "-") {
|
} else if (op === "-") {
|
||||||
const args: Polynomial[] = [];
|
const args: Polynomial[] = [];
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
for (let i = 1; i < exp.length; ++i) {
|
||||||
@@ -688,9 +684,9 @@ function normalizeCallback(exp: Expression): Expression {
|
|||||||
if (p == null) return null;
|
if (p == null) return null;
|
||||||
args.push(p);
|
args.push(p);
|
||||||
}
|
}
|
||||||
return (args.reduce((previousValue, currentValue) =>
|
return args.reduce((previousValue, currentValue) =>
|
||||||
previousValue.subtract(currentValue)
|
previousValue.subtract(currentValue)
|
||||||
) as unknown) as Expression;
|
) as unknown as Expression;
|
||||||
} else if (op === "/") {
|
} else if (op === "/") {
|
||||||
const args: Polynomial[] = [];
|
const args: Polynomial[] = [];
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
for (let i = 1; i < exp.length; ++i) {
|
||||||
@@ -698,9 +694,9 @@ function normalizeCallback(exp: Expression): Expression {
|
|||||||
if (p == null) return null;
|
if (p == null) return null;
|
||||||
args.push(p);
|
args.push(p);
|
||||||
}
|
}
|
||||||
return (args.reduce((previousValue, currentValue) =>
|
return args.reduce((previousValue, currentValue) =>
|
||||||
previousValue.divide(currentValue)
|
previousValue.divide(currentValue)
|
||||||
) as unknown) as Expression;
|
) as unknown as Expression;
|
||||||
} else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) {
|
} else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) {
|
||||||
if (exp[1] == null || exp[2] == null) return null;
|
if (exp[1] == null || exp[2] == null) return null;
|
||||||
let lhs: Polynomial, rhs: Polynomial;
|
let lhs: Polynomial, rhs: Polynomial;
|
||||||
@@ -798,17 +794,14 @@ function toBoolExprSynth(e: Expression | BoolExprSynth): BoolExprSynth {
|
|||||||
return new FalseSynth();
|
return new FalseSynth();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sopToExpression(
|
function sopToExpression(sop: number[][], context: SynthContext): Expression {
|
||||||
sop: number[][],
|
|
||||||
expressions: Map<number, Expression>
|
|
||||||
): Expression {
|
|
||||||
if (!sop.length) return false;
|
if (!sop.length) return false;
|
||||||
const res: Expression[] = [];
|
const res: Expression[] = [];
|
||||||
for (const s of sop) {
|
for (const s of sop) {
|
||||||
if (!s.length) return true;
|
if (!s.length) return true;
|
||||||
const conjs: Expression[] = [];
|
const conjs: Expression[] = [];
|
||||||
for (const i of s) {
|
for (const i of s) {
|
||||||
let expr = expressions.get(i >>> 2);
|
let expr = context.getExpression(i >>> 2);
|
||||||
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
|
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
|
||||||
if (Array.isArray(expr) && expr[0] === "NOT" && Array.isArray(expr[1])) {
|
if (Array.isArray(expr) && expr[0] === "NOT" && Array.isArray(expr[1])) {
|
||||||
const e: Expression[] = expr[1];
|
const e: Expression[] = expr[1];
|
||||||
@@ -832,163 +825,196 @@ function sopToExpression(
|
|||||||
return res[0];
|
return res[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateDcSetAndIsNull(
|
function findIsNullDeps(exp: Expression): Expression[] {
|
||||||
variables: Map<number, Expression>
|
if (!Array.isArray(exp)) return [];
|
||||||
): { dcSet: number[][]; isNull: Map<number, number> } {
|
const op = exp[0];
|
||||||
const relations: Map<
|
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") {
|
||||||
|
return findIsNullDeps(exp[2]);
|
||||||
|
} else if (exp[1] === "ROUND") {
|
||||||
|
return exp
|
||||||
|
.slice(2, 4)
|
||||||
|
.map((e) => findIsNullDeps(e))
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
|
} else if (op !== "PARAM") {
|
||||||
|
return exp
|
||||||
|
.slice(1)
|
||||||
|
.map((e) => findIsNullDeps(e))
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
|
return [exp];
|
||||||
|
}
|
||||||
|
|
||||||
|
class SynthContext {
|
||||||
|
private declare _dcSet: number[][];
|
||||||
|
private declare _variables: Map<string, number>;
|
||||||
|
private declare _expressions: Map<number, Expression>;
|
||||||
|
private declare _isNullRelations: Map<number, number[]>;
|
||||||
|
private declare _isNullVars: Set<number>;
|
||||||
|
private declare _comparisons: Map<
|
||||||
string,
|
string,
|
||||||
{ op: string; rhs: boolean | number | string; var: number }[]
|
Map<string, { eq: number; gt: number; lt: number }>
|
||||||
> = new Map();
|
>;
|
||||||
|
|
||||||
for (const [n, e] of variables) {
|
constructor() {
|
||||||
const op = e[0] as string;
|
this._dcSet = [];
|
||||||
if (["=", ">", "<"].includes(op) && !Array.isArray(e[2])) {
|
this._variables = new Map();
|
||||||
const lhs = JSON.stringify(e[1]);
|
this._expressions = new Map();
|
||||||
let r = relations.get(lhs);
|
this._isNullRelations = new Map();
|
||||||
if (!r) relations.set(lhs, (r = []));
|
this._isNullVars = new Set();
|
||||||
r.push({ op: op, rhs: e[2], var: n });
|
this._comparisons = new Map();
|
||||||
} else if (op === "LIKE" && !Array.isArray(e[2]) && !Array.isArray(e[3])) {
|
|
||||||
const lhs = JSON.stringify(e[1]);
|
|
||||||
let r = relations.get(lhs);
|
|
||||||
if (!r) relations.set(lhs, (r = []));
|
|
||||||
r.push({ op: "", rhs: null, var: n });
|
|
||||||
} else {
|
|
||||||
const lhs = JSON.stringify(e);
|
|
||||||
let r = relations.get(lhs);
|
|
||||||
if (!r) relations.set(lhs, (r = []));
|
|
||||||
r.push({ op: "", rhs: null, var: n });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dcSet: number[][] = [];
|
getVariable(exp: Expression): number {
|
||||||
const isNull: Map<number, number> = new Map();
|
const expStr = JSON.stringify(exp);
|
||||||
|
if (this._variables.has(expStr)) return this._variables.get(expStr);
|
||||||
|
const v = this._variables.size;
|
||||||
|
this._variables.set(expStr, v);
|
||||||
|
this._expressions.set(v, exp);
|
||||||
|
this.generateIsNullDc(v);
|
||||||
|
this.generateComparisionDc(v);
|
||||||
|
|
||||||
for (const [key, rels] of relations) {
|
if (
|
||||||
const n = variables.size;
|
expStr === '["IS NULL",["PARAM","DeviceID.ID"]]' ||
|
||||||
variables.set(n, ["IS NULL", JSON.parse(key)]);
|
expStr === '["IS NULL",["PARAM","_id"]]'
|
||||||
|
)
|
||||||
|
this._dcSet.push([(v << 2) ^ 3]);
|
||||||
|
|
||||||
if (key === '["PARAM","DeviceID.ID"]' || key === '["PARAM","_id"]')
|
return v;
|
||||||
dcSet.push([(n << 2) ^ 3]);
|
|
||||||
|
|
||||||
const existingOps = new Map() as Map<string, Map<string, number>>;
|
|
||||||
for (const rel of rels) {
|
|
||||||
if (!rel.op) continue;
|
|
||||||
const rhsStr = JSON.stringify(rel.rhs);
|
|
||||||
const s = existingOps.get(rhsStr);
|
|
||||||
if (s) s.set(rel.op, rel.var);
|
|
||||||
else existingOps.set(rhsStr, new Map([[rel.op, rel.var]]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [k, v] of existingOps) {
|
private generateIsNullDc(v: number): void {
|
||||||
if (v.size > 1) {
|
this._isNullRelations.set(v, []);
|
||||||
if (!v.has("=")) {
|
const exp = this._expressions.get(v);
|
||||||
const va = variables.size;
|
|
||||||
variables.set(va, ["=", JSON.parse(key), JSON.parse(k)]);
|
const isNull = new Set(
|
||||||
rels.push({ op: "=", rhs: JSON.parse(k), var: va });
|
findIsNullDeps(exp).map((e) => this.getVariable(["IS NULL", e]))
|
||||||
v.set("=", va);
|
);
|
||||||
|
|
||||||
|
if (!isNull.size) return;
|
||||||
|
|
||||||
|
for (const n of isNull) {
|
||||||
|
this._isNullVars.add(n);
|
||||||
|
this._isNullRelations.get(n).push(v);
|
||||||
|
this._isNullRelations.get(v).push(n);
|
||||||
|
this._dcSet.push([(n << 2) ^ 3, (v << 2) ^ 1]);
|
||||||
|
this._dcSet.push([(n << 2) ^ 3, (v << 2) ^ 3]);
|
||||||
|
}
|
||||||
|
this._dcSet.push(
|
||||||
|
[...isNull].map((n) => (n << 2) ^ 2).concat([(v << 2) ^ 0, (v << 2) ^ 2])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!v.has(">")) {
|
private generateComparisionDc(v: number): void {
|
||||||
const va = variables.size;
|
const exp = this._expressions.get(v);
|
||||||
variables.set(va, [">", JSON.parse(key), JSON.parse(k)]);
|
if (!Array.isArray(exp)) return;
|
||||||
rels.push({ op: ">", rhs: JSON.parse(k), var: va });
|
const op = exp[0];
|
||||||
v.set(">", va);
|
if (![">", "<", "="].includes(op)) return;
|
||||||
}
|
const rhs = exp[2];
|
||||||
|
const rhsStr = JSON.stringify(rhs);
|
||||||
|
const lhsStr = JSON.stringify(exp[1]);
|
||||||
|
let allComps = this._comparisons.get(lhsStr);
|
||||||
|
if (!allComps) this._comparisons.set(lhsStr, (allComps = new Map()));
|
||||||
|
if (allComps.has(rhsStr)) return;
|
||||||
|
|
||||||
if (!v.has("<")) {
|
const comp = {
|
||||||
const va = variables.size;
|
eq: -1,
|
||||||
variables.set(va, ["<", JSON.parse(key), JSON.parse(k)]);
|
gt: -1,
|
||||||
rels.push({ op: "<", rhs: JSON.parse(k), var: va });
|
lt: -1,
|
||||||
v.set("<", va);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
dcSet.push([
|
allComps.set(rhsStr, comp);
|
||||||
(v.get("=") << 2) ^ 1,
|
|
||||||
(v.get(">") << 2) ^ 1,
|
comp.eq = this.getVariable(["=", exp[1], exp[2]]);
|
||||||
(v.get("<") << 2) ^ 1,
|
comp.gt = this.getVariable([">", exp[1], exp[2]]);
|
||||||
|
comp.lt = this.getVariable(["<", exp[1], exp[2]]);
|
||||||
|
|
||||||
|
this._dcSet.push([
|
||||||
|
(comp.eq << 2) ^ 1,
|
||||||
|
(comp.gt << 2) ^ 1,
|
||||||
|
(comp.lt << 2) ^ 1,
|
||||||
]);
|
]);
|
||||||
|
this._dcSet.push([(comp.eq << 2) ^ 3, (comp.gt << 2) ^ 3]);
|
||||||
|
this._dcSet.push([(comp.eq << 2) ^ 3, (comp.lt << 2) ^ 3]);
|
||||||
|
this._dcSet.push([(comp.gt << 2) ^ 3, (comp.lt << 2) ^ 3]);
|
||||||
|
|
||||||
|
const type1 = typeof exp[2];
|
||||||
|
if (!["boolean", "number", "string"].includes(type1)) return;
|
||||||
|
|
||||||
|
for (const [rhs2Str, comp2] of allComps) {
|
||||||
|
if (comp2 === comp) continue;
|
||||||
|
const rhs2 = JSON.parse(rhs2Str);
|
||||||
|
const type2 = typeof rhs2;
|
||||||
|
|
||||||
|
if (!["boolean", "number", "string"].includes(type2)) continue;
|
||||||
|
let cmp = 0;
|
||||||
|
if (type1 === type2) cmp = rhs > rhs2 ? 1 : -1;
|
||||||
|
else if (type1 === "string") cmp = 1;
|
||||||
|
else if (type2 === "string") cmp = -1;
|
||||||
|
else cmp = +rhs - +rhs2;
|
||||||
|
|
||||||
|
const c1 = cmp > 0 ? comp : comp2;
|
||||||
|
const c2 = cmp > 0 ? comp2 : comp;
|
||||||
|
|
||||||
|
// This is the minimum clauses required if all relavent vars
|
||||||
|
// were included in the DC set.
|
||||||
|
// this._dcSet.push([(c1.eq << 2) ^ 3, (c2.eq << 2) ^ 3]);
|
||||||
|
// this._dcSet.push([(c1.lt << 2) ^ 1, (c2.gt << 2) ^ 1]);
|
||||||
|
|
||||||
|
// But we use non-minimal set because intermediate vars
|
||||||
|
// between any two may not be present in the DC set.
|
||||||
|
this._dcSet.push([(c1.lt << 2) ^ 1, (c2.gt << 2) ^ 1]);
|
||||||
|
this._dcSet.push([(c1.lt << 2) ^ 1, (c2.eq << 2) ^ 3]);
|
||||||
|
this._dcSet.push([(c1.lt << 2) ^ 1, (c2.lt << 2) ^ 3]);
|
||||||
|
this._dcSet.push([(c1.eq << 2) ^ 3, (c2.gt << 2) ^ 1]);
|
||||||
|
this._dcSet.push([(c1.eq << 2) ^ 3, (c2.eq << 2) ^ 3]);
|
||||||
|
this._dcSet.push([(c1.eq << 2) ^ 3, (c2.lt << 2) ^ 3]);
|
||||||
|
this._dcSet.push([(c1.gt << 2) ^ 3, (c2.gt << 2) ^ 1]);
|
||||||
|
this._dcSet.push([(c1.gt << 2) ^ 3, (c2.eq << 2) ^ 3]);
|
||||||
|
this._dcSet.push([(c1.gt << 2) ^ 3, (c2.lt << 2) ^ 3]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < rels.length; ++i) {
|
getExpression(v: number): Expression {
|
||||||
const rel1 = rels[i];
|
return this._expressions.get(v);
|
||||||
isNull.set(rel1.var, n);
|
|
||||||
|
|
||||||
dcSet.push([(n << 2) ^ 3, (rel1.var << 2) ^ 1]);
|
|
||||||
dcSet.push([(n << 2) ^ 3, (rel1.var << 2) ^ 3]);
|
|
||||||
dcSet.push([(rel1.var << 2) ^ 0, (rel1.var << 2) ^ 2, (n << 2) ^ 2]);
|
|
||||||
|
|
||||||
if (!rel1.op) continue;
|
|
||||||
for (let j = i + 1; j < rels.length; ++j) {
|
|
||||||
const rel2 = rels[j];
|
|
||||||
if (!rel2.op) continue;
|
|
||||||
let comp = 0;
|
|
||||||
if (Array.isArray(rel1.rhs) || Array.isArray(rel2.rhs)) {
|
|
||||||
if (JSON.stringify(rel1.rhs) !== JSON.stringify(rel2.rhs)) continue;
|
|
||||||
} else if (typeof rel1.rhs === "string") {
|
|
||||||
if (typeof rel2.rhs === "string") {
|
|
||||||
if (rel1.rhs > rel2.rhs) comp = 1;
|
|
||||||
else if (rel1.rhs < rel2.rhs) comp = -1;
|
|
||||||
} else {
|
|
||||||
comp = 1;
|
|
||||||
}
|
|
||||||
} else if (typeof rel2.rhs === "string") {
|
|
||||||
comp = -1;
|
|
||||||
} else {
|
|
||||||
comp = +rel1.rhs - +rel2.rhs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rel1.op === rel2.op) {
|
getIsNullRelations(v: number): number[] {
|
||||||
if (comp === 0)
|
return this._isNullRelations.get(v);
|
||||||
dcSet.push([(rel1.var << 2) ^ 1, (rel2.var << 2) ^ 1]);
|
|
||||||
else if (rel1.op === "=")
|
|
||||||
dcSet.push([(rel1.var << 2) ^ 3, (rel2.var << 2) ^ 3]);
|
|
||||||
else if (
|
|
||||||
(rel1.op === ">" && comp > 0) ||
|
|
||||||
(rel1.op === "<" && comp < 0)
|
|
||||||
)
|
|
||||||
dcSet.push([(rel1.var << 2) ^ 3, (rel2.var << 2) ^ 1]);
|
|
||||||
else if (
|
|
||||||
(rel1.op === "<" && comp > 0) ||
|
|
||||||
(rel1.op === ">" && comp < 0)
|
|
||||||
)
|
|
||||||
dcSet.push([(rel1.var << 2) ^ 1, (rel2.var << 2) ^ 3]);
|
|
||||||
} else if (rel1.op === "=" || rel2.op === "=") {
|
|
||||||
let r1 = rel1,
|
|
||||||
r2 = rel2,
|
|
||||||
cmp = comp;
|
|
||||||
if (rel1.op !== "=") {
|
|
||||||
r2 = rel1;
|
|
||||||
r1 = rel2;
|
|
||||||
cmp = -comp;
|
|
||||||
}
|
|
||||||
if (r2.op === ">" || r2.op === "<") {
|
|
||||||
if (cmp === 0) {
|
|
||||||
dcSet.push([(r1.var << 2) ^ 3, (r2.var << 2) ^ 3]);
|
|
||||||
} else {
|
|
||||||
if ((cmp > 0 && r2.op === ">") || (cmp < 0 && r2.op === "<"))
|
|
||||||
dcSet.push([(r1.var << 2) ^ 3, (r2.var << 2) ^ 1]);
|
|
||||||
else dcSet.push([(r1.var << 2) ^ 3, (r2.var << 2) ^ 3]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
[">", "<"].includes(rel1.op) &&
|
|
||||||
[">", "<"].includes(rel2.op)
|
|
||||||
) {
|
|
||||||
if ((rel1.op === ">" && comp < 0) || (rel1.op === "<" && comp > 0))
|
|
||||||
dcSet.push([(rel1.var << 2) ^ 1, (rel2.var << 2) ^ 1]);
|
|
||||||
else dcSet.push([(rel1.var << 2) ^ 3, (rel2.var << 2) ^ 3]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isIsNull(v: number): boolean {
|
||||||
|
return this._isNullVars.has(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDcSet(minterms?: Minterm[]): number[][] {
|
||||||
|
if (!minterms) return this._dcSet;
|
||||||
|
const vars = new Set(minterms.flat().map((v) => v >> 2));
|
||||||
|
|
||||||
|
for (const comps of this._comparisons.values()) {
|
||||||
|
for (const comp of comps.values()) {
|
||||||
|
const c = Object.values(comp).filter((v) => !vars.has(v));
|
||||||
|
if (c.length === 1) vars.add(c[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { dcSet, isNull };
|
for (const n of this._isNullVars)
|
||||||
|
if (this._isNullRelations.get(n).some((v) => vars.has(v))) vars.add(n);
|
||||||
|
|
||||||
|
const dcSet = this._dcSet.filter((m) => m.every((v) => vars.has(v >> 2)));
|
||||||
|
|
||||||
|
return dcSet;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeMinterms(
|
function sanitizeMinterms(
|
||||||
minterms: number[][],
|
minterms: number[][],
|
||||||
isNull: Map<number, number>
|
context: SynthContext
|
||||||
): number[][] {
|
): number[][] {
|
||||||
const res = [] as number[][];
|
const res = [] as number[][];
|
||||||
loop: for (const m of minterms) {
|
loop: for (const m of minterms) {
|
||||||
@@ -999,13 +1025,20 @@ function sanitizeMinterms(
|
|||||||
const perms: number[][] = [];
|
const perms: number[][] = [];
|
||||||
for (const [k, v] of merged) {
|
for (const [k, v] of merged) {
|
||||||
if (v === 0b1010) continue loop;
|
if (v === 0b1010) continue loop;
|
||||||
const n = isNull.get(k) << 2;
|
const isNullVars = context.getIsNullRelations(k);
|
||||||
const t = k << 2;
|
const t = k << 2;
|
||||||
if (v === 0b0101) minterm.push(n ^ 3);
|
if (v === 0b0101) {
|
||||||
else if (v === 0b0001) perms.push([n ^ 3, t ^ 3]);
|
if (isNullVars.length === 1) minterm.push((isNullVars[0] << 2) ^ 3);
|
||||||
else if (v === 0b0100) perms.push([n ^ 3, t ^ 1]);
|
else perms.push(isNullVars.map((n) => (n << 2) ^ 3));
|
||||||
else if (v & 0b1000) minterm.push(t ^ 3);
|
} else if (v === 0b0001) {
|
||||||
else if (v & 0b0010) minterm.push(t ^ 1);
|
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];
|
let ms = [minterm];
|
||||||
while (perms.length) {
|
while (perms.length) {
|
||||||
@@ -1020,17 +1053,13 @@ function sanitizeMinterms(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function boolExprSynthToExpression(boolExpr: BoolExprSynth): Expression {
|
function boolExprSynthToExpression(boolExpr: BoolExprSynth): Expression {
|
||||||
const context: Map<string, number> = new Map();
|
const context = new SynthContext();
|
||||||
let minterms = boolExpr.true(context);
|
let minterms = boolExpr.true(context);
|
||||||
const variables = new Map(
|
minterms = sanitizeMinterms(minterms, context);
|
||||||
Array.from(context).map(([k, v]) => [v, JSON.parse(k) as Expression])
|
const canRaise = getCanRaiseCallback(context);
|
||||||
);
|
const dcSet = context.getDcSet(minterms);
|
||||||
const { dcSet, isNull } = generateDcSetAndIsNull(variables);
|
|
||||||
minterms = sanitizeMinterms(minterms, isNull);
|
|
||||||
|
|
||||||
const canRaise = getCanRaiseCallback(isNull);
|
|
||||||
minterms = espresso(minterms, dcSet, { canRaise });
|
minterms = espresso(minterms, dcSet, { canRaise });
|
||||||
return sopToExpression(minterms, variables);
|
return sopToExpression(minterms, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCallback(exp: Expression | BoolExprSynth): Expression {
|
function mapCallback(exp: Expression | BoolExprSynth): Expression {
|
||||||
@@ -1043,22 +1072,22 @@ function mapCallback(exp: Expression | BoolExprSynth): Expression {
|
|||||||
|
|
||||||
const op = exp[0];
|
const op = exp[0];
|
||||||
if (op === "IS NULL")
|
if (op === "IS NULL")
|
||||||
return (new IsNullSynth(toBoolExprSynth(exp[1])) as unknown) as Expression;
|
return new IsNullSynth(toBoolExprSynth(exp[1])) as unknown as Expression;
|
||||||
|
|
||||||
if (op === "IS NOT NULL") {
|
if (op === "IS NOT NULL") {
|
||||||
return (new NotSynth(
|
return new NotSynth(
|
||||||
new IsNullSynth(toBoolExprSynth(exp[1]))
|
new IsNullSynth(toBoolExprSynth(exp[1]))
|
||||||
) as unknown) as Expression;
|
) as unknown as Expression;
|
||||||
} else if (op === "NOT") {
|
} else if (op === "NOT") {
|
||||||
return (new NotSynth(toBoolExprSynth(exp[1])) as unknown) as Expression;
|
return new NotSynth(toBoolExprSynth(exp[1])) as unknown as Expression;
|
||||||
} else if (op === "OR") {
|
} else if (op === "OR") {
|
||||||
return (new OrSynth(
|
return new OrSynth(
|
||||||
...exp.slice(1).map((a) => toBoolExprSynth(a))
|
...exp.slice(1).map((a) => toBoolExprSynth(a))
|
||||||
) as unknown) as Expression;
|
) as unknown as Expression;
|
||||||
} else if (op === "AND") {
|
} else if (op === "AND") {
|
||||||
return (new AndSynth(
|
return new AndSynth(
|
||||||
...exp.slice(1).map((a) => toBoolExprSynth(a))
|
...exp.slice(1).map((a) => toBoolExprSynth(a))
|
||||||
) as unknown) as Expression;
|
) as unknown as Expression;
|
||||||
}
|
}
|
||||||
for (let i = 1; i < exp.length; ++i) {
|
for (let i = 1; i < exp.length; ++i) {
|
||||||
if (exp[i] instanceof BoolExprSynth) {
|
if (exp[i] instanceof BoolExprSynth) {
|
||||||
@@ -1074,19 +1103,14 @@ function mapCallback(exp: Expression | BoolExprSynth): Expression {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCanRaiseCallback(
|
function getCanRaiseCallback(
|
||||||
isNull: Map<number, number>
|
context: SynthContext
|
||||||
): (idx: number, set: Set<number>) => boolean {
|
): (idx: number, set: Set<number>) => boolean {
|
||||||
return (idx: number, set: Set<number>): boolean => {
|
return (idx: number, set: Set<number>): boolean => {
|
||||||
const i = idx >> 2;
|
const i = idx >> 2;
|
||||||
const n = isNull.get(i);
|
const vars = context.getIsNullRelations(i);
|
||||||
if (n != null) {
|
if (!context.isIsNull(i)) return !(idx & 1) || !set.has(idx ^ 3);
|
||||||
if (!(idx & 1)) return true;
|
|
||||||
if (set.has((n << 2) ^ 2)) return true;
|
for (const k of vars) {
|
||||||
if (set.has((n << 2) ^ 3)) return true;
|
|
||||||
return !set.has(idx ^ 3);
|
|
||||||
}
|
|
||||||
for (const [k, v] of isNull) {
|
|
||||||
if (v !== i) continue;
|
|
||||||
if (set.has((k << 2) ^ 1)) continue;
|
if (set.has((k << 2) ^ 1)) continue;
|
||||||
if (set.has((k << 2) ^ 3)) continue;
|
if (set.has((k << 2) ^ 3)) continue;
|
||||||
if (set.has((k << 2) ^ 0)) return false;
|
if (set.has((k << 2) ^ 0)) return false;
|
||||||
@@ -1101,33 +1125,31 @@ export function minimize(expr: Expression, boolean = false): Expression {
|
|||||||
expr = map(expr, mapCallback);
|
expr = map(expr, mapCallback);
|
||||||
if (Array.isArray(expr) && expr[0] === "CASE") {
|
if (Array.isArray(expr) && expr[0] === "CASE") {
|
||||||
if (!boolean) {
|
if (!boolean) {
|
||||||
const context: Map<string, number> = new Map();
|
const context = new SynthContext();
|
||||||
const whens = expr.filter((e, i) => i % 2).map((e) => e.true(context));
|
const whens = expr.filter((e, i) => i % 2).map((e) => e.true(context));
|
||||||
const variables = new Map(
|
const caseDcSet: Minterm[] = [];
|
||||||
Array.from(context).map(([k, v]) => [v, JSON.parse(k) as Expression])
|
const canRaise = getCanRaiseCallback(context);
|
||||||
);
|
|
||||||
const { dcSet, isNull } = generateDcSetAndIsNull(variables);
|
|
||||||
const canRaise = getCanRaiseCallback(isNull);
|
|
||||||
const res: Expression = ["CASE"];
|
const res: Expression = ["CASE"];
|
||||||
for (let i = 1; i < expr.length; i += 2) {
|
for (let i = 1; i < expr.length; i += 2) {
|
||||||
let minterms = sanitizeMinterms(whens[(i - 1) / 2], isNull);
|
let minterms = sanitizeMinterms(whens[(i - 1) / 2], context);
|
||||||
|
const dcSet = context.getDcSet(minterms.concat(caseDcSet));
|
||||||
minterms = espresso(minterms, dcSet, { canRaise });
|
minterms = espresso(minterms, dcSet, { canRaise });
|
||||||
if (!minterms.length) continue;
|
if (!minterms.length) continue;
|
||||||
const w = sopToExpression(minterms, variables);
|
const w = sopToExpression(minterms, context);
|
||||||
let t = expr[i + 1];
|
let t = expr[i + 1];
|
||||||
if (t instanceof BoolExprSynth) t = boolExprSynthToExpression(t);
|
if (t instanceof BoolExprSynth) t = boolExprSynthToExpression(t);
|
||||||
res.push(w, t);
|
res.push(w, t);
|
||||||
if (w === true) break;
|
if (w === true) break;
|
||||||
dcSet.push(...minterms);
|
caseDcSet.push(...minterms);
|
||||||
}
|
}
|
||||||
while (res[res.length - 1] == null) res.splice(-2);
|
while (res[res.length - 1] == null) res.splice(-2);
|
||||||
if (res.length < 3) return null;
|
if (res.length < 3) return null;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
expr = (toBoolExprSynth(expr) as unknown) as Expression;
|
expr = toBoolExprSynth(expr) as unknown as Expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boolean) expr = (toBoolExprSynth(expr) as unknown) as Expression;
|
if (boolean) expr = toBoolExprSynth(expr) as unknown as Expression;
|
||||||
|
|
||||||
if (expr instanceof BoolExprSynth) expr = boolExprSynthToExpression(expr);
|
if (expr instanceof BoolExprSynth) expr = boolExprSynthToExpression(expr);
|
||||||
return expr;
|
return expr;
|
||||||
@@ -1145,7 +1167,7 @@ export function unionDiff(
|
|||||||
|
|
||||||
if (!expr1) {
|
if (!expr1) {
|
||||||
if (Array.isArray(expr2) && expr2[0] === "CASE")
|
if (Array.isArray(expr2) && expr2[0] === "CASE")
|
||||||
expr2 = (toBoolExprSynth(expr2) as unknown) as Expression;
|
expr2 = toBoolExprSynth(expr2) as unknown as Expression;
|
||||||
if (expr2 instanceof BoolExprSynth)
|
if (expr2 instanceof BoolExprSynth)
|
||||||
expr2 = boolExprSynthToExpression(expr2);
|
expr2 = boolExprSynthToExpression(expr2);
|
||||||
return [expr2, expr2];
|
return [expr2, expr2];
|
||||||
@@ -1156,35 +1178,37 @@ export function unionDiff(
|
|||||||
expr1 = map(expr1, mapCallback);
|
expr1 = map(expr1, mapCallback);
|
||||||
const b1 = toBoolExprSynth(expr1);
|
const b1 = toBoolExprSynth(expr1);
|
||||||
|
|
||||||
const context: Map<string, number> = new Map();
|
const context = new SynthContext();
|
||||||
|
|
||||||
const expr2Minterms = b2.true(context);
|
const expr2Minterms = b2.true(context);
|
||||||
const expr1Minterms = b1.true(context);
|
const expr1Minterms = b1.true(context);
|
||||||
const expr1NullMinterms = b1.null(context);
|
const expr1NullMinterms = b1.null(context);
|
||||||
const expr1FalseMinterms = b1.false(context);
|
const expr1FalseMinterms = b1.false(context);
|
||||||
const variables = new Map(
|
|
||||||
Array.from(context).map(([k, v]) => [v, JSON.parse(k) as Expression])
|
|
||||||
);
|
|
||||||
const { dcSet, isNull } = generateDcSetAndIsNull(variables);
|
|
||||||
|
|
||||||
const unionMinterms = sanitizeMinterms(
|
const unionMinterms = sanitizeMinterms(
|
||||||
[...expr1Minterms, ...expr2Minterms],
|
[...expr1Minterms, ...expr2Minterms],
|
||||||
isNull
|
context
|
||||||
);
|
);
|
||||||
|
|
||||||
const diffMinterms = sanitizeMinterms(
|
const diffMinterms = sanitizeMinterms(
|
||||||
complement([
|
complement([
|
||||||
...complement([...expr1NullMinterms, ...expr1FalseMinterms]),
|
...complement([...expr1NullMinterms, ...expr1FalseMinterms]),
|
||||||
...complement(expr2Minterms),
|
...complement(expr2Minterms),
|
||||||
]),
|
]),
|
||||||
isNull
|
context
|
||||||
);
|
);
|
||||||
|
|
||||||
const canRaise = getCanRaiseCallback(isNull);
|
const canRaise = getCanRaiseCallback(context);
|
||||||
|
|
||||||
const union = espresso(unionMinterms, dcSet, { canRaise });
|
const union = espresso(unionMinterms, context.getDcSet(unionMinterms), {
|
||||||
const diff = espresso(diffMinterms, dcSet, { canRaise });
|
canRaise,
|
||||||
|
});
|
||||||
|
|
||||||
return [sopToExpression(union, variables), sopToExpression(diff, variables)];
|
const diff = espresso(diffMinterms, context.getDcSet(diffMinterms), {
|
||||||
|
canRaise,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [sopToExpression(union, context), sopToExpression(diff, context)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function covers(expr1: Expression, expr2: Expression): boolean {
|
export function covers(expr1: Expression, expr2: Expression): boolean {
|
||||||
@@ -1198,13 +1222,13 @@ export function covers(expr1: Expression, expr2: Expression): boolean {
|
|||||||
expr2 = map(expr2, mapCallback);
|
expr2 = map(expr2, mapCallback);
|
||||||
const b2 = toBoolExprSynth(expr2);
|
const b2 = toBoolExprSynth(expr2);
|
||||||
|
|
||||||
const context: Map<string, number> = new Map();
|
const context = new SynthContext();
|
||||||
const expr1Minterms = b1.true(context);
|
const expr1Minterms = b1.true(context);
|
||||||
const expr2Minterms = b2.true(context);
|
const expr2Minterms = b2.true(context);
|
||||||
const variables = new Map(
|
|
||||||
Array.from(context).map(([k, v]) => [v, JSON.parse(k) as Expression])
|
|
||||||
);
|
|
||||||
const { dcSet } = generateDcSetAndIsNull(variables);
|
|
||||||
|
|
||||||
return tautology([...complement(expr2Minterms), ...dcSet, ...expr1Minterms]);
|
return tautology([
|
||||||
|
...complement(expr2Minterms),
|
||||||
|
...context.getDcSet(),
|
||||||
|
...expr1Minterms,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ function interpretEscapes(str): string {
|
|||||||
r: "\r",
|
r: "\r",
|
||||||
t: "\t",
|
t: "\t",
|
||||||
};
|
};
|
||||||
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/, (_, escape) => {
|
return str.replace(/\\(u[0-9a-fA-F]{4}|[^u])/g, (_, 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));
|
||||||
@@ -87,8 +87,6 @@ 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)
|
||||||
);
|
);
|
||||||
@@ -287,7 +285,11 @@ const lang = parsimmon.createLanguage({
|
|||||||
.skip(parsimmon.optWhitespace),
|
.skip(parsimmon.optWhitespace),
|
||||||
binaryLeft(
|
binaryLeft(
|
||||||
parsimmon
|
parsimmon
|
||||||
.alt(parsimmon.string("*"), parsimmon.string("/"))
|
.alt(
|
||||||
|
parsimmon.string("*"),
|
||||||
|
parsimmon.string("/"),
|
||||||
|
parsimmon.string("%")
|
||||||
|
)
|
||||||
.skip(parsimmon.optWhitespace),
|
.skip(parsimmon.optWhitespace),
|
||||||
parsimmon.alt(
|
parsimmon.alt(
|
||||||
r.Value,
|
r.Value,
|
||||||
@@ -386,6 +388,7 @@ export function stringify(exp: Expression, level = 0): string {
|
|||||||
"-": 31,
|
"-": 31,
|
||||||
"*": 32,
|
"*": 32,
|
||||||
"/": 32,
|
"/": 32,
|
||||||
|
"%": 32,
|
||||||
};
|
};
|
||||||
|
|
||||||
const op = exp[0].toUpperCase();
|
const op = exp[0].toUpperCase();
|
||||||
|
|||||||
+27
-14
@@ -130,6 +130,15 @@ export function evaluateCallback(exp: Expression): Expression {
|
|||||||
} else if (exp[1] === "LOWER") {
|
} else if (exp[1] === "LOWER") {
|
||||||
if (exp[2] == null) return null;
|
if (exp[2] == null) return null;
|
||||||
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
|
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
|
||||||
|
} else if (exp[1] === "ROUND") {
|
||||||
|
const n = exp[2];
|
||||||
|
const p = exp.length > 3 ? exp[3] : 0;
|
||||||
|
if (n == null || p == null) return null;
|
||||||
|
if (!isArray(n) && !isArray(p)) {
|
||||||
|
const d = 10 ** p;
|
||||||
|
const m = n * d * (1 + Number.EPSILON);
|
||||||
|
return Math.round(m) / d;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (exp[0] === "PARAM") {
|
} else if (exp[0] === "PARAM") {
|
||||||
if (exp[1] == null) return null;
|
if (exp[1] == null) return null;
|
||||||
@@ -169,11 +178,11 @@ export function evaluateCallback(exp: Expression): Expression {
|
|||||||
} else if (exp[0] === "IS NULL") {
|
} else if (exp[0] === "IS NULL") {
|
||||||
if (isArray(exp[1])) return exp;
|
if (isArray(exp[1])) return exp;
|
||||||
else if (exp[1] == null) return true;
|
else if (exp[1] == null) return true;
|
||||||
else return null;
|
else return false;
|
||||||
} else if (exp[0] === "IS NOT NULL") {
|
} else if (exp[0] === "IS NOT NULL") {
|
||||||
if (isArray(exp[1])) return exp;
|
if (isArray(exp[1])) return exp;
|
||||||
else if (exp[1] != null) return true;
|
else if (exp[1] != null) return true;
|
||||||
else return null;
|
else return false;
|
||||||
} else if (exp[0] === "LIKE") {
|
} else if (exp[0] === "LIKE") {
|
||||||
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
|
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
|
||||||
else if (
|
else if (
|
||||||
@@ -235,9 +244,12 @@ export function evaluateCallback(exp: Expression): Expression {
|
|||||||
} else if (exp[0] === "/") {
|
} else if (exp[0] === "/") {
|
||||||
return reduce(exp, (a, b, i) => {
|
return reduce(exp, (a, b, i) => {
|
||||||
if (a == null || b == null) return null;
|
if (a == null || b == null) return null;
|
||||||
if (!isArray(a) && !isArray(b))
|
if (isArray(a) || isArray(b)) return REDUCE_SKIP;
|
||||||
return i === 0 ? toNumber(a) / toNumber(b) : toNumber(a) * toNumber(b);
|
const n1 = toNumber(a);
|
||||||
return REDUCE_SKIP;
|
const n2 = toNumber(b);
|
||||||
|
if (i !== 0) return n1 * n2;
|
||||||
|
if (n2 === 0) return null;
|
||||||
|
return n1 / n2;
|
||||||
});
|
});
|
||||||
} else if (exp[0] === "+") {
|
} else if (exp[0] === "+") {
|
||||||
return reduce(exp, (a, b) => {
|
return reduce(exp, (a, b) => {
|
||||||
@@ -252,6 +264,15 @@ export function evaluateCallback(exp: Expression): Expression {
|
|||||||
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
|
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
|
||||||
return REDUCE_SKIP;
|
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] === "||") {
|
} else if (exp[0] === "||") {
|
||||||
return reduce(exp, (a, b) => {
|
return reduce(exp, (a, b) => {
|
||||||
if (a == null || b == null) return null;
|
if (a == null || b == null) return null;
|
||||||
@@ -325,15 +346,7 @@ export async function evaluateAsync(
|
|||||||
if (!isArray(e)) return e;
|
if (!isArray(e)) return e;
|
||||||
|
|
||||||
if (e[0] === "FUNC") {
|
if (e[0] === "FUNC") {
|
||||||
if (e[1] === "NOW") {
|
if (e[1] === "NOW") if (now) return now;
|
||||||
if (now) return now;
|
|
||||||
} else if (e[1] === "UPPER") {
|
|
||||||
if (e[2] == null) return null;
|
|
||||||
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
|
|
||||||
} else if (e[1] === "LOWER") {
|
|
||||||
if (e[2] == null) return null;
|
|
||||||
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
|
|
||||||
}
|
|
||||||
} else if (e[0] === "PARAM") {
|
} else if (e[0] === "PARAM") {
|
||||||
if (e[1] == null) return null;
|
if (e[1] == null) return null;
|
||||||
if (obj && !isArray(e[1])) {
|
if (obj && !isArray(e[1])) {
|
||||||
|
|||||||
+12
-3
@@ -47,9 +47,18 @@ 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)) r = cache2.get(key);
|
if (cache2.has(key)) {
|
||||||
else r = func(...args);
|
cache1.set(key, (r = cache2.get(key)));
|
||||||
cache1.set(key, r);
|
} else {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
import Path from "./path";
|
import Path from "./path";
|
||||||
|
|
||||||
export default class PathSet {
|
export default class PathSet {
|
||||||
private lengthIndex: Set<Path>[];
|
private declare lengthIndex: Set<Path>[];
|
||||||
private fragmentIndex: Map<string, Set<Path>>[];
|
private declare fragmentIndex: Map<string, Set<Path>>[];
|
||||||
private stringIndex: Map<string, Path>;
|
private declare stringIndex: Map<string, Path>;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.lengthIndex = [];
|
this.lengthIndex = [];
|
||||||
|
|||||||
+109
-54
@@ -23,44 +23,102 @@ 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 readonly segments: Segments;
|
public declare readonly segments: Segments;
|
||||||
public readonly wildcard: number;
|
public declare readonly wildcard: number;
|
||||||
public readonly alias: number;
|
public declare readonly alias: number;
|
||||||
protected _string: string;
|
protected declare _string: string;
|
||||||
protected _stringIndex: number[];
|
protected declare _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 = [];
|
||||||
while (index < pattern.length && pattern[index] !== "]") {
|
let i = index;
|
||||||
const { index: idx, segments } = Path.parsePath(pattern, index);
|
for (;;) {
|
||||||
let j = (index = idx + 1);
|
let path: Path;
|
||||||
while (pattern[j] !== "]" && pattern[j] !== ",") {
|
let value: string;
|
||||||
if (pattern[j] === '"' && index === j) {
|
({ index: i, path } = Path.parseAliasPath(pattern, i));
|
||||||
++j;
|
if (!path) break;
|
||||||
while (pattern[j] !== '"' || pattern[j - 1] === "\\") {
|
({ index: i, value } = Path.parseAliasValue(pattern, i));
|
||||||
if (++j >= pattern.length)
|
if (value == null) break;
|
||||||
throw new Error("Invalid alias expression");
|
alias.push([path, value]);
|
||||||
}
|
index = i;
|
||||||
}
|
|
||||||
if (++j >= pattern.length) throw new Error("Invalid alias expression");
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = pattern.slice(index, j).trim();
|
if (charCodeAt(pattern, index) !== CHAR_COMMA) break;
|
||||||
index = j;
|
++i;
|
||||||
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
|
||||||
@@ -81,30 +139,26 @@ export default class Path {
|
|||||||
index: number
|
index: number
|
||||||
): { index: number; segments: Segments } {
|
): { index: number; segments: Segments } {
|
||||||
const segments = [];
|
const segments = [];
|
||||||
// Colon separator is needed for parseAlias
|
|
||||||
if (index < pattern.length && pattern[index] !== ":") {
|
|
||||||
for (;;) {
|
|
||||||
if (pattern[index] === "[") {
|
|
||||||
const { index: idx, alias } = Path.parseAlias(pattern, index + 1);
|
|
||||||
index = idx + 1;
|
|
||||||
segments.push(alias);
|
|
||||||
} else {
|
|
||||||
const j = index;
|
|
||||||
while (
|
|
||||||
index < pattern.length &&
|
|
||||||
pattern[index] !== ":" &&
|
|
||||||
pattern[index] !== "."
|
|
||||||
)
|
|
||||||
++index;
|
|
||||||
const s = pattern.slice(j, index).trim();
|
|
||||||
if (!s) throw new Error("Invalid parameter path");
|
|
||||||
segments.push(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index >= pattern.length || pattern[index] === ":") break;
|
let segStart = index;
|
||||||
else if (pattern[index] !== ".")
|
let wildcard = -1;
|
||||||
throw new Error("Invalid alias expression");
|
for (let i = index; i <= pattern.length; ++i) {
|
||||||
++index;
|
const c = charCodeAt(pattern, i);
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +197,8 @@ export default class Path {
|
|||||||
if (!path) {
|
if (!path) {
|
||||||
path = cache2.get(str);
|
path = cache2.get(str);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
const { segments } = Path.parsePath(str, 0);
|
const { index, 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
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";
|
||||||
|
}
|
||||||
+4
-4
@@ -43,7 +43,7 @@ const options = {
|
|||||||
|
|
||||||
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: "0.0.0.0" },
|
CWMP_INTERFACE: { type: "string", default: "::" },
|
||||||
CWMP_SSL_CERT: { type: "string", default: "" },
|
CWMP_SSL_CERT: { type: "string", default: "" },
|
||||||
CWMP_SSL_KEY: { type: "string", default: "" },
|
CWMP_SSL_KEY: { type: "string", default: "" },
|
||||||
CWMP_LOG_FILE: { type: "path", default: "" },
|
CWMP_LOG_FILE: { type: "path", default: "" },
|
||||||
@@ -51,7 +51,7 @@ const options = {
|
|||||||
|
|
||||||
NBI_WORKER_PROCESSES: { type: "int", default: 0 },
|
NBI_WORKER_PROCESSES: { type: "int", default: 0 },
|
||||||
NBI_PORT: { type: "int", default: 7557 },
|
NBI_PORT: { type: "int", default: 7557 },
|
||||||
NBI_INTERFACE: { type: "string", default: "0.0.0.0" },
|
NBI_INTERFACE: { type: "string", default: "::" },
|
||||||
NBI_SSL_CERT: { type: "string", default: "" },
|
NBI_SSL_CERT: { type: "string", default: "" },
|
||||||
NBI_SSL_KEY: { type: "string", default: "" },
|
NBI_SSL_KEY: { type: "string", default: "" },
|
||||||
NBI_LOG_FILE: { type: "path", default: "" },
|
NBI_LOG_FILE: { type: "path", default: "" },
|
||||||
@@ -59,7 +59,7 @@ 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: "0.0.0.0" },
|
FS_INTERFACE: { type: "string", default: "::" },
|
||||||
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_URL_PREFIX: { type: "string", default: "" },
|
||||||
@@ -68,7 +68,7 @@ const options = {
|
|||||||
|
|
||||||
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: "0.0.0.0" },
|
UI_INTERFACE: { type: "string", default: "::" },
|
||||||
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: "" },
|
||||||
|
|||||||
+23
-20
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import * as dgram from "dgram";
|
import * as dgram from "dgram";
|
||||||
import { parse } from "url";
|
import { URL } from "url";
|
||||||
import * as http from "http";
|
import * as http from "http";
|
||||||
import { evaluateAsync } from "./common/expression";
|
import { evaluateAsync } from "./common/expression";
|
||||||
import { Expression } from "./types";
|
import { Expression } from "./types";
|
||||||
@@ -61,24 +61,26 @@ async function extractAuth(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function httpGet(
|
function httpGet(
|
||||||
|
url: URL,
|
||||||
options: http.RequestOptions,
|
options: http.RequestOptions,
|
||||||
_debug: boolean,
|
_debug: boolean,
|
||||||
deviceId: string
|
deviceId: string
|
||||||
): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders }> {
|
): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const req = http
|
const req = http
|
||||||
.get(options, (res) => {
|
.get(url, 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, options, null);
|
debug.outgoingHttpRequest(req, deviceId, "GET", url, null);
|
||||||
debug.incomingHttpResponse(res, deviceId, null);
|
debug.incomingHttpResponse(res, deviceId, null);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("error", (err) => {
|
.on("error", (err) => {
|
||||||
req.destroy();
|
req.destroy();
|
||||||
reject(err);
|
reject(err);
|
||||||
if (_debug) debug.outgoingHttpRequestError(req, deviceId, options, err);
|
if (_debug)
|
||||||
|
debug.outgoingHttpRequestError(req, deviceId, "GET", url, err);
|
||||||
})
|
})
|
||||||
.on("timeout", () => {
|
.on("timeout", () => {
|
||||||
req.destroy();
|
req.destroy();
|
||||||
@@ -94,15 +96,13 @@ export async function httpConnectionRequest(
|
|||||||
_debug: boolean,
|
_debug: boolean,
|
||||||
deviceId: string
|
deviceId: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const options: http.RequestOptions = parse(address);
|
const url = new URL(address);
|
||||||
if (options.protocol !== "http:")
|
if (url.protocol !== "http:")
|
||||||
return "Invalid connection request URL or protocol";
|
return "Invalid connection request URL or protocol";
|
||||||
|
|
||||||
options.agent = new http.Agent({
|
const options: http.RequestOptions = {
|
||||||
maxSockets: 1,
|
agent: new http.Agent({ maxSockets: 1, keepAlive: true, timeout: timeout }),
|
||||||
keepAlive: true,
|
};
|
||||||
timeout: timeout,
|
|
||||||
});
|
|
||||||
|
|
||||||
let authHeader: Record<string, string>;
|
let authHeader: Record<string, string>;
|
||||||
let username: string;
|
let username: string;
|
||||||
@@ -129,7 +129,7 @@ export async function httpConnectionRequest(
|
|||||||
Authorization: auth.solveDigest(
|
Authorization: auth.solveDigest(
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
options.path,
|
url.pathname + url.search,
|
||||||
"GET",
|
"GET",
|
||||||
null,
|
null,
|
||||||
authHeader
|
authHeader
|
||||||
@@ -145,12 +145,12 @@ export async function httpConnectionRequest(
|
|||||||
|
|
||||||
let res: { statusCode: number; headers: http.IncomingHttpHeaders };
|
let res: { statusCode: number; headers: http.IncomingHttpHeaders };
|
||||||
try {
|
try {
|
||||||
res = await httpGet(opts, _debug, deviceId);
|
res = await httpGet(url, opts, _debug, deviceId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Workaround for some devices unexpectedly closing the connection
|
// Workaround for some devices unexpectedly closing the connection
|
||||||
if (authHeader) {
|
if (authHeader) {
|
||||||
try {
|
try {
|
||||||
res = await httpGet(opts, _debug, deviceId);
|
res = await httpGet(url, opts, _debug, deviceId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return `Connection request error: ${err.message}`;
|
return `Connection request error: ${err.message}`;
|
||||||
}
|
}
|
||||||
@@ -169,9 +169,13 @@ export async function httpConnectionRequest(
|
|||||||
if (res.statusCode === 503) return "Device is offline";
|
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"]
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return "Connection request error: Error parsing www-authenticate header";
|
||||||
|
}
|
||||||
[username, password, authExp] = await extractAuth(authExp, false);
|
[username, password, authExp] = await extractAuth(authExp, false);
|
||||||
} else {
|
} else {
|
||||||
return `Connection request error: Unexpected status code ${res.statusCode}`;
|
return `Connection request error: Unexpected status code ${res.statusCode}`;
|
||||||
@@ -182,14 +186,13 @@ export async function httpConnectionRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function udpConnectionRequest(
|
export async function udpConnectionRequest(
|
||||||
address: string,
|
host: string,
|
||||||
|
port: number,
|
||||||
authExp: Expression,
|
authExp: Expression,
|
||||||
sourcePort = 0,
|
sourcePort = 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 });
|
||||||
@@ -216,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://${address}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
|
const uri = `http://${host}:${port}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
|
||||||
const msg = `GET ${uri} HTTP/1.1\r\nHost: ${address}\r\n\r\n`;
|
const msg = `GET ${uri} HTTP/1.1\r\nHost: ${host}:${port}\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((resolve, reject) => {
|
await new Promise<void>((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();
|
||||||
|
|||||||
+239
-178
@@ -45,6 +45,7 @@ import {
|
|||||||
InformRequest,
|
InformRequest,
|
||||||
Preset,
|
Preset,
|
||||||
GetRPCMethodsResponse,
|
GetRPCMethodsResponse,
|
||||||
|
CpeFault,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { IncomingMessage, ServerResponse } from "http";
|
import { IncomingMessage, ServerResponse } from "http";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
@@ -53,6 +54,7 @@ import { decode, encodingExists } from "iconv-lite";
|
|||||||
import { parseXmlDeclaration } from "./xml-parser";
|
import { parseXmlDeclaration } from "./xml-parser";
|
||||||
import * as debug from "./debug";
|
import * as debug from "./debug";
|
||||||
import { getRequestOrigin } from "./forwarded";
|
import { getRequestOrigin } from "./forwarded";
|
||||||
|
import { getSocketEndpoints } from "./server";
|
||||||
|
|
||||||
const gzipPromisified = promisify(zlib.gzip);
|
const gzipPromisified = promisify(zlib.gzip);
|
||||||
const deflatePromisified = promisify(zlib.deflate);
|
const deflatePromisified = promisify(zlib.deflate);
|
||||||
@@ -79,17 +81,21 @@ async function authenticate(
|
|||||||
sessionContext.cacheSnapshot,
|
sessionContext.cacheSnapshot,
|
||||||
"cwmp.auth"
|
"cwmp.auth"
|
||||||
);
|
);
|
||||||
if (!authExpression) return true;
|
if (authExpression == null) return true;
|
||||||
|
|
||||||
let authentication;
|
let authentication;
|
||||||
|
|
||||||
if (sessionContext.httpRequest.headers["authorization"]) {
|
if (sessionContext.httpRequest.headers["authorization"]) {
|
||||||
|
try {
|
||||||
authentication = auth.parseAuthorizationHeader(
|
authentication = auth.parseAuthorizationHeader(
|
||||||
sessionContext.httpRequest.headers["authorization"]
|
sessionContext.httpRequest.headers["authorization"]
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authentication && authentication.method === "Digest") {
|
if (authentication?.method === "Digest") {
|
||||||
const sessionNonce = sessionsNonces.get(sessionContext.httpRequest.socket);
|
const sessionNonce = sessionsNonces.get(sessionContext.httpRequest.socket);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -183,7 +189,8 @@ async function writeResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const httpResponse = sessionContext.httpResponse;
|
const httpResponse = sessionContext.httpResponse;
|
||||||
const connection = httpResponse.socket;
|
// Don't use httpResponse.socket as it may be null, even before end() is called
|
||||||
|
const connection = sessionContext.httpRequest.socket;
|
||||||
|
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(data));
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(data));
|
||||||
httpResponse.writeHead(res.code, res.headers);
|
httpResponse.writeHead(res.code, res.headers);
|
||||||
@@ -206,12 +213,13 @@ async function writeResponse(
|
|||||||
currentSessions.set(connection, sessionContext);
|
currentSessions.set(connection, sessionContext);
|
||||||
if (now >= sessionContext.extendLock) {
|
if (now >= sessionContext.extendLock) {
|
||||||
sessionContext.extendLock = now + 10000;
|
sessionContext.extendLock = now + 10000;
|
||||||
await cache.acquireLock(
|
const lockToken = await cache.acquireLock(
|
||||||
`cwmp_session_${sessionContext.deviceId}`,
|
`cwmp_session_${sessionContext.deviceId}`,
|
||||||
sessionContext.timeout + 10000,
|
sessionContext.timeout * 1000 + 15000,
|
||||||
0,
|
0,
|
||||||
sessionContext.sessionId
|
sessionContext.sessionId
|
||||||
);
|
);
|
||||||
|
if (!lockToken) throw new Error("Failed to extend lock");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,7 +438,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
const deviceEvents = {};
|
const deviceEvents = {};
|
||||||
for (const p of deviceData.paths.find(Path.parse("Events.*"), false, true)) {
|
for (const p of deviceData.paths.find(Path.parse("Events.*"), false, true)) {
|
||||||
const attrs = deviceData.attributes.get(p);
|
const attrs = deviceData.attributes.get(p);
|
||||||
if (attrs && attrs.value && attrs.value[1][0] >= sessionContext.timestamp)
|
if (attrs?.value && attrs.value[1][0] >= sessionContext.timestamp)
|
||||||
deviceEvents[p.segments[1] as string] = true;
|
deviceEvents[p.segments[1] as string] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +462,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
|
|
||||||
if (!eventsMatch) continue;
|
if (!eventsMatch) continue;
|
||||||
|
|
||||||
if (preset.schedule && preset.schedule.schedule) {
|
if (preset.schedule?.schedule) {
|
||||||
const r = scheduling.cron(
|
const r = scheduling.cron(
|
||||||
sessionContext.timestamp,
|
sessionContext.timestamp,
|
||||||
preset.schedule.schedule
|
preset.schedule.schedule
|
||||||
@@ -493,10 +501,11 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
defer: true,
|
defer: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { fault: flt, rpcId: reqId, rpc: acsReq } = await session.rpcRequest(
|
const {
|
||||||
sessionContext,
|
fault: flt,
|
||||||
declarations
|
rpcId: reqId,
|
||||||
);
|
rpc: acsReq,
|
||||||
|
} = await session.rpcRequest(sessionContext, declarations);
|
||||||
|
|
||||||
if (flt) {
|
if (flt) {
|
||||||
recordFault(sessionContext, flt);
|
recordFault(sessionContext, flt);
|
||||||
@@ -527,7 +536,7 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
session.configContextCallback(sessionContext, e)
|
session.configContextCallback(sessionContext, e)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
]);
|
]) as [string, ...Expression[]][];
|
||||||
if (blackList[p.channel] === 2) {
|
if (blackList[p.channel] === 2) {
|
||||||
appendProvisionsToFaults[p.channel] = (
|
appendProvisionsToFaults[p.channel] = (
|
||||||
appendProvisionsToFaults[p.channel] || []
|
appendProvisionsToFaults[p.channel] || []
|
||||||
@@ -568,10 +577,11 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
|
|
||||||
deviceData.timestamps.dirty = 0;
|
deviceData.timestamps.dirty = 0;
|
||||||
deviceData.attributes.dirty = 0;
|
deviceData.attributes.dirty = 0;
|
||||||
const { fault: fault, rpcId: id, rpc: acsRequest } = await session.rpcRequest(
|
const {
|
||||||
sessionContext,
|
fault: fault,
|
||||||
null
|
rpcId: id,
|
||||||
);
|
rpc: acsRequest,
|
||||||
|
} = await session.rpcRequest(sessionContext, null);
|
||||||
|
|
||||||
if (fault) {
|
if (fault) {
|
||||||
recordFault(sessionContext, fault);
|
recordFault(sessionContext, fault);
|
||||||
@@ -601,10 +611,11 @@ async function applyPresets(sessionContext: SessionContext): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function nextRpc(sessionContext: SessionContext): Promise<void> {
|
async function nextRpc(sessionContext: SessionContext): Promise<void> {
|
||||||
const { fault: fault, rpcId: id, rpc: acsRequest } = await session.rpcRequest(
|
const {
|
||||||
sessionContext,
|
fault: fault,
|
||||||
null
|
rpcId: id,
|
||||||
);
|
rpc: acsRequest,
|
||||||
|
} = await session.rpcRequest(sessionContext, null);
|
||||||
|
|
||||||
if (fault) {
|
if (fault) {
|
||||||
recordFault(sessionContext, fault);
|
recordFault(sessionContext, fault);
|
||||||
@@ -726,7 +737,15 @@ async function nextRpc(sessionContext: SessionContext): Promise<void> {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Task name not recognized");
|
if (!sessionContext.doneTasks) sessionContext.doneTasks = [];
|
||||||
|
sessionContext.doneTasks.push(task._id);
|
||||||
|
sessionContext.tasks = sessionContext.tasks.filter((t) => t !== task);
|
||||||
|
|
||||||
|
logger.accessWarn({
|
||||||
|
sessionContext: sessionContext,
|
||||||
|
message: "Invalid task",
|
||||||
|
taskId: task._id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return nextRpc(sessionContext);
|
return nextRpc(sessionContext);
|
||||||
@@ -774,7 +793,7 @@ async function endSession(sessionContext: SessionContext): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionContext.doneTasks && sessionContext.doneTasks.length) {
|
if (sessionContext.doneTasks?.length) {
|
||||||
saveCache = true;
|
saveCache = true;
|
||||||
promises.push(
|
promises.push(
|
||||||
db.clearTasks(sessionContext.deviceId, sessionContext.doneTasks)
|
db.clearTasks(sessionContext.deviceId, sessionContext.doneTasks)
|
||||||
@@ -866,41 +885,20 @@ async function sendAcsRequest(
|
|||||||
return writeResponse(sessionContext, res);
|
return writeResponse(sessionContext, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSession(
|
|
||||||
connection: Socket,
|
|
||||||
sessionId: string
|
|
||||||
): Promise<SessionContext> {
|
|
||||||
let sessionContext = currentSessions.get(connection);
|
|
||||||
if (sessionContext) {
|
|
||||||
currentSessions.delete(connection);
|
|
||||||
return sessionContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionId) return null;
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
const sessionContextString = await cache.pop(`session_${sessionId}`);
|
|
||||||
if (!sessionContextString) return null;
|
|
||||||
sessionContext = await session.deserialize(sessionContextString);
|
|
||||||
connection.setTimeout(sessionContext.timeout);
|
|
||||||
return sessionContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only needed to prevent tree shaking from removing the remoteAddress
|
|
||||||
// workaround in onConnection function.
|
|
||||||
const remoteAddressWorkaround = new WeakMap<Socket, string>();
|
|
||||||
|
|
||||||
// When socket closes, store active sessions in cache
|
// When socket closes, store active sessions in cache
|
||||||
export function onConnection(socket: Socket): void {
|
export function onConnection(socket: Socket): void {
|
||||||
// The property remoteAddress may be undefined after the connection is
|
|
||||||
// closed, unless we read it at least once (caching?)
|
|
||||||
remoteAddressWorkaround.set(socket, socket.remoteAddress);
|
|
||||||
|
|
||||||
socket.on("close", async () => {
|
socket.on("close", async () => {
|
||||||
const sessionContext = currentSessions.get(socket);
|
const sessionContext = currentSessions.get(socket);
|
||||||
if (!sessionContext) return;
|
if (!sessionContext) return;
|
||||||
currentSessions.delete(socket);
|
currentSessions.delete(socket);
|
||||||
|
if (sessionContext.authState !== 2) {
|
||||||
|
logger.accessError({
|
||||||
|
message: "Authentication failure",
|
||||||
|
sessionContext: sessionContext,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const lastActivity = sessionContext.lastActivity;
|
const lastActivity = sessionContext.lastActivity;
|
||||||
@@ -943,6 +941,32 @@ export function onConnection(socket: Socket): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function onClientError(err: Error, socket: Socket): void {
|
||||||
|
const remoteAddress = getSocketEndpoints(socket).remoteAddress;
|
||||||
|
localCache
|
||||||
|
.getCurrentSnapshot()
|
||||||
|
.then((cacheSnapshot) => {
|
||||||
|
const debugEnabled = !!localCache.getConfig(
|
||||||
|
cacheSnapshot,
|
||||||
|
"cwmp.debug",
|
||||||
|
{
|
||||||
|
remoteAddress: remoteAddress,
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
(e) => {
|
||||||
|
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
|
||||||
|
return remoteAddress;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (debugEnabled) debug.clientError(remoteAddress, err);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (stats.droppedRequests) {
|
if (stats.droppedRequests) {
|
||||||
logger.warn({
|
logger.warn({
|
||||||
@@ -1025,13 +1049,13 @@ async function reportBadState(sessionContext: SessionContext): Promise<void> {
|
|||||||
sessionContext: sessionContext,
|
sessionContext: sessionContext,
|
||||||
});
|
});
|
||||||
const httpResponse = sessionContext.httpResponse;
|
const httpResponse = sessionContext.httpResponse;
|
||||||
currentSessions.delete(httpResponse.socket);
|
|
||||||
const body = "Bad session state";
|
const body = "Bad session state";
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(body));
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(body));
|
||||||
httpResponse.writeHead(400, { Connection: "close" });
|
httpResponse.writeHead(400, { Connection: "close" });
|
||||||
if (sessionContext.debug)
|
if (sessionContext.debug)
|
||||||
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body);
|
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, body);
|
||||||
httpResponse.end(body);
|
httpResponse.end(body);
|
||||||
|
if (sessionContext.state) return endSession(sessionContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function responseUnauthorized(
|
async function responseUnauthorized(
|
||||||
@@ -1083,7 +1107,7 @@ async function processRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sessionContext.state === 0) {
|
if (sessionContext.state === 0) {
|
||||||
if (!rpc.cpeRequest || rpc.cpeRequest.name !== "Inform")
|
if (rpc.cpeRequest?.name !== "Inform")
|
||||||
return reportBadState(sessionContext);
|
return reportBadState(sessionContext);
|
||||||
|
|
||||||
const res = await inform(sessionContext, rpc);
|
const res = await inform(sessionContext, rpc);
|
||||||
@@ -1126,15 +1150,15 @@ async function processRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
sessionContext.extendLock = sessionContext.timestamp + 10000;
|
sessionContext.extendLock = sessionContext.timestamp + 10000;
|
||||||
await cache.acquireLock(
|
const lockToken = await cache.acquireLock(
|
||||||
`cwmp_session_${sessionContext.deviceId}`,
|
`cwmp_session_${sessionContext.deviceId}`,
|
||||||
sessionContext.timeout + 10000,
|
sessionContext.timeout * 1000 + 15000,
|
||||||
0,
|
0,
|
||||||
sessionContext.sessionId
|
sessionContext.sessionId
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
|
if (!lockToken) {
|
||||||
logger.accessError({
|
logger.accessError({
|
||||||
message: "CPE already in session",
|
message: "CPE already in session",
|
||||||
sessionContext: sessionContext,
|
sessionContext: sessionContext,
|
||||||
@@ -1228,7 +1252,15 @@ async function processRequest(
|
|||||||
} else if (rpc.cpeResponse) {
|
} else if (rpc.cpeResponse) {
|
||||||
if (sessionContext.state !== 2) return reportBadState(sessionContext);
|
if (sessionContext.state !== 2) return reportBadState(sessionContext);
|
||||||
|
|
||||||
await session.rpcResponse(sessionContext, rpc.id, rpc.cpeResponse);
|
const fault = await session.rpcResponse(
|
||||||
|
sessionContext,
|
||||||
|
rpc.id,
|
||||||
|
rpc.cpeResponse
|
||||||
|
);
|
||||||
|
if (fault) {
|
||||||
|
recordFault(sessionContext, fault);
|
||||||
|
session.clearProvisions(sessionContext);
|
||||||
|
}
|
||||||
return nextRpc(sessionContext);
|
return nextRpc(sessionContext);
|
||||||
} else if (rpc.cpeFault) {
|
} else if (rpc.cpeFault) {
|
||||||
if (sessionContext.state !== 2) return reportBadState(sessionContext);
|
if (sessionContext.state !== 2) return reportBadState(sessionContext);
|
||||||
@@ -1245,6 +1277,41 @@ async function processRequest(
|
|||||||
session.clearProvisions(sessionContext);
|
session.clearProvisions(sessionContext);
|
||||||
}
|
}
|
||||||
return nextRpc(sessionContext);
|
return nextRpc(sessionContext);
|
||||||
|
} else if (rpc.unknownMethod) {
|
||||||
|
if (sessionContext.state === 1) {
|
||||||
|
logger.accessWarn({
|
||||||
|
sessionContext: sessionContext,
|
||||||
|
message: "Method not supported",
|
||||||
|
method: rpc.unknownMethod,
|
||||||
|
});
|
||||||
|
|
||||||
|
const f: CpeFault = {
|
||||||
|
faultCode: "Server",
|
||||||
|
faultString: "CWMP fault",
|
||||||
|
detail: {
|
||||||
|
faultCode: "8000",
|
||||||
|
faultString: "Method not supported",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = soap.response({
|
||||||
|
id: rpc.id,
|
||||||
|
acsFault: f,
|
||||||
|
cwmpVersion: sessionContext.cwmpVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
return writeResponse(sessionContext, res);
|
||||||
|
} else if (sessionContext.state === 2) {
|
||||||
|
const fault = {
|
||||||
|
code: "invalid_response",
|
||||||
|
message: "Response name does not match request name",
|
||||||
|
};
|
||||||
|
recordFault(sessionContext, fault);
|
||||||
|
session.clearProvisions(sessionContext);
|
||||||
|
return nextRpc(sessionContext);
|
||||||
|
} else {
|
||||||
|
return reportBadState(sessionContext);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// CPE sent empty response
|
// CPE sent empty response
|
||||||
if (sessionContext.state !== 1) return reportBadState(sessionContext);
|
if (sessionContext.state !== 1) return reportBadState(sessionContext);
|
||||||
@@ -1280,13 +1347,13 @@ export function listener(
|
|||||||
stats.concurrentRequests -= 1;
|
stats.concurrentRequests -= 1;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
currentSessions.delete(httpResponse.socket);
|
currentSessions.delete(httpRequest.socket);
|
||||||
stats.concurrentRequests -= 1;
|
stats.concurrentRequests -= 1;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
httpResponse.socket.unref();
|
httpRequest.socket.unref();
|
||||||
httpResponse.writeHead(500, { Connection: "close" });
|
httpResponse.writeHead(500, { Connection: "close" });
|
||||||
httpResponse.end(`${err.name}: ${err.message}`);
|
httpResponse.end(`${err.name}: ${err.message}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1295,6 +1362,48 @@ export function listener(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clientError(
|
||||||
|
httpRequest: IncomingMessage,
|
||||||
|
httpResponse: ServerResponse,
|
||||||
|
sessionContext: SessionContext,
|
||||||
|
body: string,
|
||||||
|
msg: string
|
||||||
|
): Promise<void> {
|
||||||
|
let debugEnabled = false;
|
||||||
|
let deviceId: string = null;
|
||||||
|
|
||||||
|
if (sessionContext) {
|
||||||
|
debugEnabled = sessionContext.debug;
|
||||||
|
deviceId = sessionContext.deviceId;
|
||||||
|
} else {
|
||||||
|
const cacheSnapshot = await localCache.getCurrentSnapshot();
|
||||||
|
debugEnabled = !!localCache.getConfig(
|
||||||
|
cacheSnapshot,
|
||||||
|
"cwmp.debug",
|
||||||
|
{
|
||||||
|
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
(e) => {
|
||||||
|
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
|
||||||
|
return getRequestOrigin(httpRequest).remoteAddress;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.setHeader("Content-Length", Buffer.byteLength(msg));
|
||||||
|
httpResponse.writeHead(400, { Connection: "close" });
|
||||||
|
|
||||||
|
if (debugEnabled) {
|
||||||
|
debug.incomingHttpRequest(httpRequest, deviceId, body);
|
||||||
|
debug.outgoingHttpResponse(httpResponse, deviceId, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpResponse.end(msg);
|
||||||
|
if (sessionContext?.state) await endSession(sessionContext);
|
||||||
|
}
|
||||||
|
|
||||||
function decodeString(buffer: Buffer, charset: string): string {
|
function decodeString(buffer: Buffer, charset: string): string {
|
||||||
try {
|
try {
|
||||||
return buffer.toString(charset as BufferEncoding);
|
return buffer.toString(charset as BufferEncoding);
|
||||||
@@ -1321,7 +1430,8 @@ async function listenerAsync(
|
|||||||
|
|
||||||
let sessionId;
|
let sessionId;
|
||||||
// Separation by comma is important as some devices don't comform to standard
|
// Separation by comma is important as some devices don't comform to standard
|
||||||
const COOKIE_REGEX = /\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g;
|
const COOKIE_REGEX =
|
||||||
|
/\s*([a-zA-Z0-9\-_]+?)\s*=\s*"?([a-zA-Z0-9\-_]*?)"?\s*(,|;|$)/g;
|
||||||
let match;
|
let match;
|
||||||
while ((match = COOKIE_REGEX.exec(httpRequest.headers.cookie)))
|
while ((match = COOKIE_REGEX.exec(httpRequest.headers.cookie)))
|
||||||
if (match[1] === "session") sessionId = match[2];
|
if (match[1] === "session") sessionId = match[2];
|
||||||
@@ -1382,15 +1492,14 @@ async function listenerAsync(
|
|||||||
// Request aborted
|
// Request aborted
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
|
||||||
const newConnection = !currentSessions.has(httpRequest.socket);
|
let sessionContext = currentSessions.get(httpRequest.socket);
|
||||||
|
|
||||||
const sessionContext = await getSession(httpRequest.socket, sessionId);
|
|
||||||
|
|
||||||
if (sessionContext) {
|
if (sessionContext) {
|
||||||
|
currentSessions.delete(httpRequest.socket);
|
||||||
sessionContext.httpRequest = httpRequest;
|
sessionContext.httpRequest = httpRequest;
|
||||||
sessionContext.httpResponse = httpResponse;
|
sessionContext.httpResponse = httpResponse;
|
||||||
if (
|
if (
|
||||||
(newConnection && sessionContext.sessionId !== sessionId) ||
|
(sessionContext.sessionId !== sessionId && sessionContext.state) ||
|
||||||
sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now()
|
sessionContext.lastActivity + sessionContext.timeout * 1000 < Date.now()
|
||||||
) {
|
) {
|
||||||
logger.accessError({
|
logger.accessError({
|
||||||
@@ -1398,29 +1507,14 @@ async function listenerAsync(
|
|||||||
sessionContext: sessionContext,
|
sessionContext: sessionContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
const _body = "Invalid session";
|
return clientError(
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(_body));
|
httpRequest,
|
||||||
httpResponse.writeHead(400, { Connection: "close" });
|
|
||||||
if (sessionContext.debug) {
|
|
||||||
debug.outgoingHttpResponse(
|
|
||||||
httpResponse,
|
httpResponse,
|
||||||
sessionContext.deviceId,
|
sessionContext,
|
||||||
_body
|
body.toString(),
|
||||||
|
"Invalid session"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
httpResponse.end(_body);
|
|
||||||
await endSession(sessionContext);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (newConnection && sessionContext.authState !== 1)
|
|
||||||
sessionContext.authState = 0;
|
|
||||||
} else if (stats.concurrentRequests > MAX_CONCURRENT_REQUESTS) {
|
|
||||||
// Check again just in case device included old session ID
|
|
||||||
// from the previous session
|
|
||||||
httpResponse.writeHead(503, { "Retry-after": 60, Connection: "close" });
|
|
||||||
httpResponse.end("503 Service Unavailable");
|
|
||||||
stats.droppedRequests += 1;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let charset: string;
|
let charset: string;
|
||||||
@@ -1440,6 +1534,16 @@ async function listenerAsync(
|
|||||||
const bodyStr = decodeString(body, charset);
|
const bodyStr = decodeString(body, charset);
|
||||||
|
|
||||||
if (bodyStr == null) {
|
if (bodyStr == null) {
|
||||||
|
if (!sessionContext && sessionId) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
const sessionContextString = await cache.pop(`session_${sessionId}`);
|
||||||
|
if (sessionContextString) {
|
||||||
|
sessionContext = await session.deserialize(sessionContextString);
|
||||||
|
sessionContext.httpRequest = httpRequest;
|
||||||
|
sessionContext.httpResponse = httpResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const msg = `Unknown encoding '${charset}'`;
|
const msg = `Unknown encoding '${charset}'`;
|
||||||
logger.accessError({
|
logger.accessError({
|
||||||
message: "XML parse error",
|
message: "XML parse error",
|
||||||
@@ -1449,130 +1553,87 @@ async function listenerAsync(
|
|||||||
httpResponse: httpResponse,
|
httpResponse: httpResponse,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(msg));
|
return clientError(
|
||||||
httpResponse.writeHead(400, { Connection: "close" });
|
|
||||||
if (sessionContext) {
|
|
||||||
if (sessionContext.debug) {
|
|
||||||
debug.incomingHttpRequest(
|
|
||||||
httpRequest,
|
httpRequest,
|
||||||
sessionContext.deviceId,
|
httpResponse,
|
||||||
body.toString()
|
sessionContext,
|
||||||
|
body.toString(),
|
||||||
|
msg
|
||||||
);
|
);
|
||||||
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, msg);
|
|
||||||
}
|
|
||||||
await endSession(sessionContext);
|
|
||||||
} else {
|
|
||||||
const cacheSnapshot = await localCache.getCurrentSnapshot();
|
|
||||||
const d = !!localCache.getConfig(
|
|
||||||
cacheSnapshot,
|
|
||||||
"cwmp.debug",
|
|
||||||
{
|
|
||||||
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
|
|
||||||
},
|
|
||||||
Date.now(),
|
|
||||||
(e) => {
|
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
|
|
||||||
return getRequestOrigin(httpRequest).remoteAddress;
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (d) {
|
|
||||||
debug.incomingHttpRequest(httpRequest, null, body.toString());
|
|
||||||
debug.outgoingHttpResponse(httpResponse, null, msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
httpResponse.end(msg);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseWarnings = [];
|
const parseWarnings = [];
|
||||||
let rpc: SoapMessage;
|
let rpc: SoapMessage;
|
||||||
try {
|
try {
|
||||||
rpc = soap.request(
|
rpc = soap.request(bodyStr, parseWarnings);
|
||||||
bodyStr,
|
} catch (err) {
|
||||||
sessionContext ? sessionContext.cwmpVersion : null,
|
if (!sessionContext && sessionId) {
|
||||||
parseWarnings
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
);
|
const sessionContextString = await cache.pop(`session_${sessionId}`);
|
||||||
} catch (error) {
|
if (sessionContextString) {
|
||||||
|
sessionContext = await session.deserialize(sessionContextString);
|
||||||
|
sessionContext.httpRequest = httpRequest;
|
||||||
|
sessionContext.httpResponse = httpResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.accessError({
|
logger.accessError({
|
||||||
message: "XML parse error",
|
message: "XML parse error",
|
||||||
parseError: error.message.trim(),
|
parseError: err.message,
|
||||||
sessionContext: sessionContext || {
|
sessionContext: sessionContext || {
|
||||||
httpRequest: httpRequest,
|
httpRequest: httpRequest,
|
||||||
httpResponse: httpResponse,
|
httpResponse: httpResponse,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(error.message));
|
|
||||||
httpResponse.writeHead(400, { Connection: "close" });
|
return clientError(
|
||||||
if (sessionContext) {
|
|
||||||
if (sessionContext.debug) {
|
|
||||||
debug.incomingHttpRequest(
|
|
||||||
httpRequest,
|
httpRequest,
|
||||||
sessionContext.deviceId,
|
|
||||||
bodyStr
|
|
||||||
);
|
|
||||||
debug.outgoingHttpResponse(
|
|
||||||
httpResponse,
|
httpResponse,
|
||||||
sessionContext.deviceId,
|
sessionContext,
|
||||||
error.message
|
bodyStr,
|
||||||
|
err.message
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await endSession(sessionContext);
|
|
||||||
} else {
|
if (!sessionContext && sessionId && rpc.cpeRequest?.name !== "Inform") {
|
||||||
const cacheSnapshot = await localCache.getCurrentSnapshot();
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
const d = !!localCache.getConfig(
|
const sessionContextString = await cache.pop(`session_${sessionId}`);
|
||||||
cacheSnapshot,
|
if (sessionContextString) {
|
||||||
"cwmp.debug",
|
sessionContext = await session.deserialize(sessionContextString);
|
||||||
{
|
sessionContext.httpRequest = httpRequest;
|
||||||
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
|
sessionContext.httpResponse = httpResponse;
|
||||||
},
|
httpRequest.socket.setTimeout(sessionContext.timeout * 1000);
|
||||||
Date.now(),
|
if (sessionContext.authState !== 1) sessionContext.authState = 0;
|
||||||
(e) => {
|
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
|
|
||||||
return getRequestOrigin(httpRequest).remoteAddress;
|
|
||||||
return e;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
if (d) {
|
|
||||||
debug.incomingHttpRequest(httpRequest, null, bodyStr);
|
|
||||||
debug.outgoingHttpResponse(httpResponse, null, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
httpResponse.end(error.message);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionContext)
|
if (sessionContext)
|
||||||
return processRequest(sessionContext, rpc, parseWarnings, bodyStr);
|
return processRequest(sessionContext, rpc, parseWarnings, bodyStr);
|
||||||
|
|
||||||
if (!(rpc.cpeRequest && rpc.cpeRequest.name === "Inform")) {
|
if (rpc.cpeRequest?.name !== "Inform") {
|
||||||
logger.accessError({
|
logger.accessError({
|
||||||
message: "Invalid session",
|
message: "Invalid session",
|
||||||
sessionContext: sessionContext || {
|
sessionContext: {
|
||||||
httpRequest: httpRequest,
|
httpRequest: httpRequest,
|
||||||
httpResponse: httpResponse,
|
httpResponse: httpResponse,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const _body = "Invalid session";
|
|
||||||
httpResponse.setHeader("Content-Length", Buffer.byteLength(_body));
|
return clientError(
|
||||||
httpResponse.writeHead(400, { Connection: "close" });
|
httpRequest,
|
||||||
const cacheSnapshot = await localCache.getCurrentSnapshot();
|
httpResponse,
|
||||||
const d = !!localCache.getConfig(
|
null,
|
||||||
cacheSnapshot,
|
bodyStr,
|
||||||
"cwmp.debug",
|
"Invalid session"
|
||||||
{
|
|
||||||
remoteAddress: getRequestOrigin(httpRequest).remoteAddress,
|
|
||||||
},
|
|
||||||
Date.now(),
|
|
||||||
(e) => {
|
|
||||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "REMOTE_ADDRESS")
|
|
||||||
return getRequestOrigin(httpRequest).remoteAddress;
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (d)
|
}
|
||||||
debug.outgoingHttpResponse(httpResponse, sessionContext.deviceId, _body);
|
|
||||||
httpResponse.end(_body);
|
if (stats.concurrentRequests > MAX_CONCURRENT_REQUESTS) {
|
||||||
|
// Check again just in case device included old session ID
|
||||||
|
// from the previous session
|
||||||
|
httpResponse.writeHead(503, { "Retry-after": 60, Connection: "close" });
|
||||||
|
httpResponse.end("503 Service Unavailable");
|
||||||
|
stats.droppedRequests += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MongoClient, ObjectID, Collection, Db } from "mongodb";
|
import { MongoClient, ObjectId, Collection, Db } from "mongodb";
|
||||||
import { get } from "./config";
|
import { get } from "./config";
|
||||||
import { encodeTag, escapeRegExp } from "./common";
|
import { decodeTag, encodeTag, escapeRegExp } from "./common";
|
||||||
import { parse } from "./common/expression-parser";
|
import { parse } from "./common/expression-parser";
|
||||||
import {
|
import {
|
||||||
DeviceData,
|
DeviceData,
|
||||||
@@ -30,22 +30,25 @@ import {
|
|||||||
Expression,
|
Expression,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import Path from "./common/path";
|
import Path from "./common/path";
|
||||||
|
import * as MongoTypes from "./mongodb-types";
|
||||||
|
|
||||||
export let tasksCollection: Collection,
|
export let tasksCollection: Collection<MongoTypes.Task>,
|
||||||
devicesCollection: Collection,
|
devicesCollection: Collection,
|
||||||
presetsCollection: Collection,
|
presetsCollection: Collection,
|
||||||
objectsCollection: Collection,
|
objectsCollection: Collection,
|
||||||
provisionsCollection: Collection,
|
provisionsCollection: Collection,
|
||||||
virtualParametersCollection: Collection,
|
virtualParametersCollection: Collection,
|
||||||
faultsCollection: Collection,
|
faultsCollection: Collection<MongoTypes.Fault>,
|
||||||
filesCollection: Collection,
|
filesCollection: Collection,
|
||||||
operationsCollection: Collection,
|
operationsCollection: Collection<MongoTypes.Operation>,
|
||||||
permissionsCollection: Collection,
|
permissionsCollection: Collection,
|
||||||
usersCollection: Collection,
|
usersCollection: Collection,
|
||||||
configCollection: Collection;
|
configCollection: Collection<MongoTypes.Config>;
|
||||||
|
|
||||||
let clientPromise: Promise<MongoClient>;
|
let clientPromise: Promise<MongoClient>;
|
||||||
|
|
||||||
|
const INVALID_PATH_SUFFIX = "__invalid";
|
||||||
|
|
||||||
function compareAccessLists(list1: string[], list2: string[]): boolean {
|
function compareAccessLists(list1: string[], list2: string[]): boolean {
|
||||||
if (list1.length !== list2.length) return false;
|
if (list1.length !== list2.length) return false;
|
||||||
for (const [i, v] of list1.entries()) if (v !== list2[i]) return false;
|
for (const [i, v] of list1.entries()) if (v !== list2[i]) return false;
|
||||||
@@ -59,10 +62,7 @@ export function onConnect(callback: (db: Db) => Promise<void>): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function connect(): Promise<void> {
|
export async function connect(): Promise<void> {
|
||||||
clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), {
|
clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"));
|
||||||
useNewUrlParser: true,
|
|
||||||
useUnifiedTopology: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = await clientPromise;
|
const client = await clientPromise;
|
||||||
const db = client.db();
|
const db = client.db();
|
||||||
@@ -92,7 +92,9 @@ export async function disconnect(): Promise<void> {
|
|||||||
|
|
||||||
// Optimize projection by removing overlaps
|
// Optimize projection by removing overlaps
|
||||||
// This can modify the object
|
// This can modify the object
|
||||||
function optimizeProjection(obj: { [path: string]: 1 }): { [path: string]: 1 } {
|
export function optimizeProjection(obj: { [path: string]: 1 }): {
|
||||||
|
[path: string]: 1;
|
||||||
|
} {
|
||||||
if (obj[""]) return { "": obj[""] };
|
if (obj[""]) return { "": obj[""] };
|
||||||
|
|
||||||
const keys = Object.keys(obj).sort();
|
const keys = Object.keys(obj).sort();
|
||||||
@@ -131,7 +133,12 @@ export async function fetchDevice(
|
|||||||
const device = await devicesCollection.findOne({ _id: id });
|
const device = await devicesCollection.findOne({ _id: id });
|
||||||
if (!device) return null;
|
if (!device) return null;
|
||||||
|
|
||||||
function storeParams(obj, path: string, pathLength: number, ts): void {
|
function storeParams(
|
||||||
|
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"];
|
||||||
@@ -142,7 +149,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")
|
if (obj["_type"] === "xsd:dateTime" && obj["_value"] instanceof Date)
|
||||||
attrs.value[1][0] = +attrs.value[1][0];
|
attrs.value[1][0] = +attrs.value[1][0];
|
||||||
|
|
||||||
obj["_object"] = false;
|
obj["_object"] = false;
|
||||||
@@ -162,19 +169,34 @@ export async function fetchDevice(
|
|||||||
if (obj["_accessList"] != null)
|
if (obj["_accessList"] != null)
|
||||||
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
||||||
|
|
||||||
res.push([Path.parse(path.slice(0, -1)), t, attrs]);
|
try {
|
||||||
|
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"] || !pathLength) && obj["_timestamp"])
|
if (obj["_object"] && 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":
|
||||||
@@ -302,10 +324,11 @@ export async function fetchDevice(
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (!k.startsWith("_")) storeParams(v, k, 1, ts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
storeParams(device, "", 0, 0);
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,21 +449,34 @@ 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(path.segments[1]);
|
update["$addToSet"]["_tags"]["$each"].push(
|
||||||
|
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(path.segments[1]);
|
update["$pull"]["_tags"]["$in"].push(
|
||||||
|
decodeTag(path.segments[1] as string)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (!diff[2]) {
|
if (!diff[2]) {
|
||||||
update["$unset"][path.toString()] = 1;
|
let pathStr = path.toString();
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +512,7 @@ export async function saveDevice(
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case "object":
|
case "object":
|
||||||
if (!diff[1] || !diff[1].object || object2 !== object1) {
|
if (!diff[1]?.object || object2 !== object1) {
|
||||||
update["$set"][
|
update["$set"][
|
||||||
path.length ? path.toString() + "._object" : "_object"
|
path.length ? path.toString() + "._object" : "_object"
|
||||||
] = !!object2;
|
] = !!object2;
|
||||||
@@ -484,7 +520,7 @@ export async function saveDevice(
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case "writable":
|
case "writable":
|
||||||
if (!diff[1] || !diff[1].writable || writable2 !== writable1) {
|
if (!diff[1]?.writable || writable2 !== writable1) {
|
||||||
update["$set"][
|
update["$set"][
|
||||||
path.length ? path.toString() + "._writable" : "_writable"
|
path.length ? path.toString() + "._writable" : "_writable"
|
||||||
] = !!writable2;
|
] = !!writable2;
|
||||||
@@ -505,9 +541,8 @@ export async function saveDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (attributesTimestamp2 !== attributesTimestamp1) {
|
if (attributesTimestamp2 !== attributesTimestamp1) {
|
||||||
update["$set"][
|
update["$set"][path.toString() + "._attributesTimestamp"] =
|
||||||
path.toString() + "._attributesTimestamp"
|
new Date(attributesTimestamp2);
|
||||||
] = new Date(attributesTimestamp2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -525,9 +560,8 @@ export async function saveDevice(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (attributesTimestamp2 !== attributesTimestamp1) {
|
if (attributesTimestamp2 !== attributesTimestamp1) {
|
||||||
update["$set"][
|
update["$set"][path.toString() + "._attributesTimestamp"] =
|
||||||
path.toString() + "._attributesTimestamp"
|
new Date(attributesTimestamp2);
|
||||||
] = new Date(attributesTimestamp2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,7 +571,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] || !diff[2][attrName] || diff[2][attrName][1] == null)
|
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;
|
||||||
@@ -591,7 +625,7 @@ export async function saveDevice(
|
|||||||
upsert: isNew,
|
upsert: isNew,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.result.n !== 1)
|
if (!result.matchedCount && !result.upsertedCount)
|
||||||
throw new Error(`Device ${deviceId} not found in database`);
|
throw new Error(`Device ${deviceId} not found in database`);
|
||||||
|
|
||||||
if (update2) {
|
if (update2) {
|
||||||
@@ -610,12 +644,16 @@ export async function getFaults(
|
|||||||
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);
|
||||||
delete r._id;
|
const fault: SessionFault = {
|
||||||
delete r.channel;
|
code: r.code,
|
||||||
delete r.device;
|
message: r.message,
|
||||||
r.timestamp = +r.timestamp;
|
...(r.detail && { detail: r.detail }),
|
||||||
r.provisions = JSON.parse(r.provisions);
|
timestamp: +r.timestamp,
|
||||||
faults[channel] = r;
|
provisions: JSON.parse(r.provisions),
|
||||||
|
retries: r.retries,
|
||||||
|
...(r.expiry && { expiry: +r.expiry }),
|
||||||
|
};
|
||||||
|
faults[channel] = fault;
|
||||||
}
|
}
|
||||||
|
|
||||||
return faults;
|
return faults;
|
||||||
@@ -627,12 +665,18 @@ export async function saveFault(
|
|||||||
fault: SessionFault
|
fault: SessionFault
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id = `${deviceId}:${channel}`;
|
const id = `${deviceId}:${channel}`;
|
||||||
const f = Object.assign({}, fault) as Record<string, any>;
|
const f: MongoTypes.Fault = {
|
||||||
f["_id"] = id;
|
_id: id,
|
||||||
f["device"] = deviceId;
|
device: deviceId,
|
||||||
f["channel"] = channel;
|
channel: channel,
|
||||||
f["timestamp"] = new Date(fault.timestamp);
|
timestamp: new Date(fault.timestamp),
|
||||||
f["provisions"] = JSON.stringify(fault.provisions);
|
code: fault.code,
|
||||||
|
message: fault.message,
|
||||||
|
...(fault.detail && { detail: fault.detail }),
|
||||||
|
retries: fault.retries,
|
||||||
|
...(fault.expiry && { expiry: new Date(fault.expiry) }),
|
||||||
|
provisions: JSON.stringify(fault.provisions),
|
||||||
|
};
|
||||||
await faultsCollection.replaceOne({ _id: id }, f, { upsert: true });
|
await faultsCollection.replaceOne({ _id: id }, f, { upsert: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,20 +694,47 @@ export async function getDueTasks(
|
|||||||
const cur = tasksCollection.find({ device: deviceId }).sort({ timestamp: 1 });
|
const cur = tasksCollection.find({ device: deviceId }).sort({ timestamp: 1 });
|
||||||
const tasks = [] as Task[];
|
const tasks = [] as Task[];
|
||||||
|
|
||||||
for await (const task of cur) {
|
for await (const t of cur) {
|
||||||
if (task.timestamp) task.timestamp = +task.timestamp;
|
if (+t.timestamp >= timestamp) return [tasks, +t.timestamp];
|
||||||
if (task.expiry) task.expiry = +task.expiry;
|
const task: Task = {
|
||||||
if (task.timestamp >= timestamp) return [tasks, +task.timestamp];
|
_id: t._id.toString(),
|
||||||
task._id = String(task._id);
|
name: t.name,
|
||||||
|
...(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" && task.file) {
|
if (task.name === "download" && t["file"]) {
|
||||||
let q;
|
let q;
|
||||||
if (ObjectID.isValid(task.file))
|
if (ObjectId.isValid(t["file"]))
|
||||||
q = { _id: { $in: [task.file, new ObjectID(task.file)] } };
|
q = { _id: { $in: [t["file"], new ObjectId(t["file"])] } };
|
||||||
else q = { _id: task.file };
|
else q = { _id: t["file"] };
|
||||||
|
|
||||||
const res = await filesCollection.find(q).toArray();
|
const res = await filesCollection.find(q).toArray();
|
||||||
|
|
||||||
@@ -683,7 +754,7 @@ export async function clearTasks(
|
|||||||
taskIds: string[]
|
taskIds: string[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await tasksCollection.deleteMany({
|
await tasksCollection.deleteMany({
|
||||||
_id: { $in: taskIds.map((id) => new ObjectID(id)) },
|
_id: { $in: taskIds.map((id) => new ObjectId(id)) },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,17 +768,22 @@ export async function getOperations(
|
|||||||
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);
|
||||||
delete r._id;
|
|
||||||
// Workaround for a bug in v1.2.1 where operation object is saved without deserialization
|
// Workaround for a bug in v1.2.1 where operation object is saved without deserialization
|
||||||
if (typeof r.provisions !== "string") {
|
if (typeof r.provisions !== "string") {
|
||||||
operations[commandKey] = r;
|
delete r._id;
|
||||||
|
operations[commandKey] = r as unknown as Operation;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
r.timestamp = +r.timestamp;
|
const operation: Operation = {
|
||||||
if (r.args) r.args = JSON.parse(r.args);
|
name: r.name,
|
||||||
r.provisions = JSON.parse(r.provisions);
|
timestamp: +r.timestamp,
|
||||||
r.retries = JSON.parse(r.retries);
|
channels:
|
||||||
operations[commandKey] = r;
|
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;
|
||||||
}
|
}
|
||||||
@@ -718,12 +794,15 @@ export async function saveOperation(
|
|||||||
operation: Operation
|
operation: Operation
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const id = `${deviceId}:${commandKey}`;
|
const id = `${deviceId}:${commandKey}`;
|
||||||
const o = Object.assign({}, operation) as Record<string, any>;
|
const o: MongoTypes.Operation = {
|
||||||
o["_id"] = id;
|
_id: id,
|
||||||
o["timestamp"] = new Date(operation.timestamp);
|
name: operation.name,
|
||||||
o["provisions"] = JSON.stringify(operation.provisions);
|
timestamp: new Date(operation.timestamp),
|
||||||
o["retries"] = JSON.stringify(operation.retries);
|
channels: JSON.stringify(operation.channels),
|
||||||
o["args"] = JSON.stringify(operation.args);
|
provisions: JSON.stringify(operation.provisions),
|
||||||
|
retries: JSON.stringify(operation.retries),
|
||||||
|
args: JSON.stringify(operation.args),
|
||||||
|
};
|
||||||
await operationsCollection.replaceOne({ _id: id }, o, {
|
await operationsCollection.replaceOne({ _id: id }, o, {
|
||||||
upsert: true,
|
upsert: true,
|
||||||
});
|
});
|
||||||
@@ -775,7 +854,7 @@ interface Permission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getPermissions(): Promise<Permission[]> {
|
export async function getPermissions(): Promise<Permission[]> {
|
||||||
return permissionsCollection.find().toArray();
|
return permissionsCollection.find().toArray() as unknown as Permission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -786,5 +865,5 @@ interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getUsers(): Promise<User[]> {
|
export async function getUsers(): Promise<User[]> {
|
||||||
return usersCollection.find().toArray();
|
return usersCollection.find().toArray() as unknown as User[];
|
||||||
}
|
}
|
||||||
+42
-25
@@ -17,16 +17,12 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { IncomingMessage, ServerResponse, ClientRequest } from "http";
|
||||||
IncomingMessage,
|
|
||||||
ServerResponse,
|
|
||||||
ClientRequest,
|
|
||||||
RequestOptions,
|
|
||||||
} from "http";
|
|
||||||
import { Socket } from "net";
|
import { Socket } from "net";
|
||||||
import { appendFileSync } from "fs";
|
import { appendFileSync } from "fs";
|
||||||
import * as yaml from "yaml";
|
import { stringify } from "./common/yaml";
|
||||||
import * as config from "./config";
|
import * as config from "./config";
|
||||||
|
import { getSocketEndpoints } from "./server";
|
||||||
|
|
||||||
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");
|
||||||
@@ -50,13 +46,14 @@ export function incomingHttpRequest(
|
|||||||
if (!DEBUG_FILE) return;
|
if (!DEBUG_FILE) return;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const con = httpRequest.socket;
|
const con = httpRequest.socket;
|
||||||
|
const socketEndpoints = getSocketEndpoints(con);
|
||||||
const msg = {
|
const msg = {
|
||||||
event: "incoming HTTP request",
|
event: "incoming HTTP request",
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
remoteAddress: con.remoteAddress,
|
remoteAddress: socketEndpoints.remoteAddress,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
connection: getConnectionTimestamp(con),
|
connection: getConnectionTimestamp(con),
|
||||||
localPort: con.localPort,
|
localPort: socketEndpoints.localPort,
|
||||||
method: httpRequest.method,
|
method: httpRequest.method,
|
||||||
url: httpRequest.url,
|
url: httpRequest.url,
|
||||||
headers: httpRequest.headers,
|
headers: httpRequest.headers,
|
||||||
@@ -64,7 +61,7 @@ export function incomingHttpRequest(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
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`);
|
||||||
@@ -78,10 +75,11 @@ export function outgoingHttpResponse(
|
|||||||
if (!DEBUG_FILE) return;
|
if (!DEBUG_FILE) return;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const con = httpResponse.socket;
|
const con = httpResponse.socket;
|
||||||
|
const socketEndpoints = getSocketEndpoints(con);
|
||||||
const msg = {
|
const msg = {
|
||||||
event: "outgoing HTTP response",
|
event: "outgoing HTTP response",
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
remoteAddress: con.remoteAddress,
|
remoteAddress: socketEndpoints.remoteAddress,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
connection: getConnectionTimestamp(con),
|
connection: getConnectionTimestamp(con),
|
||||||
statusCode: httpResponse.statusCode,
|
statusCode: httpResponse.statusCode,
|
||||||
@@ -90,7 +88,7 @@ export function outgoingHttpResponse(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
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`);
|
||||||
@@ -99,7 +97,8 @@ export function outgoingHttpResponse(
|
|||||||
export function outgoingHttpRequest(
|
export function outgoingHttpRequest(
|
||||||
httpRequest: ClientRequest,
|
httpRequest: ClientRequest,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
options: RequestOptions,
|
method: "GET" | "PUT" | "POST" | "DELETE",
|
||||||
|
url: URL,
|
||||||
body: string
|
body: string
|
||||||
): void {
|
): void {
|
||||||
if (!DEBUG_FILE) return;
|
if (!DEBUG_FILE) return;
|
||||||
@@ -111,15 +110,15 @@ export function outgoingHttpRequest(
|
|||||||
remoteAddress: con.remoteAddress,
|
remoteAddress: con.remoteAddress,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
connection: getConnectionTimestamp(con),
|
connection: getConnectionTimestamp(con),
|
||||||
remotePort: options.port,
|
remotePort: url.port,
|
||||||
method: options.method || "GET",
|
method: method,
|
||||||
url: options.path,
|
url: url.pathname + url.search,
|
||||||
headers: httpRequest.getHeaders(),
|
headers: httpRequest.getHeaders(),
|
||||||
body: body,
|
body: body,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
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`);
|
||||||
@@ -128,7 +127,8 @@ export function outgoingHttpRequest(
|
|||||||
export function outgoingHttpRequestError(
|
export function outgoingHttpRequestError(
|
||||||
httpRequest: ClientRequest,
|
httpRequest: ClientRequest,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
options: RequestOptions,
|
method: "GET" | "PUT" | "POST" | "DELETE",
|
||||||
|
url: URL,
|
||||||
err: Error
|
err: Error
|
||||||
): void {
|
): void {
|
||||||
if (!DEBUG_FILE) return;
|
if (!DEBUG_FILE) return;
|
||||||
@@ -136,18 +136,18 @@ export function outgoingHttpRequestError(
|
|||||||
const msg = {
|
const msg = {
|
||||||
event: "outgoing HTTP request",
|
event: "outgoing HTTP request",
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
remoteAddress: options.hostname,
|
remoteAddress: url.hostname,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
connection: null,
|
connection: null,
|
||||||
remotePort: options.port,
|
remotePort: url.port,
|
||||||
method: options.method,
|
method: method,
|
||||||
url: options.path,
|
url: url.pathname + url.search,
|
||||||
headers: httpRequest.getHeaders(),
|
headers: httpRequest.getHeaders(),
|
||||||
error: err.message,
|
error: err.message,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
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`);
|
||||||
@@ -173,7 +173,7 @@ export function incomingHttpResponse(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
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`);
|
||||||
@@ -197,7 +197,24 @@ export function outgoingUdpMessage(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (DEBUG_FORMAT === "yaml")
|
if (DEBUG_FORMAT === "yaml")
|
||||||
appendFileSync(DEBUG_FILE, "---\n" + yaml.stringify(msg));
|
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 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")
|
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`);
|
||||||
|
|||||||
+2
-9
@@ -54,13 +54,6 @@ 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") {
|
||||||
@@ -83,7 +76,7 @@ export function sanitizeParameterValue(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0]));
|
parameterValue[0] = "" + parameterValue[0];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,7 +201,7 @@ export function clear(
|
|||||||
timestamp = timestamp || 0;
|
timestamp = timestamp || 0;
|
||||||
|
|
||||||
let descendantsTimestamp = timestamp;
|
let descendantsTimestamp = timestamp;
|
||||||
if (attributes && attributes.object) {
|
if (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))
|
||||||
|
|||||||
+23
-10
@@ -22,20 +22,14 @@ import * as crypto from "crypto";
|
|||||||
import * as config from "./config";
|
import * as config from "./config";
|
||||||
import { Fault } from "./types";
|
import { Fault } from "./types";
|
||||||
import { ROOT_DIR } from "./config";
|
import { ROOT_DIR } from "./config";
|
||||||
|
import * as logger from "./logger";
|
||||||
|
import readline from "readline";
|
||||||
|
|
||||||
const TIMEOUT = +config.get("EXT_TIMEOUT");
|
const TIMEOUT = +config.get("EXT_TIMEOUT");
|
||||||
|
|
||||||
const processes: { [script: string]: ChildProcess } = {};
|
const processes: { [script: string]: ChildProcess } = {};
|
||||||
const jobs = new Map();
|
const jobs = new Map();
|
||||||
|
|
||||||
function messageHandler(message): void {
|
|
||||||
const func = jobs.get(message[0]);
|
|
||||||
if (func) {
|
|
||||||
jobs.delete(message[0]);
|
|
||||||
func({ fault: message[1], value: message[2] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const scriptName = args[0];
|
const scriptName = args[0];
|
||||||
@@ -45,7 +39,7 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
|||||||
|
|
||||||
if (!processes[scriptName]) {
|
if (!processes[scriptName]) {
|
||||||
const p = spawn(ROOT_DIR + "/bin/genieacs-ext", [scriptName], {
|
const p = spawn(ROOT_DIR + "/bin/genieacs-ext", [scriptName], {
|
||||||
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
||||||
});
|
});
|
||||||
processes[scriptName] = p;
|
processes[scriptName] = p;
|
||||||
|
|
||||||
@@ -68,7 +62,26 @@ 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", messageHandler);
|
p.on("message", (message) => {
|
||||||
|
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(() => {
|
||||||
|
|||||||
+10
-6
@@ -21,6 +21,7 @@ import { IncomingMessage } from "http";
|
|||||||
import { TLSSocket } from "tls";
|
import { TLSSocket } from "tls";
|
||||||
import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js";
|
import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js";
|
||||||
import * as config from "./config";
|
import * as config from "./config";
|
||||||
|
import { getSocketEndpoints } from "./server";
|
||||||
|
|
||||||
interface RequestOrigin {
|
interface RequestOrigin {
|
||||||
localAddress: string;
|
localAddress: string;
|
||||||
@@ -98,19 +99,22 @@ export function getRequestOrigin(request: IncomingMessage): RequestOrigin {
|
|||||||
let origin = cache.get(request);
|
let origin = cache.get(request);
|
||||||
if (!origin) {
|
if (!origin) {
|
||||||
const soc = request.socket;
|
const soc = request.socket;
|
||||||
|
const socketEndpoints = getSocketEndpoints(soc);
|
||||||
origin = {
|
origin = {
|
||||||
localAddress: soc.localAddress,
|
localAddress: socketEndpoints.localAddress,
|
||||||
localPort: soc.localPort,
|
localPort: socketEndpoints.localPort,
|
||||||
remoteAddress: soc.remoteAddress,
|
remoteAddress: socketEndpoints.remoteAddress,
|
||||||
remotePort: soc.remotePort,
|
remotePort: socketEndpoints.remotePort,
|
||||||
host: request.headers["host"],
|
host: request.headers["host"],
|
||||||
encrypted: !!(request.socket as TLSSocket).encrypted,
|
encrypted: !!(request.socket as TLSSocket).encrypted,
|
||||||
};
|
};
|
||||||
|
|
||||||
const header = request.headers["forwarded"];
|
const header = request.headers["forwarded"];
|
||||||
if (header) {
|
if (header) {
|
||||||
const ip = parse(soc.remoteAddress) as IPv4;
|
const ip = parse(socketEndpoints.remoteAddress);
|
||||||
if (cidrs.some((cidr) => ip.match(cidr as [IPv4, number]))) {
|
if (
|
||||||
|
cidrs.some((cidr) => ip.kind() === cidr[0].kind() && ip.match(cidr))
|
||||||
|
) {
|
||||||
const parsed = parseForwardedHeader(header);
|
const parsed = parseForwardedHeader(header);
|
||||||
|
|
||||||
if (parsed["proto"] === "https") {
|
if (parsed["proto"] === "https") {
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
import * as querystring from "querystring";
|
|
||||||
import { IncomingMessage, ServerResponse } from "http";
|
import { IncomingMessage, ServerResponse } from "http";
|
||||||
import { Collection, GridFSBucket } from "mongodb";
|
import { Collection, GridFSBucket } from "mongodb";
|
||||||
import { onConnect } from "./db";
|
import { onConnect } from "./db";
|
||||||
import * as logger from "./logger";
|
import * as logger from "./logger";
|
||||||
import { getRequestOrigin } from "./forwarded";
|
import { getRequestOrigin } from "./forwarded";
|
||||||
|
import memoize from "./common/memoize";
|
||||||
|
|
||||||
let filesCollection: Collection;
|
let filesCollection: Collection;
|
||||||
let filesBucket: GridFSBucket;
|
let filesBucket: GridFSBucket;
|
||||||
@@ -33,13 +33,37 @@ onConnect(async (db) => {
|
|||||||
filesBucket = new GridFSBucket(db);
|
filesBucket = new GridFSBucket(db);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function listener(
|
const getFile = memoize(
|
||||||
|
(uploadDate: number, size: number, filename: string): Promise<Buffer> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const buffer = Buffer.allocUnsafe(size);
|
||||||
|
let i = 0;
|
||||||
|
const downloadStream = filesBucket.openDownloadStreamByName(filename);
|
||||||
|
downloadStream.on("error", reject);
|
||||||
|
downloadStream.on("data", (data: Buffer) => {
|
||||||
|
data.copy(buffer, i);
|
||||||
|
i += data.length;
|
||||||
|
});
|
||||||
|
downloadStream.on("end", () => {
|
||||||
|
if (i !== size) reject(new Error("File size mismatch"));
|
||||||
|
else resolve(buffer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function listener(
|
||||||
request: IncomingMessage,
|
request: IncomingMessage,
|
||||||
response: ServerResponse
|
response: ServerResponse
|
||||||
): void {
|
): Promise<void> {
|
||||||
|
if (request.method !== "GET") {
|
||||||
|
response.writeHead(405, { Allow: "GET" });
|
||||||
|
response.end("405 Method Not Allowed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const urlParts = url.parse(request.url, true);
|
const urlParts = url.parse(request.url, true);
|
||||||
if (request.method === "GET") {
|
const filename = decodeURIComponent(urlParts.pathname.substring(1));
|
||||||
const filename = querystring.unescape(urlParts.pathname.substring(1));
|
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: "Fetch file",
|
message: "Fetch file",
|
||||||
@@ -47,8 +71,7 @@ export function listener(
|
|||||||
remoteAddress: getRequestOrigin(request).remoteAddress,
|
remoteAddress: getRequestOrigin(request).remoteAddress,
|
||||||
};
|
};
|
||||||
|
|
||||||
filesCollection.findOne({ _id: filename }, (err, file) => {
|
const file = await filesCollection.findOne({ _id: filename });
|
||||||
if (err) throw err;
|
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
response.writeHead(404);
|
response.writeHead(404);
|
||||||
@@ -58,18 +81,17 @@ export function listener(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buffer = await getFile(
|
||||||
|
file["uploadDate"].getTime(),
|
||||||
|
file.length,
|
||||||
|
filename
|
||||||
|
);
|
||||||
|
|
||||||
response.writeHead(200, {
|
response.writeHead(200, {
|
||||||
"Content-Type": file.contentType || "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
"Content-Length": file.length,
|
"Content-Length": file.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadStream = filesBucket.openDownloadStreamByName(filename);
|
response.end(buffer);
|
||||||
downloadStream.pipe(response);
|
|
||||||
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
});
|
|
||||||
} else {
|
|
||||||
response.writeHead(405, { Allow: "GET" });
|
|
||||||
response.end("405 Method Not Allowed");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+2
-2
@@ -59,7 +59,7 @@ const INFORM_SCRIPT = `
|
|||||||
// Device ID as user name
|
// Device ID as user name
|
||||||
const username = declare("DeviceID.ID", {value: 1}).value[0]
|
const username = declare("DeviceID.ID", {value: 1}).value[0]
|
||||||
|
|
||||||
// Password will be fixed a given device because Math.random() is seeded with devcie ID by default.
|
// Password will be fixed for a given device because Math.random() is seeded with device 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;
|
||||||
@@ -257,7 +257,7 @@ export async function seed(options: Record<string, boolean>): Promise<void> {
|
|||||||
"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: "'WLAN hosts'" },
|
{ _id: "ui.device.3.label", value: "'LAN hosts'" },
|
||||||
{
|
{
|
||||||
_id: "ui.device.3.parameter",
|
_id: "ui.device.3.parameter",
|
||||||
value: "InternetGatewayDevice.LANDevice.1.Hosts.Host",
|
value: "InternetGatewayDevice.LANDevice.1.Hosts.Host",
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ interface Instance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class InstanceSet {
|
export default class InstanceSet {
|
||||||
private set: Set<Instance>;
|
private declare set: Set<Instance>;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.set = new Set();
|
this.set = new Set();
|
||||||
|
|||||||
+19
-17
@@ -50,8 +50,8 @@ interface Snapshot {
|
|||||||
ui: UiConfig;
|
ui: UiConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const REFRESH = 3000;
|
const REFRESH = 5000;
|
||||||
const EVICT_TIMEOUT = 60000;
|
const EVICT_TIMEOUT = 120000;
|
||||||
|
|
||||||
const snapshots = new Map<string, Snapshot>();
|
const snapshots = new Map<string, Snapshot>();
|
||||||
let currentSnapshot: string = null;
|
let currentSnapshot: string = null;
|
||||||
@@ -131,7 +131,7 @@ async function fetchPresets(): Promise<Preset[]> {
|
|||||||
|
|
||||||
// If no keys are defined, consider all parameters as keys to keep the
|
// If no keys are defined, consider all parameters as keys to keep the
|
||||||
// same behavior from v1.0
|
// same behavior from v1.0
|
||||||
if (!obj["_keys"] || !obj["_keys"].length)
|
if (!obj["_keys"]?.length)
|
||||||
obj["_keys"] = Object.keys(obj).filter((k) => !k.startsWith("_"));
|
obj["_keys"] = Object.keys(obj).filter((k) => !k.startsWith("_"));
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
@@ -266,9 +266,7 @@ async function fetchProvisions(): Promise<Provisions> {
|
|||||||
.createHash("md5")
|
.createHash("md5")
|
||||||
.update(r["script"])
|
.update(r["script"])
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
provisions[
|
provisions[r["_id"]].script = new vm.Script(
|
||||||
r["_id"]
|
|
||||||
].script = new vm.Script(
|
|
||||||
`"use strict";(function(){\n${r["script"]}\n})();`,
|
`"use strict";(function(){\n${r["script"]}\n})();`,
|
||||||
{ filename: r["_id"], lineOffset: -1, timeout: 50 }
|
{ filename: r["_id"], lineOffset: -1, timeout: 50 }
|
||||||
);
|
);
|
||||||
@@ -287,9 +285,7 @@ async function fetchVirtualParameters(): Promise<VirtualParameters> {
|
|||||||
.createHash("md5")
|
.createHash("md5")
|
||||||
.update(r["script"])
|
.update(r["script"])
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
virtualParameters[
|
virtualParameters[r["_id"]].script = new vm.Script(
|
||||||
r["_id"]
|
|
||||||
].script = new vm.Script(
|
|
||||||
`"use strict";(function(){\n${r["script"]}\n})();`,
|
`"use strict";(function(){\n${r["script"]}\n})();`,
|
||||||
{ filename: r["_id"], lineOffset: -1, timeout: 50 }
|
{ filename: r["_id"], lineOffset: -1, timeout: 50 }
|
||||||
);
|
);
|
||||||
@@ -325,8 +321,6 @@ async function fetchFiles(): Promise<Files> {
|
|||||||
const id = r["filename"] || r["_id"].toString();
|
const id = r["filename"] || r["_id"].toString();
|
||||||
files[id] = {};
|
files[id] = {};
|
||||||
files[id].length = r["length"];
|
files[id].length = r["length"];
|
||||||
files[id].md5 = r["md5"];
|
|
||||||
files[id].contentType = r["contentType"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
@@ -357,6 +351,7 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
|
|||||||
device: {},
|
device: {},
|
||||||
index: {},
|
index: {},
|
||||||
overview: {},
|
overview: {},
|
||||||
|
pageSize: null,
|
||||||
};
|
};
|
||||||
const _config = {};
|
const _config = {};
|
||||||
|
|
||||||
@@ -366,12 +361,13 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
|
|||||||
_config[c.id] = val;
|
_config[c.id] = val;
|
||||||
if (c.id.startsWith("ui.")) {
|
if (c.id.startsWith("ui.")) {
|
||||||
const keys = c.id.split(".");
|
const keys = c.id.split(".");
|
||||||
|
if (!(keys[1] in ui)) continue;
|
||||||
// remove the first key(ui)
|
// remove the first key(ui)
|
||||||
keys.shift();
|
keys.shift();
|
||||||
let ref = ui;
|
let ref = ui;
|
||||||
while (keys.length > 1) {
|
while (keys.length > 1) {
|
||||||
const k = keys.shift();
|
const k = keys.shift();
|
||||||
if (typeof ref[k] !== "object") ref[k] = {};
|
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
|
||||||
ref = ref[k];
|
ref = ref[k];
|
||||||
}
|
}
|
||||||
ref[keys[0]] = val;
|
ref[keys[0]] = val;
|
||||||
@@ -384,13 +380,15 @@ async function fetchConfig(): Promise<[Config, UiConfig]> {
|
|||||||
async function refresh(): Promise<void> {
|
async function refresh(): Promise<void> {
|
||||||
if (!nextRefresh) {
|
if (!nextRefresh) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
await refresh();
|
return refresh();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextRefresh = 0;
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (now < nextRefresh) return;
|
||||||
|
|
||||||
|
nextRefresh = 0;
|
||||||
|
|
||||||
const dbHash = await cache.get("presets_hash");
|
const dbHash = await cache.get("presets_hash");
|
||||||
|
|
||||||
if (currentSnapshot && dbHash === currentSnapshot) {
|
if (currentSnapshot && dbHash === currentSnapshot) {
|
||||||
@@ -398,7 +396,7 @@ async function refresh(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lockToken = await cache.acquireLock("presets_hash_lock", 4000, 4000);
|
const lockToken = await cache.acquireLock("presets_hash_lock", 5000);
|
||||||
|
|
||||||
const res = await Promise.all([
|
const res = await Promise.all([
|
||||||
fetchPresets(),
|
fetchPresets(),
|
||||||
@@ -431,8 +429,12 @@ async function refresh(): Promise<void> {
|
|||||||
|
|
||||||
currentSnapshot = computeHash(snapshot);
|
currentSnapshot = computeHash(snapshot);
|
||||||
snapshots.set(currentSnapshot, snapshot);
|
snapshots.set(currentSnapshot, snapshot);
|
||||||
|
|
||||||
|
if (lockToken) {
|
||||||
|
if (currentSnapshot !== dbHash)
|
||||||
await cache.set("presets_hash", currentSnapshot, 300);
|
await cache.set("presets_hash", currentSnapshot, 300);
|
||||||
await cache.releaseLock("presets_hash_lock", lockToken);
|
await cache.releaseLock("presets_hash_lock", lockToken);
|
||||||
|
}
|
||||||
|
|
||||||
nextRefresh = now + (REFRESH - (now % REFRESH));
|
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||||
}
|
}
|
||||||
@@ -501,7 +503,7 @@ export function getConfig(
|
|||||||
if (!(key in snapshot.config)) {
|
if (!(key in snapshot.config)) {
|
||||||
if (key in oldOpts) {
|
if (key in oldOpts) {
|
||||||
let id;
|
let id;
|
||||||
if (context && context["id"]) {
|
if (context?.["id"]) {
|
||||||
id = context["id"];
|
id = context["id"];
|
||||||
} else if (cb) {
|
} else if (cb) {
|
||||||
id = cb(["PARAM", "DeviceID.ID"]);
|
id = cb(["PARAM", "DeviceID.ID"]);
|
||||||
|
|||||||
+3
-5
@@ -292,11 +292,9 @@ function formatSimple(
|
|||||||
return `${severity}${remote}${details.message}${meta}${os.EOL}`;
|
return `${severity}${remote}${details.message}${meta}${os.EOL}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${
|
return `${details.timestamp} [${(
|
||||||
details.timestamp
|
details.severity as string
|
||||||
} [${(details.severity as string).toUpperCase()}] ${remote}${
|
).toUpperCase()}] ${remote}${details.message}${meta}${os.EOL}`;
|
||||||
details.message
|
|
||||||
}${meta}${os.EOL}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(details: Record<string, unknown>): void {
|
function log(details: Record<string, unknown>): void {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ObjectID } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import { map, parse, stringify, parseList } from "./common/expression-parser";
|
import { map, parse, stringify, parseList } from "./common/expression-parser";
|
||||||
import { likePatternToRegExp } from "./common/expression";
|
import { likePatternToRegExp } from "./common/expression";
|
||||||
import { Expression, Fault, Task } from "./types";
|
import { Expression, Fault, Task } from "./types";
|
||||||
@@ -77,7 +77,8 @@ export function processDeviceFilter(filter: Expression): Expression {
|
|||||||
if (typeof e[2] === "number") {
|
if (typeof e[2] === "number") {
|
||||||
const alt = (e as any[]).slice();
|
const alt = (e as any[]).slice();
|
||||||
alt[2] = new Date(e[2]);
|
alt[2] = new Date(e[2]);
|
||||||
e = ["OR", e, alt];
|
if (exp[0] === "<>" || exp[0] === "NOT LIKE") e = ["AND", e, alt];
|
||||||
|
else e = ["OR", e, alt];
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
@@ -90,7 +91,7 @@ export function processTasksFilter(filter: Expression): Expression {
|
|||||||
if (!isArray(exp)) return exp;
|
if (!isArray(exp)) return exp;
|
||||||
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
|
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
|
||||||
const e = exp.slice();
|
const e = exp.slice();
|
||||||
if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectID(e[2]);
|
if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectId(e[2]);
|
||||||
else if (e[1][0] === "PARAM" && e[1][1] === "timestamp")
|
else if (e[1][0] === "PARAM" && e[1][1] === "timestamp")
|
||||||
e[2] = new Date(e[2]);
|
e[2] = new Date(e[2]);
|
||||||
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
|
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { ObjectId } from "mongodb";
|
||||||
|
import { Expression, FaultStruct } from "./types";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
+112
-42
@@ -19,20 +19,23 @@
|
|||||||
|
|
||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
import { Collection, GridFSBucket, ObjectId } from "mongodb";
|
import { Collection, GridFSBucket, ObjectId } from "mongodb";
|
||||||
import * as querystring from "querystring";
|
|
||||||
import * as vm from "vm";
|
import * as vm from "vm";
|
||||||
import * as config from "./config";
|
import * as config from "./config";
|
||||||
import { onConnect } from "./db";
|
import { onConnect, optimizeProjection } from "./db";
|
||||||
import * as query from "./query";
|
import * as query from "./query";
|
||||||
import * as apiFunctions from "./api-functions";
|
import * as apiFunctions from "./api-functions";
|
||||||
import { IncomingMessage, ServerResponse } from "http";
|
import { IncomingMessage, ServerResponse } from "http";
|
||||||
import * as cache from "./cache";
|
import * as cache from "./cache";
|
||||||
import { version as VERSION } from "../package.json";
|
import { version as VERSION } from "../package.json";
|
||||||
import { ping } from "./ping";
|
import { ping } from "./ping";
|
||||||
|
import * as logger from "./logger";
|
||||||
|
import { flattenDevice } from "./mongodb-functions";
|
||||||
|
import { getSocketEndpoints } from "./server";
|
||||||
|
|
||||||
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
||||||
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
const TASKS_REGEX = /^\/tasks\/([a-zA-Z0-9\-_%]+)(\/[a-zA-Z_]*)?$/;
|
||||||
const TAGS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tags\/([a-zA-Z0-9\-_%]+)\/?$/;
|
const TAGS_REGEX =
|
||||||
|
/^\/devices\/([a-zA-Z0-9\-_%]+)\/tags\/([a-zA-Z0-9\-_%]+)\/?$/;
|
||||||
const PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-_%]+)\/?$/;
|
const PRESETS_REGEX = /^\/presets\/([a-zA-Z0-9\-_%]+)\/?$/;
|
||||||
const OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-_%]+)\/?$/;
|
const OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-_%]+)\/?$/;
|
||||||
const FILES_REGEX = /^\/files\/([a-zA-Z0-9%!*'();:@&=+$,?#[\]\-_.~]+)\/?$/;
|
const FILES_REGEX = /^\/files\/([a-zA-Z0-9%!*'();:@&=+$,?#[\]\-_.~]+)\/?$/;
|
||||||
@@ -40,15 +43,16 @@ const PING_REGEX = /^\/ping\/([a-zA-Z0-9\-_.:]+)\/?$/;
|
|||||||
const QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/;
|
const QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/;
|
||||||
const DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/?$/;
|
const DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/?$/;
|
||||||
const PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-_%]+)\/?$/;
|
const PROVISIONS_REGEX = /^\/provisions\/([a-zA-Z0-9\-_%]+)\/?$/;
|
||||||
const VIRTUAL_PARAMETERS_REGEX = /^\/virtual_parameters\/([a-zA-Z0-9\-_%]+)\/?$/;
|
const VIRTUAL_PARAMETERS_REGEX =
|
||||||
|
/^\/virtual_parameters\/([a-zA-Z0-9\-_%]+)\/?$/;
|
||||||
const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/;
|
const FAULTS_REGEX = /^\/faults\/([a-zA-Z0-9\-_%:]+)\/?$/;
|
||||||
|
|
||||||
const collections = {
|
const collections: Record<string, Collection> = {
|
||||||
tasks: null as Collection,
|
tasks: null as Collection,
|
||||||
devices: null as Collection,
|
devices: null as Collection,
|
||||||
presets: null as Collection,
|
presets: null as Collection,
|
||||||
objects: null as Collection,
|
objects: null as Collection,
|
||||||
"fs.files": null as Collection,
|
files: null as Collection,
|
||||||
provisions: null as Collection,
|
provisions: null as Collection,
|
||||||
virtualParameters: null as Collection,
|
virtualParameters: null as Collection,
|
||||||
faults: null as Collection,
|
faults: null as Collection,
|
||||||
@@ -56,7 +60,10 @@ const collections = {
|
|||||||
let filesBucket: GridFSBucket;
|
let filesBucket: GridFSBucket;
|
||||||
|
|
||||||
onConnect(async (db) => {
|
onConnect(async (db) => {
|
||||||
for (const k of Object.keys(collections)) collections[k] = db.collection(k);
|
for (const k of Object.keys(collections)) {
|
||||||
|
if (k === "files") collections[k] = db.collection("fs.files");
|
||||||
|
else collections[k] = db.collection(k);
|
||||||
|
}
|
||||||
filesBucket = new GridFSBucket(db);
|
filesBucket = new GridFSBucket(db);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,12 +102,28 @@ export function listener(
|
|||||||
request.addListener("end", () => {
|
request.addListener("end", () => {
|
||||||
const body = getBody();
|
const body = getBody();
|
||||||
const urlParts = url.parse(request.url, true);
|
const urlParts = url.parse(request.url, true);
|
||||||
|
const socketEndpoints = getSocketEndpoints(request.socket);
|
||||||
|
|
||||||
|
logger.accessInfo(
|
||||||
|
Object.assign({}, urlParts.query, {
|
||||||
|
remoteAddress: socketEndpoints.remoteAddress,
|
||||||
|
message: `${request.method} ${urlParts.pathname}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (PRESETS_REGEX.test(urlParts.pathname)) {
|
if (PRESETS_REGEX.test(urlParts.pathname)) {
|
||||||
const presetName = querystring.unescape(
|
const presetName = decodeURIComponent(
|
||||||
PRESETS_REGEX.exec(urlParts.pathname)[1]
|
PRESETS_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
if (request.method === "PUT") {
|
if (request.method === "PUT") {
|
||||||
const preset = JSON.parse(body.toString());
|
let preset;
|
||||||
|
try {
|
||||||
|
preset = JSON.parse(body.toString());
|
||||||
|
} catch (err) {
|
||||||
|
response.writeHead(400);
|
||||||
|
response.end(`${err.name}: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
preset._id = presetName;
|
preset._id = presetName;
|
||||||
collections.presets.replaceOne(
|
collections.presets.replaceOne(
|
||||||
{ _id: presetName },
|
{ _id: presetName },
|
||||||
@@ -143,11 +166,18 @@ export function listener(
|
|||||||
response.end("405 Method Not Allowed");
|
response.end("405 Method Not Allowed");
|
||||||
}
|
}
|
||||||
} else if (OBJECTS_REGEX.test(urlParts.pathname)) {
|
} else if (OBJECTS_REGEX.test(urlParts.pathname)) {
|
||||||
const objectName = querystring.unescape(
|
const objectName = decodeURIComponent(
|
||||||
OBJECTS_REGEX.exec(urlParts.pathname)[1]
|
OBJECTS_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
if (request.method === "PUT") {
|
if (request.method === "PUT") {
|
||||||
const object = JSON.parse(body.toString());
|
let object;
|
||||||
|
try {
|
||||||
|
object = JSON.parse(body.toString());
|
||||||
|
} catch (err) {
|
||||||
|
response.writeHead(400);
|
||||||
|
response.end(`${err.name}: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
object._id = objectName;
|
object._id = objectName;
|
||||||
collections.objects.replaceOne(
|
collections.objects.replaceOne(
|
||||||
{ _id: objectName },
|
{ _id: objectName },
|
||||||
@@ -190,7 +220,7 @@ export function listener(
|
|||||||
response.end("405 Method Not Allowed");
|
response.end("405 Method Not Allowed");
|
||||||
}
|
}
|
||||||
} else if (PROVISIONS_REGEX.test(urlParts.pathname)) {
|
} else if (PROVISIONS_REGEX.test(urlParts.pathname)) {
|
||||||
const provisionName = querystring.unescape(
|
const provisionName = decodeURIComponent(
|
||||||
PROVISIONS_REGEX.exec(urlParts.pathname)[1]
|
PROVISIONS_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
if (request.method === "PUT") {
|
if (request.method === "PUT") {
|
||||||
@@ -248,7 +278,7 @@ export function listener(
|
|||||||
response.end("405 Method Not Allowed");
|
response.end("405 Method Not Allowed");
|
||||||
}
|
}
|
||||||
} else if (VIRTUAL_PARAMETERS_REGEX.test(urlParts.pathname)) {
|
} else if (VIRTUAL_PARAMETERS_REGEX.test(urlParts.pathname)) {
|
||||||
const virtualParameterName = querystring.unescape(
|
const virtualParameterName = decodeURIComponent(
|
||||||
VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1]
|
VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
if (request.method === "PUT") {
|
if (request.method === "PUT") {
|
||||||
@@ -310,8 +340,8 @@ export function listener(
|
|||||||
}
|
}
|
||||||
} else if (TAGS_REGEX.test(urlParts.pathname)) {
|
} else if (TAGS_REGEX.test(urlParts.pathname)) {
|
||||||
const r = TAGS_REGEX.exec(urlParts.pathname);
|
const r = TAGS_REGEX.exec(urlParts.pathname);
|
||||||
const deviceId = querystring.unescape(r[1]);
|
const deviceId = decodeURIComponent(r[1]);
|
||||||
const tag = querystring.unescape(r[2]);
|
const tag = decodeURIComponent(r[2]);
|
||||||
if (request.method === "POST") {
|
if (request.method === "POST") {
|
||||||
collections.devices.updateOne(
|
collections.devices.updateOne(
|
||||||
{ _id: deviceId },
|
{ _id: deviceId },
|
||||||
@@ -339,7 +369,7 @@ export function listener(
|
|||||||
}
|
}
|
||||||
} else if (FAULTS_REGEX.test(urlParts.pathname)) {
|
} else if (FAULTS_REGEX.test(urlParts.pathname)) {
|
||||||
if (request.method === "DELETE") {
|
if (request.method === "DELETE") {
|
||||||
const faultId = querystring.unescape(
|
const faultId = decodeURIComponent(
|
||||||
FAULTS_REGEX.exec(urlParts.pathname)[1]
|
FAULTS_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
const deviceId = faultId.split(":", 1)[0];
|
const deviceId = faultId.split(":", 1)[0];
|
||||||
@@ -384,15 +414,31 @@ export function listener(
|
|||||||
}
|
}
|
||||||
} else if (DEVICE_TASKS_REGEX.test(urlParts.pathname)) {
|
} else if (DEVICE_TASKS_REGEX.test(urlParts.pathname)) {
|
||||||
if (request.method === "POST") {
|
if (request.method === "POST") {
|
||||||
const deviceId = querystring.unescape(
|
const deviceId = decodeURIComponent(
|
||||||
DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1]
|
DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
if (body.length) {
|
if (body.length) {
|
||||||
const task = JSON.parse(body.toString());
|
let task;
|
||||||
|
try {
|
||||||
|
task = JSON.parse(body.toString());
|
||||||
|
} catch (err) {
|
||||||
|
response.writeHead(400);
|
||||||
|
response.end(`${err.name}: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
task.device = deviceId;
|
task.device = deviceId;
|
||||||
apiFunctions
|
collections.devices
|
||||||
.insertTasks(task)
|
.findOne({ _id: deviceId })
|
||||||
.then(async () => {
|
.then(async (dev) => {
|
||||||
|
if (!dev) {
|
||||||
|
response.writeHead(404);
|
||||||
|
response.end("No such device");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = flattenDevice(dev);
|
||||||
|
await apiFunctions.insertTasks(task);
|
||||||
|
|
||||||
const lastInform = Date.now();
|
const lastInform = Date.now();
|
||||||
|
|
||||||
const socketTimeout: number = request.socket["timeout"];
|
const socketTimeout: number = request.socket["timeout"];
|
||||||
@@ -423,7 +469,10 @@ export function listener(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = await apiFunctions.connectionRequest(deviceId);
|
const status = await apiFunctions.connectionRequest(
|
||||||
|
deviceId,
|
||||||
|
device
|
||||||
|
);
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
if (socketTimeout) request.socket.setTimeout(socketTimeout);
|
if (socketTimeout) request.socket.setTimeout(socketTimeout);
|
||||||
@@ -506,15 +555,28 @@ export function listener(
|
|||||||
});
|
});
|
||||||
} else if (urlParts.query.connection_request != null) {
|
} else if (urlParts.query.connection_request != null) {
|
||||||
// No task, send connection request only
|
// No task, send connection request only
|
||||||
apiFunctions
|
collections.devices
|
||||||
.connectionRequest(deviceId)
|
.findOne({ _id: deviceId })
|
||||||
.then(() => {
|
.then(async (dev) => {
|
||||||
|
if (!dev) {
|
||||||
|
response.writeHead(404);
|
||||||
|
response.end("No such device");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return apiFunctions.connectionRequest(deviceId).then((status) => {
|
||||||
|
if (status) {
|
||||||
|
response.writeHead(504, status);
|
||||||
|
response.end(status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
response.writeHead(200);
|
response.writeHead(200);
|
||||||
response.end();
|
response.end();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
response.writeHead(504);
|
setTimeout(() => {
|
||||||
response.end(`${err.name}: ${err.message}`);
|
throwError(err, response);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(400);
|
response.writeHead(400);
|
||||||
@@ -526,7 +588,7 @@ export function listener(
|
|||||||
}
|
}
|
||||||
} else if (TASKS_REGEX.test(urlParts.pathname)) {
|
} else if (TASKS_REGEX.test(urlParts.pathname)) {
|
||||||
const r = TASKS_REGEX.exec(urlParts.pathname);
|
const r = TASKS_REGEX.exec(urlParts.pathname);
|
||||||
const taskId = querystring.unescape(r[1]);
|
const taskId = decodeURIComponent(r[1]);
|
||||||
const action = r[2];
|
const action = r[2];
|
||||||
if (!action || action === "/") {
|
if (!action || action === "/") {
|
||||||
if (request.method === "DELETE") {
|
if (request.method === "DELETE") {
|
||||||
@@ -612,7 +674,7 @@ export function listener(
|
|||||||
response.end();
|
response.end();
|
||||||
}
|
}
|
||||||
} else if (FILES_REGEX.test(urlParts.pathname)) {
|
} else if (FILES_REGEX.test(urlParts.pathname)) {
|
||||||
const filename = querystring.unescape(
|
const filename = decodeURIComponent(
|
||||||
FILES_REGEX.exec(urlParts.pathname)[1]
|
FILES_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
if (request.method === "PUT") {
|
if (request.method === "PUT") {
|
||||||
@@ -622,9 +684,9 @@ export function listener(
|
|||||||
productClass: request.headers.productclass,
|
productClass: request.headers.productclass,
|
||||||
version: request.headers.version,
|
version: request.headers.version,
|
||||||
};
|
};
|
||||||
filesBucket.delete((filename as unknown) as ObjectId, () => {
|
filesBucket.delete(filename as unknown as ObjectId, () => {
|
||||||
const uploadStream = filesBucket.openUploadStreamWithId(
|
const uploadStream = filesBucket.openUploadStreamWithId(
|
||||||
filename,
|
filename as unknown as ObjectId,
|
||||||
filename,
|
filename,
|
||||||
{
|
{
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
@@ -641,7 +703,7 @@ export function listener(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (request.method === "DELETE") {
|
} else if (request.method === "DELETE") {
|
||||||
filesBucket.delete((filename as unknown) as ObjectId, (err) => {
|
filesBucket.delete(filename as unknown as ObjectId, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.message.startsWith("FileNotFound")) {
|
if (err.message.startsWith("FileNotFound")) {
|
||||||
response.writeHead(404);
|
response.writeHead(404);
|
||||||
@@ -659,7 +721,7 @@ export function listener(
|
|||||||
response.end("405 Method Not Allowed");
|
response.end("405 Method Not Allowed");
|
||||||
}
|
}
|
||||||
} else if (PING_REGEX.test(urlParts.pathname)) {
|
} else if (PING_REGEX.test(urlParts.pathname)) {
|
||||||
const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]);
|
const host = decodeURIComponent(PING_REGEX.exec(urlParts.pathname)[1]);
|
||||||
ping(host, (err, res, stdout) => {
|
ping(host, (err, res, stdout) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (!res) {
|
if (!res) {
|
||||||
@@ -685,7 +747,7 @@ export function listener(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceId = querystring.unescape(
|
const deviceId = decodeURIComponent(
|
||||||
DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1]
|
DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1]
|
||||||
);
|
);
|
||||||
apiFunctions
|
apiFunctions
|
||||||
@@ -759,12 +821,20 @@ export function listener(
|
|||||||
projection = {};
|
projection = {};
|
||||||
for (const p of (urlParts.query.projection as string).split(","))
|
for (const p of (urlParts.query.projection as string).split(","))
|
||||||
projection[p.trim()] = 1;
|
projection[p.trim()] = 1;
|
||||||
|
projection = optimizeProjection(projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cur = collection.find(q, { projection: projection });
|
const cur = collection.find(q, { projection: projection });
|
||||||
|
|
||||||
if (urlParts.query.sort) {
|
if (urlParts.query.sort) {
|
||||||
const s = JSON.parse(urlParts.query.sort as string);
|
let s;
|
||||||
|
try {
|
||||||
|
s = JSON.parse(urlParts.query.sort as string);
|
||||||
|
} catch (err) {
|
||||||
|
response.writeHead(400);
|
||||||
|
response.end(`${err.name}: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sort = {};
|
const sort = {};
|
||||||
for (const [k, v] of Object.entries(s)) {
|
for (const [k, v] of Object.entries(s)) {
|
||||||
if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices")
|
if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices")
|
||||||
@@ -775,13 +845,7 @@ export function listener(
|
|||||||
cur.sort(sort);
|
cur.sort(sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urlParts.query.skip)
|
collection.countDocuments(q, (err, total) => {
|
||||||
cur.skip(parseInt(urlParts.query.skip as string));
|
|
||||||
|
|
||||||
if (urlParts.query.limit)
|
|
||||||
cur.limit(parseInt(urlParts.query.limit as string));
|
|
||||||
|
|
||||||
cur.count(false, (err, total) => {
|
|
||||||
if (err) return void throwError(err);
|
if (err) return void throwError(err);
|
||||||
|
|
||||||
response.writeHead(200, {
|
response.writeHead(200, {
|
||||||
@@ -794,6 +858,12 @@ export function listener(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (urlParts.query.skip)
|
||||||
|
cur.skip(parseInt(urlParts.query.skip as string));
|
||||||
|
|
||||||
|
if (urlParts.query.limit)
|
||||||
|
cur.limit(parseInt(urlParts.query.limit as string));
|
||||||
|
|
||||||
response.write("[\n");
|
response.write("[\n");
|
||||||
i = 0;
|
i = 0;
|
||||||
cur.forEach(
|
cur.forEach(
|
||||||
|
|||||||
+49
-21
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
import { platform } from "os";
|
import { platform } from "os";
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
|
import { domainToASCII } from "url";
|
||||||
|
|
||||||
export interface PingResult {
|
export interface PingResult {
|
||||||
packetsTransmitted: number;
|
packetsTransmitted: number;
|
||||||
@@ -30,33 +31,33 @@ export interface PingResult {
|
|||||||
mdev: number;
|
mdev: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ping(
|
function isValidHost(host: string): boolean {
|
||||||
host: string,
|
// Valid chars in IPv4, IPv6, domain names
|
||||||
callback: (err: Error, res?: PingResult, stdout?: string) => void
|
if (/^[a-zA-Z0-9\-.:[\]-]+$/.test(host)) return true;
|
||||||
): void {
|
|
||||||
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
|
// Check if input is an IDN convert to Punycode
|
||||||
switch (platform()) {
|
// 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":
|
case "linux":
|
||||||
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
|
parseRegExp1 =
|
||||||
parseRegExp1 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
|
/(\d+) packets transmitted, (\d+) .*received, ([\d.]+)% .*loss[^]*= ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
|
||||||
parseRegExp2 = /(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/;
|
parseRegExp2 =
|
||||||
|
/(\d+) packets transmitted, (\d+) .*received, ([\d.]+)% .*loss/;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "freebsd":
|
case "freebsd":
|
||||||
// Send a single packet because on FreeBSD only superuser can send
|
parseRegExp1 =
|
||||||
// packets that are only 200 ms apart.
|
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
|
||||||
cmd = `ping -t 1 -c 3 ${host}`;
|
parseRegExp2 =
|
||||||
parseRegExp1 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
|
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
|
||||||
parseRegExp2 = /(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
|
||||||
return callback(new Error("Platform not supported"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exec(cmd, (err, stdout) => {
|
|
||||||
let parsed: PingResult;
|
|
||||||
if (stdout) {
|
|
||||||
const m1 = stdout.match(parseRegExp1);
|
const m1 = stdout.match(parseRegExp1);
|
||||||
if (m1) {
|
if (m1) {
|
||||||
parsed = {
|
parsed = {
|
||||||
@@ -82,8 +83,35 @@ export function ping(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ping(
|
||||||
|
host: string,
|
||||||
|
callback: (err: Error, res?: PingResult, stdout?: string) => void
|
||||||
|
): void {
|
||||||
|
// Validate input to prevent possible remote code execution
|
||||||
|
// Credit to Alex Hordijk for reporting this vulnerability
|
||||||
|
if (!isValidHost(host)) return callback(new Error("Invalid host"));
|
||||||
|
host = host.replace("[", "").replace("]", "");
|
||||||
|
let cmd: string;
|
||||||
|
|
||||||
|
switch (platform()) {
|
||||||
|
case "linux":
|
||||||
|
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
|
||||||
|
break;
|
||||||
|
case "freebsd":
|
||||||
|
// Send a single packet because on FreeBSD only superuser can send
|
||||||
|
// packets that are only 200 ms apart.
|
||||||
|
cmd = `ping -t 1 -c 3 ${host}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return callback(new Error("Platform not supported"));
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(err, parsed, stdout);
|
exec(cmd, (err, stdout) => {
|
||||||
|
if (err) return callback(err);
|
||||||
|
const parsed: PingResult = parsePing(platform(), stdout);
|
||||||
|
return callback(err, parsed, stdout);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+13
-1
@@ -54,6 +54,17 @@ function normalize(input): any {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EXPAND_OPS = new Set([
|
||||||
|
"$eq",
|
||||||
|
"$gt",
|
||||||
|
"$gte",
|
||||||
|
"$in",
|
||||||
|
"$lt",
|
||||||
|
"$lte",
|
||||||
|
"$ne",
|
||||||
|
"$nin",
|
||||||
|
]);
|
||||||
|
|
||||||
function expandValue(value: unknown): unknown[] {
|
function expandValue(value: unknown): unknown[] {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
let a = [];
|
let a = [];
|
||||||
@@ -71,7 +82,8 @@ 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);
|
||||||
values.push(expandValue(v));
|
if (EXPAND_OPS.has(k)) values.push(expandValue(v));
|
||||||
|
else values.push([v]);
|
||||||
indices.push(0);
|
indices.push(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ 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 {
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import * as later from "later";
|
import * as later from "@breejs/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();
|
||||||
|
|||||||
@@ -59,8 +59,20 @@ interface ServerOptions {
|
|||||||
timeout?: number;
|
timeout?: number;
|
||||||
keepAliveTimeout?: number;
|
keepAliveTimeout?: number;
|
||||||
onConnection?: (socket: Socket) => void;
|
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();
|
||||||
|
|
||||||
export function start(
|
export function start(
|
||||||
options: ServerOptions,
|
options: ServerOptions,
|
||||||
_listener: http.RequestListener
|
_listener: http.RequestListener
|
||||||
@@ -85,6 +97,27 @@ export function start(
|
|||||||
if (options.onConnection) server.on("connection", options.onConnection);
|
if (options.onConnection) server.on("connection", options.onConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.on("connection", (socket: Socket) => {
|
||||||
|
socketEndpoints.set(socket, {
|
||||||
|
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;
|
server.timeout = options.timeout || 0;
|
||||||
if (options.keepAliveTimeout != null)
|
if (options.keepAliveTimeout != null)
|
||||||
server.keepAliveTimeout = options.keepAliveTimeout;
|
server.keepAliveTimeout = options.keepAliveTimeout;
|
||||||
@@ -99,3 +132,8 @@ export function stop(): 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);
|
||||||
|
}
|
||||||
+198
-86
@@ -59,6 +59,7 @@ import {
|
|||||||
GetParameterValuesResponse,
|
GetParameterValuesResponse,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getRequestOrigin } from "./forwarded";
|
import { getRequestOrigin } from "./forwarded";
|
||||||
|
import * as logger from "./logger";
|
||||||
|
|
||||||
const VALID_PARAM_TYPES = new Set([
|
const VALID_PARAM_TYPES = new Set([
|
||||||
"xsd:int",
|
"xsd:int",
|
||||||
@@ -135,7 +136,7 @@ export function configContextCallback(
|
|||||||
const path = paths.get(Path.parse(name));
|
const path = paths.get(Path.parse(name));
|
||||||
if (path) {
|
if (path) {
|
||||||
const attrs = deviceData.attributes.get(path, 1);
|
const attrs = deviceData.attributes.get(path, 1);
|
||||||
if (attrs && attrs.value && attrs.value[1]) return attrs.value[1][0];
|
if (attrs?.value?.[1]) return attrs.value[1][0];
|
||||||
}
|
}
|
||||||
} else if (exp[0] === "FUNC") {
|
} else if (exp[0] === "FUNC") {
|
||||||
if (exp[1] === "REMOTE_ADDRESS")
|
if (exp[1] === "REMOTE_ADDRESS")
|
||||||
@@ -298,7 +299,7 @@ export async function transferComplete(
|
|||||||
if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {};
|
if (!sessionContext.operationsTouched) sessionContext.operationsTouched = {};
|
||||||
sessionContext.operationsTouched[commandKey] = 1;
|
sessionContext.operationsTouched[commandKey] = 1;
|
||||||
|
|
||||||
if (rpcReq.faultStruct && rpcReq.faultStruct.faultCode !== "0") {
|
if (rpcReq.faultStruct?.faultCode !== "0") {
|
||||||
revertDownloadParameters(sessionContext, operation.args.instance);
|
revertDownloadParameters(sessionContext, operation.args.instance);
|
||||||
|
|
||||||
const fault: Fault = {
|
const fault: Fault = {
|
||||||
@@ -417,13 +418,7 @@ function revertDownloadParameters(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const toClear = device.set(sessionContext.deviceData, p, timestamp, {
|
const toClear = device.set(sessionContext.deviceData, p, timestamp, {
|
||||||
value: [
|
value: [timestamp, [lastDownload?.value[1]?.[0] || 0, "xsd:dateTime"]],
|
||||||
timestamp,
|
|
||||||
[
|
|
||||||
lastDownload && lastDownload.value[1] ? lastDownload.value[1][0] : 0,
|
|
||||||
"xsd:dateTime",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (toClear) {
|
if (toClear) {
|
||||||
@@ -457,7 +452,36 @@ export async function timeoutOperations(
|
|||||||
(e) => configContextCallback(sessionContext, e)
|
(e) => configContextCallback(sessionContext, e)
|
||||||
) * 1000;
|
) * 1000;
|
||||||
|
|
||||||
if (sessionContext.timestamp > operation.timestamp + DOWNLOAD_TIMEOUT) {
|
if (sessionContext.timestamp < operation.timestamp + DOWNLOAD_TIMEOUT)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
logger.accessWarn({
|
||||||
|
sessionContext: sessionContext,
|
||||||
|
message: "Download operation timed out",
|
||||||
|
commandKey: commandKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SUCCESS_ON_TIMEOUT = +localCache.getConfig(
|
||||||
|
sessionContext.cacheSnapshot,
|
||||||
|
"cwmp.downloadSuccessOnTimeout",
|
||||||
|
{},
|
||||||
|
sessionContext.timestamp,
|
||||||
|
(e) => configContextCallback(sessionContext, e)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (SUCCESS_ON_TIMEOUT) {
|
||||||
|
const r = {
|
||||||
|
name: "TransferComplete" as const,
|
||||||
|
commandKey: commandKey,
|
||||||
|
startTime: 0,
|
||||||
|
completeTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call transferComplete code and ignore the response
|
||||||
|
await transferComplete(sessionContext, r);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
delete sessionContext.operations[commandKey];
|
delete sessionContext.operations[commandKey];
|
||||||
if (!sessionContext.operationsTouched)
|
if (!sessionContext.operationsTouched)
|
||||||
sessionContext.operationsTouched = {};
|
sessionContext.operationsTouched = {};
|
||||||
@@ -473,7 +497,6 @@ export async function timeoutOperations(
|
|||||||
|
|
||||||
revertDownloadParameters(sessionContext, operation.args.instance);
|
revertDownloadParameters(sessionContext, operation.args.instance);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return { faults, operations };
|
return { faults, operations };
|
||||||
}
|
}
|
||||||
@@ -481,7 +504,7 @@ export async function timeoutOperations(
|
|||||||
export function addProvisions(
|
export function addProvisions(
|
||||||
sessionContext: SessionContext,
|
sessionContext: SessionContext,
|
||||||
channel: string,
|
channel: string,
|
||||||
provisions: (string | number | boolean)[][]
|
provisions: [string, ...Expression[]][]
|
||||||
): void {
|
): void {
|
||||||
// Multiply by two because every iteration is two
|
// Multiply by two because every iteration is two
|
||||||
// phases: read and update
|
// phases: read and update
|
||||||
@@ -910,10 +933,15 @@ function runDeclarations(
|
|||||||
if (!unpacked)
|
if (!unpacked)
|
||||||
unpacked = device.unpack(sessionContext.deviceData, path);
|
unpacked = device.unpack(sessionContext.deviceData, path);
|
||||||
|
|
||||||
for (const u of unpacked)
|
for (const u of unpacked) {
|
||||||
mergeAttributeValues(u, declaration.attrSet, declaration.defer);
|
mergeAttributeValues(u, declaration.attrSet, declaration.defer);
|
||||||
|
// Ensure writable attr is available
|
||||||
|
mergeAttributeTimestamps(u, { writable: 1 });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mergeAttributeValues(path, declaration.attrSet, declaration.defer);
|
mergeAttributeValues(path, declaration.attrSet, declaration.defer);
|
||||||
|
// Ensure writable attr is available
|
||||||
|
mergeAttributeTimestamps(path, { writable: 1 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,14 +965,15 @@ function runDeclarations(
|
|||||||
keys = {};
|
keys = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!parent.wildcard && !parent.alias) {
|
||||||
((path.wildcard | path.alias) & ((1 << (path.length - 1)) - 1)) ===
|
|
||||||
0
|
|
||||||
) {
|
|
||||||
parent = sessionContext.deviceData.paths.add(parent);
|
parent = sessionContext.deviceData.paths.add(parent);
|
||||||
if (!unpacked)
|
if (!unpacked)
|
||||||
unpacked = device.unpack(sessionContext.deviceData, path);
|
unpacked = device.unpack(sessionContext.deviceData, path);
|
||||||
|
|
||||||
|
// Ensure writable attr is available
|
||||||
|
mergeAttributeTimestamps(parent, { writable: 1 });
|
||||||
|
for (const u of unpacked) mergeAttributeTimestamps(u, { writable: 1 });
|
||||||
|
|
||||||
processInstances(
|
processInstances(
|
||||||
sessionContext,
|
sessionContext,
|
||||||
parent,
|
parent,
|
||||||
@@ -960,13 +989,19 @@ function runDeclarations(
|
|||||||
parent
|
parent
|
||||||
);
|
);
|
||||||
for (const par of parentsUnpacked) {
|
for (const par of parentsUnpacked) {
|
||||||
|
const up = device.unpack(
|
||||||
|
sessionContext.deviceData,
|
||||||
|
par.concat(path.slice(-1))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure writable attr is available
|
||||||
|
mergeAttributeTimestamps(par, { writable: 1 });
|
||||||
|
for (const u of up) mergeAttributeTimestamps(u, { writable: 1 });
|
||||||
|
|
||||||
processInstances(
|
processInstances(
|
||||||
sessionContext,
|
sessionContext,
|
||||||
par,
|
par,
|
||||||
device.unpack(
|
up,
|
||||||
sessionContext.deviceData,
|
|
||||||
par.concat(path.slice(-1))
|
|
||||||
),
|
|
||||||
keys,
|
keys,
|
||||||
minInstances,
|
minInstances,
|
||||||
maxInstances,
|
maxInstances,
|
||||||
@@ -1000,7 +1035,7 @@ export async function rpcRequest(
|
|||||||
if (
|
if (
|
||||||
!sessionContext.virtualParameters.length &&
|
!sessionContext.virtualParameters.length &&
|
||||||
!sessionContext.declarations.length &&
|
!sessionContext.declarations.length &&
|
||||||
!(_declarations && _declarations.length) &&
|
!_declarations?.length &&
|
||||||
!sessionContext.provisions.length
|
!sessionContext.provisions.length
|
||||||
)
|
)
|
||||||
return { fault: null, rpcId: null, rpc: null };
|
return { fault: null, rpcId: null, rpc: null };
|
||||||
@@ -1075,12 +1110,11 @@ export async function rpcRequest(
|
|||||||
return rpcRequest(sessionContext, _declarations);
|
return rpcRequest(sessionContext, _declarations);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_declarations && _declarations.length) {
|
if (_declarations?.length) {
|
||||||
delete sessionContext.syncState;
|
delete sessionContext.syncState;
|
||||||
if (!sessionContext.declarations[0]) sessionContext.declarations[0] = [];
|
if (!sessionContext.declarations[0]) sessionContext.declarations[0] = [];
|
||||||
sessionContext.declarations[0] = sessionContext.declarations[0].concat(
|
sessionContext.declarations[0] =
|
||||||
_declarations
|
sessionContext.declarations[0].concat(_declarations);
|
||||||
);
|
|
||||||
return rpcRequest(sessionContext, null);
|
return rpcRequest(sessionContext, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1411,7 +1445,7 @@ export async function rpcRequest(
|
|||||||
for (const [p, v] of sessionContext.syncState.downloadsValues) {
|
for (const [p, v] of sessionContext.syncState.downloadsValues) {
|
||||||
const attrs = sessionContext.deviceData.attributes.get(p);
|
const attrs = sessionContext.deviceData.attributes.get(p);
|
||||||
if (attrs) {
|
if (attrs) {
|
||||||
if (attrs.writable && attrs.writable[1] && attrs.value) {
|
if (attrs.writable?.[1] && attrs.value) {
|
||||||
const val = device.sanitizeParameterValue([v, attrs.value[1][1]]);
|
const val = device.sanitizeParameterValue([v, attrs.value[1][1]]);
|
||||||
if (val[0] !== attrs.value[1][0]) {
|
if (val[0] !== attrs.value[1][0]) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
@@ -1590,6 +1624,20 @@ function generateGetRpcRequest(
|
|||||||
(a, b) => b.length - a.length
|
(a, b) => b.length - a.length
|
||||||
);
|
);
|
||||||
let path = paths.pop();
|
let path = paths.pop();
|
||||||
|
|
||||||
|
// Skip root GPN workaround
|
||||||
|
if (path && !path.length) {
|
||||||
|
const SKIP_ROOT_GPN = !!localCache.getConfig(
|
||||||
|
sessionContext.cacheSnapshot,
|
||||||
|
"cwmp.skipRootGpn",
|
||||||
|
{},
|
||||||
|
sessionContext.timestamp,
|
||||||
|
(e) => configContextCallback(sessionContext, e)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (SKIP_ROOT_GPN) path = paths.pop();
|
||||||
|
}
|
||||||
|
|
||||||
while (
|
while (
|
||||||
path &&
|
path &&
|
||||||
path.length &&
|
path.length &&
|
||||||
@@ -1652,7 +1700,7 @@ function generateGetRpcRequest(
|
|||||||
syncState.refreshAttributes.value.delete(path);
|
syncState.refreshAttributes.value.delete(path);
|
||||||
// Need to check in case param is deleted or changed to object
|
// Need to check in case param is deleted or changed to object
|
||||||
const attrs = sessionContext.deviceData.attributes.get(path);
|
const attrs = sessionContext.deviceData.attributes.get(path);
|
||||||
if (attrs && attrs.object && attrs.object[1] === 0) {
|
if (attrs?.object?.[1] === 0) {
|
||||||
parameterNames.push(path.toString());
|
parameterNames.push(path.toString());
|
||||||
if (parameterNames.length >= GPV_BATCH_SIZE) break;
|
if (parameterNames.length >= GPV_BATCH_SIZE) break;
|
||||||
}
|
}
|
||||||
@@ -1805,7 +1853,7 @@ function generateSetRpcRequest(
|
|||||||
for (const [k, v] of syncState.spv) {
|
for (const [k, v] of syncState.spv) {
|
||||||
syncState.spv.delete(k);
|
syncState.spv.delete(k);
|
||||||
const attrs = sessionContext.deviceData.attributes.get(k);
|
const attrs = sessionContext.deviceData.attributes.get(k);
|
||||||
const curVal = attrs.value ? attrs.value[1] : null;
|
const curVal = attrs.value?.[1];
|
||||||
if (curVal && canWrite(attrs)) {
|
if (curVal && canWrite(attrs)) {
|
||||||
const val = v.slice() as [string | number | boolean, string];
|
const val = v.slice() as [string | number | boolean, string];
|
||||||
if (!val[1]) val[1] = curVal[1];
|
if (!val[1]) val[1] = curVal[1];
|
||||||
@@ -1869,8 +1917,9 @@ function generateSetRpcRequest(
|
|||||||
|
|
||||||
// Downloads
|
// Downloads
|
||||||
for (const [p, t] of syncState.downloadsDownload) {
|
for (const [p, t] of syncState.downloadsDownload) {
|
||||||
|
if (!(t > 0 && t <= sessionContext.timestamp)) continue;
|
||||||
const attrs = deviceData.attributes.get(p);
|
const attrs = deviceData.attributes.get(p);
|
||||||
if (!(attrs && attrs.value && t <= attrs.value[1][0])) {
|
if (!(t <= attrs?.value?.[1]?.[0])) {
|
||||||
const fileTypeAttrs = deviceData.attributes.get(
|
const fileTypeAttrs = deviceData.attributes.get(
|
||||||
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType")))
|
deviceData.paths.get(p.slice(0, -1).concat(Path.parse("FileType")))
|
||||||
);
|
);
|
||||||
@@ -1887,42 +1936,31 @@ function generateSetRpcRequest(
|
|||||||
name: "Download",
|
name: "Download",
|
||||||
commandKey: generateRpcId(sessionContext),
|
commandKey: generateRpcId(sessionContext),
|
||||||
instance: p.segments[1] as string,
|
instance: p.segments[1] as string,
|
||||||
fileType: fileTypeAttrs
|
fileType: fileTypeAttrs?.value?.[1][0] as string,
|
||||||
? fileTypeAttrs.value
|
fileName: fileNameAttrs?.value?.[1][0] as string,
|
||||||
? (fileTypeAttrs.value[1][0] as string)
|
targetFileName: targetFileNameAttrs?.value?.[1][0] as string,
|
||||||
: null
|
|
||||||
: null,
|
|
||||||
fileName: fileNameAttrs
|
|
||||||
? fileNameAttrs.value
|
|
||||||
? (fileNameAttrs.value[1][0] as string)
|
|
||||||
: null
|
|
||||||
: null,
|
|
||||||
targetFileName: targetFileNameAttrs
|
|
||||||
? targetFileNameAttrs.value
|
|
||||||
? (targetFileNameAttrs.value[1][0] as string)
|
|
||||||
: null
|
|
||||||
: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reboot
|
// Reboot
|
||||||
if (syncState.reboot) {
|
if (syncState.reboot > 0 && syncState.reboot <= sessionContext.timestamp) {
|
||||||
const p = sessionContext.deviceData.paths.get(Path.parse("Reboot"));
|
const p = sessionContext.deviceData.paths.get(Path.parse("Reboot"));
|
||||||
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
|
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
|
||||||
if (!(attrs && attrs.value && attrs.value[1][0] >= syncState.reboot)) {
|
if (!(attrs?.value?.[1][0] >= syncState.reboot)) {
|
||||||
delete syncState.reboot;
|
delete syncState.reboot;
|
||||||
return { name: "Reboot" };
|
return { name: "Reboot" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Factory reset
|
// Factory reset
|
||||||
if (syncState.factoryReset) {
|
if (
|
||||||
|
syncState.factoryReset > 0 &&
|
||||||
|
syncState.factoryReset <= sessionContext.timestamp
|
||||||
|
) {
|
||||||
const p = sessionContext.deviceData.paths.get(Path.parse("FactoryReset"));
|
const p = sessionContext.deviceData.paths.get(Path.parse("FactoryReset"));
|
||||||
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
|
const attrs = p ? sessionContext.deviceData.attributes.get(p) : null;
|
||||||
if (
|
if (!(attrs?.value?.[1][0] >= syncState.factoryReset)) {
|
||||||
!(attrs && attrs.value && attrs.value[1][0] >= syncState.factoryReset)
|
|
||||||
) {
|
|
||||||
delete syncState.factoryReset;
|
delete syncState.factoryReset;
|
||||||
return { name: "FactoryReset" };
|
return { name: "FactoryReset" };
|
||||||
}
|
}
|
||||||
@@ -1990,7 +2028,7 @@ function generateSetVirtualParameterProvisions(
|
|||||||
let provisions;
|
let provisions;
|
||||||
if (virtualParameterDeclarations) {
|
if (virtualParameterDeclarations) {
|
||||||
for (const declaration of virtualParameterDeclarations) {
|
for (const declaration of virtualParameterDeclarations) {
|
||||||
if (declaration[2] && declaration[2].value != null) {
|
if (declaration[2]?.value != null) {
|
||||||
const attrs = sessionContext.deviceData.attributes.get(declaration[0]);
|
const attrs = sessionContext.deviceData.attributes.get(declaration[0]);
|
||||||
if (
|
if (
|
||||||
attrs &&
|
attrs &&
|
||||||
@@ -2119,9 +2157,7 @@ function processDeclarations(
|
|||||||
|
|
||||||
if (currentAttributes) {
|
if (currentAttributes) {
|
||||||
leafParam = currentPath;
|
leafParam = currentPath;
|
||||||
leafIsObject = currentAttributes.object
|
leafIsObject = currentAttributes.object?.[1];
|
||||||
? currentAttributes.object[1]
|
|
||||||
: null;
|
|
||||||
// Possible V8 bug causes null === 0
|
// Possible V8 bug causes null === 0
|
||||||
if (leafIsObject != null && leafIsObject === 0)
|
if (leafIsObject != null && leafIsObject === 0)
|
||||||
leafTimestamp = Math.max(leafTimestamp, currentAttributes.object[0]);
|
leafTimestamp = Math.max(leafTimestamp, currentAttributes.object[0]);
|
||||||
@@ -2136,13 +2172,13 @@ function processDeclarations(
|
|||||||
) {
|
) {
|
||||||
case "Reboot":
|
case "Reboot":
|
||||||
if (currentPath.length === 1) {
|
if (currentPath.length === 1) {
|
||||||
if (declareAttributeValues && declareAttributeValues.value)
|
if (declareAttributeValues?.value)
|
||||||
syncState.reboot = +new Date(declareAttributeValues.value[0]);
|
syncState.reboot = +new Date(declareAttributeValues.value[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "FactoryReset":
|
case "FactoryReset":
|
||||||
if (currentPath.length === 1) {
|
if (currentPath.length === 1) {
|
||||||
if (declareAttributeValues && declareAttributeValues.value)
|
if (declareAttributeValues?.value)
|
||||||
syncState.factoryReset = +new Date(declareAttributeValues.value[0]);
|
syncState.factoryReset = +new Date(declareAttributeValues.value[0]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -2278,7 +2314,7 @@ function processDeclarations(
|
|||||||
syncState.refreshAttributes.object.add(currentPath);
|
syncState.refreshAttributes.object.add(currentPath);
|
||||||
else if (currentAttributes.object[1] === 0)
|
else if (currentAttributes.object[1] === 0)
|
||||||
syncState.refreshAttributes.value.add(currentPath);
|
syncState.refreshAttributes.value.add(currentPath);
|
||||||
} else {
|
} else if (attrName in syncState.refreshAttributes) {
|
||||||
syncState.refreshAttributes[attrName].add(currentPath);
|
syncState.refreshAttributes[attrName].add(currentPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2407,9 +2443,16 @@ export async function rpcResponse(
|
|||||||
sessionContext: SessionContext,
|
sessionContext: SessionContext,
|
||||||
id: string,
|
id: string,
|
||||||
_rpcRes: CpeResponse
|
_rpcRes: CpeResponse
|
||||||
): Promise<void> {
|
): Promise<Fault> {
|
||||||
|
function invalidResponse(message: string): Fault {
|
||||||
|
return {
|
||||||
|
code: "invalid_response",
|
||||||
|
message: message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (id !== generateRpcId(sessionContext))
|
if (id !== generateRpcId(sessionContext))
|
||||||
throw new Error("Request ID not recognized");
|
return invalidResponse("Request ID not recognized");
|
||||||
|
|
||||||
++sessionContext.rpcCount;
|
++sessionContext.rpcCount;
|
||||||
|
|
||||||
@@ -2500,39 +2543,99 @@ export async function rpcResponse(
|
|||||||
|
|
||||||
if (rpcRes.name === "GetParameterValuesResponse") {
|
if (rpcRes.name === "GetParameterValuesResponse") {
|
||||||
if (rpcReq.name !== "GetParameterValues")
|
if (rpcReq.name !== "GetParameterValues")
|
||||||
throw new Error("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
for (const p of rpcRes.parameterList) {
|
const requested = new Set(rpcReq.parameterNames);
|
||||||
|
|
||||||
|
for (const [path, value, type] of rpcRes.parameterList) {
|
||||||
|
if (!requested.delete(path.toString())) {
|
||||||
|
logger.accessWarn({
|
||||||
|
sessionContext: sessionContext,
|
||||||
|
message: "Unexpected parameter in response",
|
||||||
|
parameter: path.toString(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p[0],
|
path,
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
object: [timestamp, 0],
|
object: [timestamp, 0],
|
||||||
value: [timestamp, p.slice(1) as [string | number | boolean, string]],
|
value: [timestamp, [value, type]],
|
||||||
},
|
},
|
||||||
toClear
|
toClear
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requested.size) {
|
||||||
|
for (const p of requested) {
|
||||||
|
logger.accessWarn({
|
||||||
|
sessionContext: sessionContext,
|
||||||
|
message: "Missing parameter in response",
|
||||||
|
parameter: p,
|
||||||
|
});
|
||||||
|
toClear = device.set(
|
||||||
|
sessionContext.deviceData,
|
||||||
|
Path.parse(p),
|
||||||
|
timestamp,
|
||||||
|
{
|
||||||
|
object: [timestamp, 0],
|
||||||
|
value: [timestamp, ["", "xsd:string"]],
|
||||||
|
},
|
||||||
|
toClear
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (rpcRes.name === "GetParameterAttributesResponse") {
|
} else if (rpcRes.name === "GetParameterAttributesResponse") {
|
||||||
if (rpcReq.name !== "GetParameterAttributes")
|
if (rpcReq.name !== "GetParameterAttributes")
|
||||||
throw new Error("Response name does not match request name");
|
throw new Error("Response name does not match request name");
|
||||||
|
|
||||||
for (const p of rpcRes.parameterList) {
|
const requested = new Set(rpcReq.parameterNames);
|
||||||
|
|
||||||
|
for (const [path, notification, accessList] of rpcRes.parameterList) {
|
||||||
|
if (!requested.delete(path.toString())) {
|
||||||
|
logger.accessWarn({
|
||||||
|
sessionContext: sessionContext,
|
||||||
|
message: "Unexpected parameter in response",
|
||||||
|
parameter: path.toString(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
p[0],
|
path,
|
||||||
timestamp,
|
timestamp,
|
||||||
{
|
{
|
||||||
notification: [timestamp, p[1] as number],
|
notification: [timestamp, notification],
|
||||||
accessList: [timestamp, p[2] as string[]],
|
accessList: [timestamp, accessList],
|
||||||
},
|
},
|
||||||
toClear
|
toClear
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requested.size) {
|
||||||
|
for (const p of requested) {
|
||||||
|
logger.accessWarn({
|
||||||
|
sessionContext: sessionContext,
|
||||||
|
message: "Missing parameter in response",
|
||||||
|
parameter: p,
|
||||||
|
});
|
||||||
|
toClear = device.set(
|
||||||
|
sessionContext.deviceData,
|
||||||
|
Path.parse(p),
|
||||||
|
timestamp,
|
||||||
|
{
|
||||||
|
notification: [timestamp, 0],
|
||||||
|
accessList: [timestamp, []],
|
||||||
|
},
|
||||||
|
toClear
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (rpcRes.name === "GetParameterNamesResponse") {
|
} else if (rpcRes.name === "GetParameterNamesResponse") {
|
||||||
if (rpcReq.name !== "GetParameterNames")
|
if (rpcReq.name !== "GetParameterNames")
|
||||||
throw new Error("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
let root: Path;
|
let root: Path;
|
||||||
if (rpcReq.parameterPath.endsWith("."))
|
if (rpcReq.parameterPath.endsWith("."))
|
||||||
@@ -2589,6 +2692,17 @@ export async function rpcResponse(
|
|||||||
const wildcardParams: Path[] = [root.concat(wildcardPath)];
|
const wildcardParams: Path[] = [root.concat(wildcardPath)];
|
||||||
|
|
||||||
for (const [path, object, writable] of rpcRes.parameterList) {
|
for (const [path, object, writable] of rpcRes.parameterList) {
|
||||||
|
if (
|
||||||
|
!path.toString().startsWith(rpcReq.parameterPath) &&
|
||||||
|
!(`${path.toString()}.` === rpcReq.parameterPath && !rpcReq.nextLevel)
|
||||||
|
) {
|
||||||
|
logger.accessWarn({
|
||||||
|
sessionContext: sessionContext,
|
||||||
|
message: "Unexpected parameter in response",
|
||||||
|
parameter: path.toString(),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (object && !rpcReq.nextLevel)
|
if (object && !rpcReq.nextLevel)
|
||||||
wildcardParams.push(path.concat(wildcardPath));
|
wildcardParams.push(path.concat(wildcardPath));
|
||||||
|
|
||||||
@@ -2615,7 +2729,7 @@ export async function rpcResponse(
|
|||||||
}
|
}
|
||||||
} else if (rpcRes.name === "SetParameterValuesResponse") {
|
} else if (rpcRes.name === "SetParameterValuesResponse") {
|
||||||
if (rpcReq.name !== "SetParameterValues")
|
if (rpcReq.name !== "SetParameterValues")
|
||||||
throw new Error("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
for (const p of rpcReq.parameterList) {
|
for (const p of rpcReq.parameterList) {
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
@@ -2634,7 +2748,7 @@ export async function rpcResponse(
|
|||||||
}
|
}
|
||||||
} else if (rpcRes.name === "SetParameterAttributesResponse") {
|
} else if (rpcRes.name === "SetParameterAttributesResponse") {
|
||||||
if (rpcReq.name !== "SetParameterAttributes")
|
if (rpcReq.name !== "SetParameterAttributes")
|
||||||
throw new Error("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
for (const p of rpcReq.parameterList) {
|
for (const p of rpcReq.parameterList) {
|
||||||
let attrs;
|
let attrs;
|
||||||
@@ -2664,7 +2778,7 @@ export async function rpcResponse(
|
|||||||
}
|
}
|
||||||
} else if (rpcRes.name === "AddObjectResponse") {
|
} else if (rpcRes.name === "AddObjectResponse") {
|
||||||
if (rpcReq.name !== "AddObject")
|
if (rpcReq.name !== "AddObject")
|
||||||
throw new Error("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
@@ -2675,7 +2789,7 @@ export async function rpcResponse(
|
|||||||
);
|
);
|
||||||
} else if (rpcRes.name === "DeleteObjectResponse") {
|
} else if (rpcRes.name === "DeleteObjectResponse") {
|
||||||
if (rpcReq.name !== "DeleteObject")
|
if (rpcReq.name !== "DeleteObject")
|
||||||
throw new Error("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
@@ -2686,7 +2800,7 @@ export async function rpcResponse(
|
|||||||
);
|
);
|
||||||
} else if (rpcRes.name === "RebootResponse") {
|
} else if (rpcRes.name === "RebootResponse") {
|
||||||
if (rpcReq.name !== "Reboot")
|
if (rpcReq.name !== "Reboot")
|
||||||
throw new Error("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
@@ -2697,7 +2811,7 @@ export async function rpcResponse(
|
|||||||
);
|
);
|
||||||
} else if (rpcRes.name === "FactoryResetResponse") {
|
} else if (rpcRes.name === "FactoryResetResponse") {
|
||||||
if (rpcReq.name !== "FactoryReset")
|
if (rpcReq.name !== "FactoryReset")
|
||||||
throw new Error("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
@@ -2708,7 +2822,7 @@ export async function rpcResponse(
|
|||||||
);
|
);
|
||||||
} else if (rpcRes.name === "DownloadResponse") {
|
} else if (rpcRes.name === "DownloadResponse") {
|
||||||
if (rpcReq.name !== "Download")
|
if (rpcReq.name !== "Download")
|
||||||
throw new Error("Response name does not match request name");
|
return invalidResponse("Response name does not match request name");
|
||||||
|
|
||||||
toClear = device.set(
|
toClear = device.set(
|
||||||
sessionContext.deviceData,
|
sessionContext.deviceData,
|
||||||
@@ -2794,13 +2908,15 @@ export async function rpcResponse(
|
|||||||
sessionContext.operationsTouched[rpcReq.commandKey] = 1;
|
sessionContext.operationsTouched[rpcReq.commandKey] = 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Response name not recognized");
|
return invalidResponse("Response name not recognized");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toClear) {
|
if (toClear) {
|
||||||
for (const c of toClear)
|
for (const c of toClear)
|
||||||
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
|
device.clear(sessionContext.deviceData, c[0], c[1], c[2], c[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rpcFault(
|
export async function rpcFault(
|
||||||
@@ -2833,13 +2949,9 @@ export async function rpcFault(
|
|||||||
(p) => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear
|
(p) => [Path.parse(p.replace(/\.$/, "")), timestamp] as Clear
|
||||||
);
|
);
|
||||||
} else if (rpcReq.name === "SetParameterValues") {
|
} else if (rpcReq.name === "SetParameterValues") {
|
||||||
toClear = (rpcReq.parameterList as [
|
toClear = (
|
||||||
string,
|
rpcReq.parameterList as [string, string | number | boolean, string][]
|
||||||
string | number | boolean,
|
).map((p) => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear);
|
||||||
string
|
|
||||||
][]).map(
|
|
||||||
(p) => [Path.parse(p[0].replace(/\.$/, "")), timestamp] as Clear
|
|
||||||
);
|
|
||||||
} else if (rpcReq.name === "AddObject") {
|
} else if (rpcReq.name === "AddObject") {
|
||||||
toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]];
|
toClear = [[Path.parse(rpcReq.objectName.replace(/\.$/, "")), timestamp]];
|
||||||
} else if (rpcReq.name === "DeleteObject") {
|
} else if (rpcReq.name === "DeleteObject") {
|
||||||
@@ -2876,10 +2988,10 @@ export async function deserialize(
|
|||||||
const sessionContext = JSON.parse(sessionContextString) as SessionContext;
|
const sessionContext = JSON.parse(sessionContextString) as SessionContext;
|
||||||
|
|
||||||
for (const decs of sessionContext.declarations)
|
for (const decs of sessionContext.declarations)
|
||||||
for (const d of decs) d.path = Path.parse((d.path as unknown) as string);
|
for (const d of decs) d.path = Path.parse(d.path as unknown as string);
|
||||||
|
|
||||||
const deviceData = initDeviceData();
|
const deviceData = initDeviceData();
|
||||||
for (const r of (sessionContext.deviceData as unknown) as any[]) {
|
for (const r of sessionContext.deviceData as unknown as any[]) {
|
||||||
const path = deviceData.paths.add(Path.parse(r[0]));
|
const path = deviceData.paths.add(Path.parse(r[0]));
|
||||||
|
|
||||||
if (r[1]) deviceData.trackers.set(path, r[1]);
|
if (r[1]) deviceData.trackers.set(path, r[1]);
|
||||||
|
|||||||
+53
-8
@@ -109,8 +109,8 @@ function event(xml: Element): string[] {
|
|||||||
|
|
||||||
function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
||||||
return xml.children
|
return xml.children
|
||||||
.filter((e) => e.localName === "ParameterInfoStruct")
|
|
||||||
.map<[Path, boolean, boolean]>((e) => {
|
.map<[Path, boolean, boolean]>((e) => {
|
||||||
|
if (e.localName !== "ParameterInfoStruct") return null;
|
||||||
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) {
|
||||||
@@ -134,10 +134,20 @@ function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
|||||||
parsed = false;
|
parsed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (param && !param.endsWith("."))
|
if (param && !param.endsWith("."))
|
||||||
return [Path.parse(param), false, parsed];
|
return [Path.parse(param), false, parsed];
|
||||||
else return [Path.parse(param.slice(0, -1)), true, 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) => {
|
||||||
@@ -151,8 +161,8 @@ function parameterValueList(
|
|||||||
xml: Element
|
xml: Element
|
||||||
): [Path, string | number | boolean, string][] {
|
): [Path, string | number | boolean, string][] {
|
||||||
return xml.children
|
return xml.children
|
||||||
.filter((e) => e.localName === "ParameterValueStruct")
|
|
||||||
.map<[Path, string | number | boolean, string]>((e) => {
|
.map<[Path, string | number | boolean, string]>((e) => {
|
||||||
|
if (e.localName !== "ParameterValueStruct") return null;
|
||||||
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) {
|
||||||
@@ -208,15 +218,24 @@ function parameterValueList(
|
|||||||
parsed = value;
|
parsed = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
return [Path.parse(param), parsed, valueType];
|
return [Path.parse(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): [Path, number, string[]][] {
|
||||||
return xml.children
|
return xml.children
|
||||||
.filter((e) => e.localName === "ParameterAttributeStruct")
|
|
||||||
.map<[Path, number, string[]]>((e) => {
|
.map<[Path, number, string[]]>((e) => {
|
||||||
|
if (e.localName !== "ParameterAttributeStruct") return null;
|
||||||
let notificationElement: Element,
|
let notificationElement: Element,
|
||||||
accessListElement: Element,
|
accessListElement: Element,
|
||||||
param: string;
|
param: string;
|
||||||
@@ -248,8 +267,18 @@ function parameterAttributeList(xml: Element): [Path, number, string[]][] {
|
|||||||
.filter((c) => c.localName === "string")
|
.filter((c) => c.localName === "string")
|
||||||
.map((c) => decodeEntities(c.text));
|
.map((c) => decodeEntities(c.text));
|
||||||
|
|
||||||
|
try {
|
||||||
return [Path.parse(param), notification, 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 {
|
||||||
@@ -701,6 +730,18 @@ 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: string,
|
||||||
faultString: string,
|
faultString: string,
|
||||||
@@ -798,18 +839,18 @@ function fault(xml: Element): CpeFault {
|
|||||||
|
|
||||||
export function request(
|
export function request(
|
||||||
body: string,
|
body: string,
|
||||||
cwmpVersion: string,
|
|
||||||
warn: Record<string, unknown>[]
|
warn: Record<string, unknown>[]
|
||||||
): SoapMessage {
|
): SoapMessage {
|
||||||
warnings = warn;
|
warnings = warn;
|
||||||
|
|
||||||
const rpc = {
|
const rpc = {
|
||||||
id: null,
|
id: null,
|
||||||
cwmpVersion: cwmpVersion,
|
cwmpVersion: null,
|
||||||
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;
|
||||||
@@ -848,7 +889,7 @@ export function request(
|
|||||||
|
|
||||||
const methodElement = bodyElement.children[0];
|
const methodElement = bodyElement.children[0];
|
||||||
|
|
||||||
if (!rpc.cwmpVersion && methodElement.localName !== "Fault") {
|
if (methodElement.localName === "Inform") {
|
||||||
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;
|
||||||
@@ -931,7 +972,8 @@ export function request(
|
|||||||
rpc.cpeFault = fault(methodElement);
|
rpc.cpeFault = fault(methodElement);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`8000 Method not supported ${methodElement.localName}`);
|
rpc.unknownMethod = methodElement.localName;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpc;
|
return rpc;
|
||||||
@@ -959,6 +1001,7 @@ export function response(rpc: {
|
|||||||
id: string;
|
id: string;
|
||||||
acsRequest?: AcsRequest;
|
acsRequest?: AcsRequest;
|
||||||
acsResponse?: AcsResponse;
|
acsResponse?: AcsResponse;
|
||||||
|
acsFault?: CpeFault;
|
||||||
cwmpVersion?: string;
|
cwmpVersion?: string;
|
||||||
}): { code: number; headers: Record<string, string>; data: string } {
|
}): { code: number; headers: Record<string, string>; data: string } {
|
||||||
const headers = {
|
const headers = {
|
||||||
@@ -1027,6 +1070,8 @@ export function response(rpc: {
|
|||||||
`Unknown method request ${(rpc.acsRequest as 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"';
|
||||||
|
|||||||
+3
-2
@@ -180,7 +180,7 @@ export interface Task {
|
|||||||
fileName?: string;
|
fileName?: string;
|
||||||
targetFileName?: string;
|
targetFileName?: string;
|
||||||
expiry?: number;
|
expiry?: number;
|
||||||
provisions?: (string | number | boolean)[][];
|
provisions?: [string, ...Expression[]][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Operation {
|
export interface Operation {
|
||||||
@@ -477,7 +477,7 @@ export interface VirtualParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Files {
|
export interface Files {
|
||||||
[name: string]: { length: number; md5: string; contentType: string };
|
[name: string]: { length: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Users {
|
export interface Users {
|
||||||
@@ -526,6 +526,7 @@ export interface SoapMessage {
|
|||||||
cpeRequest?: CpeRequest;
|
cpeRequest?: CpeRequest;
|
||||||
cpeFault?: CpeFault;
|
cpeFault?: CpeFault;
|
||||||
cpeResponse?: CpeResponse;
|
cpeResponse?: CpeResponse;
|
||||||
|
unknownMethod?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptResult {
|
export interface ScriptResult {
|
||||||
|
|||||||
@@ -59,8 +59,10 @@ const getAuthorizer = memoize(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
koa.on("error", async (err) => {
|
koa.on("error", async (err, ctx) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
// Ignored errors resulting from aborted requests
|
||||||
|
if (ctx?.req.aborted) return;
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -93,7 +95,7 @@ koa.use(
|
|||||||
koa.use(async (ctx, next) => {
|
koa.use(async (ctx, next) => {
|
||||||
let roles: string[] = [];
|
let roles: string[] = [];
|
||||||
|
|
||||||
if (ctx.state.user && ctx.state.user.username) {
|
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)[
|
||||||
@@ -219,13 +221,14 @@ router.post("/init", async (ctx) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/", async (ctx) => {
|
router.get("/", async (ctx) => {
|
||||||
const permissionSets: PermissionSet[] = ctx.state.authorizer.getPermissionSets();
|
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 = `
|
ctx.body = `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>GenieACS</title>
|
<title>GenieACS</title>
|
||||||
@@ -245,7 +248,7 @@ router.get("/", async (ctx) => {
|
|||||||
)};
|
)};
|
||||||
window.permissionSets = ${JSON.stringify(permissionSets)};
|
window.permissionSets = ${JSON.stringify(permissionSets)};
|
||||||
</script>
|
</script>
|
||||||
<script src="app.js"></script>${wizard}
|
<script type="module" src="app.js"></script>${wizard}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
@@ -259,6 +262,12 @@ koa.use(
|
|||||||
deflate: {
|
deflate: {
|
||||||
flush: constants.Z_SYNC_FLUSH,
|
flush: constants.Z_SYNC_FLUSH,
|
||||||
},
|
},
|
||||||
|
br: {
|
||||||
|
flush: constants.BROTLI_OPERATION_FLUSH,
|
||||||
|
params: {
|
||||||
|
[constants.BROTLI_PARAM_QUALITY]: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ObjectID } from "mongodb";
|
import { ObjectId } from "mongodb";
|
||||||
import * as db from "./db";
|
import * as db from "./db";
|
||||||
import { del } from "../cache";
|
import { del } from "../cache";
|
||||||
import { getUsers } from "../local-cache";
|
import { getUsers } from "../local-cache";
|
||||||
@@ -38,7 +38,7 @@ async function deleteFault(id): Promise<void> {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.deleteFault(id),
|
db.deleteFault(id),
|
||||||
channel.startsWith("task_")
|
channel.startsWith("task_")
|
||||||
? db.deleteTask(new ObjectID(channel.slice(5)))
|
? db.deleteTask(new ObjectId(channel.slice(5)))
|
||||||
: null,
|
: null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ export async function postTasks(
|
|||||||
const lastInform = Date.now();
|
const lastInform = Date.now();
|
||||||
const notInSession = await awaitSessionEnd(deviceId, 30000);
|
const notInSession = await awaitSessionEnd(deviceId, 30000);
|
||||||
if (!notInSession) {
|
if (!notInSession) {
|
||||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||||
return {
|
return {
|
||||||
connectionRequest: "Session took too long to complete",
|
connectionRequest: "Session took too long to complete",
|
||||||
tasks: statuses,
|
tasks: statuses,
|
||||||
@@ -120,7 +120,7 @@ export async function postTasks(
|
|||||||
const status = await connectionRequest(deviceId, device);
|
const status = await connectionRequest(deviceId, device);
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||||
return {
|
return {
|
||||||
connectionRequest: status,
|
connectionRequest: status,
|
||||||
tasks: statuses,
|
tasks: statuses,
|
||||||
@@ -129,7 +129,7 @@ export async function postTasks(
|
|||||||
|
|
||||||
const sessionStarted = await awaitSessionStart(deviceId, lastInform, timeout);
|
const sessionStarted = await awaitSessionStart(deviceId, lastInform, timeout);
|
||||||
if (!sessionStarted) {
|
if (!sessionStarted) {
|
||||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||||
return {
|
return {
|
||||||
connectionRequest: "No contact from CPE",
|
connectionRequest: "No contact from CPE",
|
||||||
tasks: statuses,
|
tasks: statuses,
|
||||||
@@ -138,7 +138,7 @@ export async function postTasks(
|
|||||||
|
|
||||||
const sessionEnded = await awaitSessionEnd(deviceId, 120000);
|
const sessionEnded = await awaitSessionEnd(deviceId, 120000);
|
||||||
if (!sessionEnded) {
|
if (!sessionEnded) {
|
||||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||||
return {
|
return {
|
||||||
connectionRequest: "Session took too long to complete",
|
connectionRequest: "Session took too long to complete",
|
||||||
tasks: statuses,
|
tasks: statuses,
|
||||||
@@ -163,7 +163,7 @@ export async function postTasks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id))));
|
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||||
|
|
||||||
return { connectionRequest: "OK", tasks: statuses };
|
return { connectionRequest: "OK", tasks: statuses };
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ export function authLocal(
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const users = getUsers(snapshot);
|
const users = getUsers(snapshot);
|
||||||
const user = users[username];
|
const user = users[username];
|
||||||
if (!user || !user.password) return void resolve(null);
|
if (!user?.password) return void resolve(null);
|
||||||
hashPassword(password, user.salt)
|
hashPassword(password, user.salt)
|
||||||
.then((hash) => {
|
.then((hash) => {
|
||||||
if (hash === user.password) resolve(true);
|
if (hash === user.password) resolve(true);
|
||||||
|
|||||||
+99
-34
@@ -17,7 +17,7 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as stream from "stream";
|
import { PassThrough } from "stream";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import * as db from "./db";
|
import * as db from "./db";
|
||||||
import * as apiFunctions from "./api-functions";
|
import * as apiFunctions from "./api-functions";
|
||||||
@@ -30,8 +30,8 @@ import { generateSalt, hashPassword } from "../auth";
|
|||||||
import { del } from "../cache";
|
import { del } from "../cache";
|
||||||
import Authorizer from "../common/authorizer";
|
import Authorizer from "../common/authorizer";
|
||||||
import { ping } from "../ping";
|
import { ping } from "../ping";
|
||||||
import * as url from "url";
|
|
||||||
import { decodeTag } from "../common";
|
import { decodeTag } from "../common";
|
||||||
|
import { stringify as yamlStringify } from "../common/yaml";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
export default router;
|
export default router;
|
||||||
@@ -70,6 +70,10 @@ const resources = {
|
|||||||
tasks: 0,
|
tasks: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function singleParam(p: string | string[]): string {
|
||||||
|
return Array.isArray(p) ? p[p.length - 1] : p;
|
||||||
|
}
|
||||||
|
|
||||||
router.get(`/devices/:id.csv`, async (ctx) => {
|
router.get(`/devices/:id.csv`, async (ctx) => {
|
||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const log = {
|
const log = {
|
||||||
@@ -99,26 +103,35 @@ router.get(`/devices/:id.csv`, async (ctx) => {
|
|||||||
.replace(/[:.]/g, "")}.csv`
|
.replace(/[:.]/g, "")}.csv`
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.body = new stream.PassThrough();
|
ctx.body = new PassThrough();
|
||||||
ctx.body.write(
|
ctx.body.write(
|
||||||
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n"
|
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const k of Object.keys(res[0]).sort()) {
|
for (const k of Object.keys(res[0]).sort()) {
|
||||||
const p = res[0][k];
|
const p = res[0][k];
|
||||||
|
let value = "";
|
||||||
|
let type = "";
|
||||||
|
if (p.value) {
|
||||||
|
value = p.value[0];
|
||||||
|
type = p.value[1];
|
||||||
|
if (type === "xsd:dateTime" && typeof value === "number")
|
||||||
|
value = new Date(value).toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
const row = [
|
const row = [
|
||||||
k,
|
k,
|
||||||
p.object,
|
p.object,
|
||||||
p.objectTimestamp,
|
new Date(p.objectTimestamp).toJSON(),
|
||||||
p.writable,
|
p.writable,
|
||||||
p.writableTimestamp,
|
new Date(p.writableTimestamp).toJSON(),
|
||||||
p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "",
|
`"${String(value).replace(/"/g, '""')}"`,
|
||||||
p.value != null ? p.value[1] : "",
|
type,
|
||||||
p.valueTimestamp,
|
new Date(p.valueTimestamp).toJSON(),
|
||||||
p.notification,
|
p.notification,
|
||||||
p.notificationTimestamp,
|
new Date(p.notificationTimestamp).toJSON(),
|
||||||
p.accessList ? p.accessList.join(", ") : "",
|
p.accessList ? p.accessList.join(", ") : "",
|
||||||
p.accessListTimestamp,
|
new Date(p.accessListTimestamp).toJSON(),
|
||||||
];
|
];
|
||||||
ctx.body.write(row.map((r) => (r != null ? r : "")).join(",") + "\n");
|
ctx.body.write(row.map((r) => (r != null ? r : "")).join(",") + "\n");
|
||||||
}
|
}
|
||||||
@@ -131,7 +144,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
let filter: Expression = authorizer.getFilter(resource, 1);
|
let filter: Expression = authorizer.getFilter(resource, 1);
|
||||||
if (ctx.request.query.filter)
|
if (ctx.request.query.filter)
|
||||||
filter = and(filter, parse(ctx.request.query.filter));
|
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: `Count ${resource}`,
|
message: `Count ${resource}`,
|
||||||
@@ -168,13 +181,13 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
const options: QueryOptions = {};
|
const options: QueryOptions = {};
|
||||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||||
if (ctx.request.query.filter)
|
if (ctx.request.query.filter)
|
||||||
filter = and(filter, parse(ctx.request.query.filter));
|
filter = and(filter, parse(singleParam(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(ctx.request.query.sort);
|
options.sort = JSON.parse(singleParam(ctx.request.query.sort));
|
||||||
if (ctx.request.query.projection) {
|
if (ctx.request.query.projection) {
|
||||||
options.projection = ctx.request.query.projection
|
options.projection = singleParam(ctx.request.query.projection)
|
||||||
.split(",")
|
.split(",")
|
||||||
.reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {});
|
.reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {});
|
||||||
}
|
}
|
||||||
@@ -203,17 +216,21 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = new stream.PassThrough();
|
ctx.body = new PassThrough();
|
||||||
ctx.type = "application/json";
|
ctx.type = "application/json";
|
||||||
|
|
||||||
let c = 0;
|
let c = 0;
|
||||||
ctx.body.write("[\n");
|
ctx.body.write("[\n");
|
||||||
await db.query(resource, filter, options, (obj) => {
|
db.query(resource, filter, options, (obj) => {
|
||||||
ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n");
|
ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n");
|
||||||
});
|
})
|
||||||
|
.then(() => {
|
||||||
ctx.body.end("]");
|
ctx.body.end("]");
|
||||||
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
ctx.body.emit("error", err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// CSV download
|
// CSV download
|
||||||
@@ -222,11 +239,11 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
const options: QueryOptions = { projection: {} };
|
const options: QueryOptions = { projection: {} };
|
||||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||||
if (ctx.request.query.filter)
|
if (ctx.request.query.filter)
|
||||||
filter = and(filter, parse(ctx.request.query.filter));
|
filter = and(filter, parse(singleParam(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(ctx.request.query.sort);
|
options.sort = JSON.parse(singleParam(ctx.request.query.sort));
|
||||||
|
|
||||||
const log = {
|
const log = {
|
||||||
message: `Query ${resource} (CSV)`,
|
message: `Query ${resource} (CSV)`,
|
||||||
@@ -243,7 +260,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns: Record<string, Expression> = JSON.parse(
|
const columns: Record<string, Expression> = JSON.parse(
|
||||||
ctx.request.query.columns
|
singleParam(ctx.request.query.columns)
|
||||||
);
|
);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
@@ -263,7 +280,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = new stream.PassThrough();
|
ctx.body = new 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`
|
||||||
@@ -272,7 +289,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
ctx.body.write(
|
ctx.body.write(
|
||||||
Object.keys(columns).map((k) => `"${k.replace(/"/, '""')}"`) + "\n"
|
Object.keys(columns).map((k) => `"${k.replace(/"/, '""')}"`) + "\n"
|
||||||
);
|
);
|
||||||
await db.query(resource, filter, options, (obj) => {
|
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)) {
|
||||||
@@ -285,11 +302,23 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
|
|
||||||
return tags.join(", ");
|
return tags.join(", ");
|
||||||
}
|
}
|
||||||
|
if (e === exp) {
|
||||||
|
const p = obj[e[1]];
|
||||||
|
if (
|
||||||
|
p &&
|
||||||
|
p.value &&
|
||||||
|
p.value[1] === "xsd:dateTime" &&
|
||||||
|
typeof p.value[0] === "number"
|
||||||
|
)
|
||||||
|
return new Date(p.value[0]).toJSON();
|
||||||
|
}
|
||||||
|
} else if (resource === "faults") {
|
||||||
|
if (e[1] === "detail") return yamlStringify(obj["detail"]);
|
||||||
}
|
}
|
||||||
} else if (e[0] === "FUNC") {
|
} else if (e[0] === "FUNC") {
|
||||||
if (e[1] === "DATE_STRING") {
|
if (e[1] === "DATE_STRING") {
|
||||||
if (e[2] && !Array.isArray(e[2]))
|
if (e[2] && !Array.isArray(e[2]))
|
||||||
return new Date(e[2]).toISOString();
|
return new Date(e[2]).toJSON();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,10 +331,14 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
return v;
|
return v;
|
||||||
});
|
});
|
||||||
ctx.body.write(arr.join(",") + "\n");
|
ctx.body.write(arr.join(",") + "\n");
|
||||||
});
|
})
|
||||||
|
.then(() => {
|
||||||
ctx.body.end();
|
ctx.body.end();
|
||||||
|
|
||||||
logger.accessInfo(log);
|
logger.accessInfo(log);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
ctx.body.emit("error", err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.head(`/${resource}/:id`, async (ctx) => {
|
router.head(`/${resource}/:id`, async (ctx) => {
|
||||||
@@ -435,6 +468,36 @@ for (const [resource, flags] of Object.entries(resources)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.get("/blob/files/:id", async (ctx) => {
|
||||||
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
|
const resource = "files";
|
||||||
|
const id = ctx.params.id;
|
||||||
|
|
||||||
|
const log = {
|
||||||
|
message: `Download ${resource}`,
|
||||||
|
context: ctx,
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const filter = and(authorizer.getFilter(resource, 2), [
|
||||||
|
"=",
|
||||||
|
["PARAM", RESOURCE_IDS[resource]],
|
||||||
|
ctx.params.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!authorizer.hasAccess(resource, 2)) {
|
||||||
|
logUnauthorizedWarning(log);
|
||||||
|
return void (ctx.status = 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = await db.count(resource, filter);
|
||||||
|
if (!count) return void (ctx.status = 404);
|
||||||
|
|
||||||
|
logger.accessInfo(log);
|
||||||
|
ctx.body = db.downloadFile(id);
|
||||||
|
ctx.attachment(id);
|
||||||
|
});
|
||||||
|
|
||||||
router.put("/files/:id", async (ctx) => {
|
router.put("/files/:id", async (ctx) => {
|
||||||
const authorizer: Authorizer = ctx.state.authorizer;
|
const authorizer: Authorizer = ctx.state.authorizer;
|
||||||
const resource = "files";
|
const resource = "files";
|
||||||
@@ -453,10 +516,11 @@ router.put("/files/:id", async (ctx) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
fileType: ctx.request.headers["metadata.filetype"] || "",
|
fileType: singleParam(ctx.request.headers["metadata-filetype"]) || "",
|
||||||
oui: ctx.request.headers["metadata.oui"] || "",
|
oui: singleParam(ctx.request.headers["metadata-oui"]) || "",
|
||||||
productClass: ctx.request.headers["metadata.productclass"] || "",
|
productClass:
|
||||||
version: ctx.request.headers["metadata.version"] || "",
|
singleParam(ctx.request.headers["metadata-productclass"]) || "",
|
||||||
|
version: singleParam(ctx.request.headers["metadata-version"]) || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = authorizer.getValidator(resource, metadata);
|
const validate = authorizer.getValidator(resource, metadata);
|
||||||
@@ -517,12 +581,12 @@ router.post("/devices/:id/tasks", async (ctx) => {
|
|||||||
if (!Array.isArray(exp)) return exp;
|
if (!Array.isArray(exp)) return exp;
|
||||||
if (exp[0] === "PARAM") {
|
if (exp[0] === "PARAM") {
|
||||||
const p = device[exp[1]];
|
const p = device[exp[1]];
|
||||||
if (p && p.value) return p.value[0];
|
if (p?.value) return p.value[0];
|
||||||
} else if (exp[0] === "FUNC") {
|
} else if (exp[0] === "FUNC") {
|
||||||
if (exp[1] === "REMOTE_ADDRESS") {
|
if (exp[1] === "REMOTE_ADDRESS") {
|
||||||
for (const root of ["InternetGatewayDevice", "Device"]) {
|
for (const root of ["InternetGatewayDevice", "Device"]) {
|
||||||
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
|
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
|
||||||
if (p && p.value) return url.parse(p.value[0]).host;
|
if (p?.value) return new URL(p.value[0]).hostname;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -596,13 +660,14 @@ router.post("/devices/:id/tags", async (ctx) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/ping/:host", async (ctx) => {
|
router.get("/ping/:host", async (ctx) => {
|
||||||
return new Promise((resolve) => {
|
if (!ctx.state.user) return void (ctx.status = 401);
|
||||||
|
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.name}: ${err.message}`;
|
ctx.body = err ? `${err.name}: ${err.message}` : "Unknown error";
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|||||||
+69
-36
@@ -17,13 +17,13 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Db, GridFSBucket, ObjectID } from "mongodb";
|
import { Db, GridFSBucket, ObjectId } from "mongodb";
|
||||||
import { Script } from "vm";
|
import { Script } from "vm";
|
||||||
import { onConnect } from "../db";
|
import { onConnect, optimizeProjection } from "../db";
|
||||||
import * as mongodbFunctions from "../mongodb-functions";
|
import * as mongodbFunctions from "../mongodb-functions";
|
||||||
import * as expression from "../common/expression";
|
import * as expression from "../common/expression";
|
||||||
import { QueryOptions, Expression } from "../types";
|
import { QueryOptions, Expression } from "../types";
|
||||||
import { Readable } from "stream";
|
import { Readable, Writable } from "stream";
|
||||||
import { minimize } from "../common/boolean-expression";
|
import { minimize } from "../common/boolean-expression";
|
||||||
|
|
||||||
const RESOURCE_COLLECTION = {
|
const RESOURCE_COLLECTION = {
|
||||||
@@ -36,12 +36,23 @@ onConnect(async (_db) => {
|
|||||||
db = _db;
|
db = _db;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function query(
|
||||||
|
resource: string,
|
||||||
|
filter: Expression,
|
||||||
|
options?: QueryOptions
|
||||||
|
): Promise<any[]>;
|
||||||
|
export function query(
|
||||||
|
resource: string,
|
||||||
|
filter: Expression,
|
||||||
|
options: QueryOptions,
|
||||||
|
callback: (doc: any) => void
|
||||||
|
): Promise<void>;
|
||||||
export function query(
|
export function query(
|
||||||
resource: string,
|
resource: string,
|
||||||
filter: Expression,
|
filter: Expression,
|
||||||
options?: QueryOptions,
|
options?: QueryOptions,
|
||||||
callback?: (doc: any) => void
|
callback?: (doc: any) => void
|
||||||
): Promise<any[]> {
|
): Promise<void | any[]> {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
let q;
|
let q;
|
||||||
filter = expression.evaluate(filter, null, Date.now());
|
filter = expression.evaluate(filter, null, Date.now());
|
||||||
@@ -78,6 +89,7 @@ export function query(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resource === "presets") projection.configurations = 1;
|
if (resource === "presets") projection.configurations = 1;
|
||||||
|
projection = optimizeProjection(projection);
|
||||||
cursor.project(projection);
|
cursor.project(projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,30 +115,31 @@ export function query(
|
|||||||
cursor.toArray((err, docs) => {
|
cursor.toArray((err, docs) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
if (resource === "devices")
|
if (resource === "devices")
|
||||||
docs = docs.map((d) => mongodbFunctions.flattenDevice(d));
|
return resolve(docs.map((d) => mongodbFunctions.flattenDevice(d)));
|
||||||
else if (resource === "faults")
|
else if (resource === "faults")
|
||||||
docs = docs.map((d) => mongodbFunctions.flattenFault(d));
|
return resolve(docs.map((d) => mongodbFunctions.flattenFault(d)));
|
||||||
else if (resource === "tasks")
|
else if (resource === "tasks")
|
||||||
docs = docs.map((d) => mongodbFunctions.flattenTask(d));
|
return resolve(docs.map((d) => mongodbFunctions.flattenTask(d)));
|
||||||
else if (resource === "presets")
|
else if (resource === "presets")
|
||||||
docs = docs.map((d) => mongodbFunctions.flattenPreset(d));
|
return resolve(docs.map((d) => mongodbFunctions.flattenPreset(d)));
|
||||||
else if (resource === "files")
|
else if (resource === "files")
|
||||||
docs = docs.map((d) => mongodbFunctions.flattenFile(d));
|
return resolve(docs.map((d) => mongodbFunctions.flattenFile(d)));
|
||||||
return resolve(docs);
|
return resolve(docs);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cursor.forEach(
|
cursor.forEach(
|
||||||
(doc) => {
|
(doc) => {
|
||||||
if (resource === "devices") doc = mongodbFunctions.flattenDevice(doc);
|
if (resource === "devices")
|
||||||
|
callback(mongodbFunctions.flattenDevice(doc));
|
||||||
else if (resource === "faults")
|
else if (resource === "faults")
|
||||||
doc = mongodbFunctions.flattenFault(doc);
|
callback(mongodbFunctions.flattenFault(doc));
|
||||||
else if (resource === "tasks")
|
else if (resource === "tasks")
|
||||||
doc = mongodbFunctions.flattenTask(doc);
|
callback(mongodbFunctions.flattenTask(doc));
|
||||||
else if (resource === "presets")
|
else if (resource === "presets")
|
||||||
doc = mongodbFunctions.flattenPreset(doc);
|
callback(mongodbFunctions.flattenPreset(doc));
|
||||||
else if (resource === "files")
|
else if (resource === "files")
|
||||||
doc = mongodbFunctions.flattenFile(doc);
|
callback(mongodbFunctions.flattenFile(doc));
|
||||||
callback(doc);
|
else callback(doc);
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
@@ -138,7 +151,8 @@ export function query(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function count(resource: string, filter: Expression): Promise<number> {
|
export function count(resource: string, filter: Expression): Promise<number> {
|
||||||
let q;
|
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||||
|
let q: Parameters<typeof collection.countDocuments>[0];
|
||||||
filter = expression.evaluate(filter, null, Date.now());
|
filter = expression.evaluate(filter, null, Date.now());
|
||||||
filter = minimize(filter, true);
|
filter = minimize(filter, true);
|
||||||
|
|
||||||
@@ -154,13 +168,7 @@ export function count(resource: string, filter: Expression): Promise<number> {
|
|||||||
return Promise.resolve(0);
|
return Promise.resolve(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return collection.countDocuments(q);
|
||||||
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
|
|
||||||
collection.find(q).count((err, c) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve(c);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateDeviceTags(
|
export async function updateDeviceTags(
|
||||||
@@ -179,8 +187,8 @@ export async function updateDeviceTags(
|
|||||||
const collection = db.collection("devices");
|
const collection = db.collection("devices");
|
||||||
const object = {};
|
const object = {};
|
||||||
|
|
||||||
if (add && add.length) object["$addToSet"] = { _tags: { $each: add } };
|
if (add?.length) object["$addToSet"] = { _tags: { $each: add } };
|
||||||
if (pull && pull.length) object["$pullAll"] = { _tags: pull };
|
if (pull?.length) object["$pullAll"] = { _tags: pull };
|
||||||
|
|
||||||
await collection.updateOne({ _id: deviceId }, object);
|
await collection.updateOne({ _id: deviceId }, object);
|
||||||
}
|
}
|
||||||
@@ -197,7 +205,7 @@ function putResource(resource, id, object): Promise<void> {
|
|||||||
|
|
||||||
function deleteResource(
|
function deleteResource(
|
||||||
resource: string,
|
resource: string,
|
||||||
id: string | ObjectID
|
id: string | ObjectId
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
|
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||||
@@ -226,9 +234,17 @@ export function putProvision(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!object.script) object.script = "";
|
if (!object.script) object.script = "";
|
||||||
try {
|
try {
|
||||||
new Script(`"use strict";(function(){\n${object.script}\n})();`);
|
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
|
||||||
} catch (error) {
|
filename: id,
|
||||||
return Promise.reject(error);
|
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);
|
||||||
}
|
}
|
||||||
return putResource("provisions", id, object);
|
return putResource("provisions", id, object);
|
||||||
}
|
}
|
||||||
@@ -243,9 +259,17 @@ export function putVirtualParameter(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!object.script) object.script = "";
|
if (!object.script) object.script = "";
|
||||||
try {
|
try {
|
||||||
new Script(`"use strict";(function(){\n${object.script}\n})();`);
|
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
|
||||||
} catch (error) {
|
filename: id,
|
||||||
return Promise.reject(error);
|
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);
|
||||||
}
|
}
|
||||||
return putResource("virtualParameters", id, object);
|
return putResource("virtualParameters", id, object);
|
||||||
}
|
}
|
||||||
@@ -299,6 +323,11 @@ export function deleteUser(id: string): Promise<void> {
|
|||||||
return deleteResource("users", id);
|
return deleteResource("users", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function downloadFile(filename: string): Readable {
|
||||||
|
const bucket = new GridFSBucket(db);
|
||||||
|
return bucket.openDownloadStreamByName(filename);
|
||||||
|
}
|
||||||
|
|
||||||
export function putFile(
|
export function putFile(
|
||||||
filename: string,
|
filename: string,
|
||||||
metadata: Record<string, string>,
|
metadata: Record<string, string>,
|
||||||
@@ -306,13 +335,17 @@ export function putFile(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const bucket = new GridFSBucket(db);
|
const bucket = new GridFSBucket(db);
|
||||||
const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
|
const uploadStream = bucket.openUploadStreamWithId(
|
||||||
|
filename as unknown as ObjectId,
|
||||||
|
filename,
|
||||||
|
{
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
uploadStream.on("error", reject);
|
uploadStream.on("error", reject);
|
||||||
contentStream.on("error", reject);
|
contentStream.on("error", reject);
|
||||||
uploadStream.on("finish", resolve);
|
uploadStream.on("finish", resolve);
|
||||||
contentStream.pipe(uploadStream);
|
contentStream.pipe(uploadStream as Writable);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +363,6 @@ export function deleteFault(id: string): Promise<void> {
|
|||||||
return deleteResource("faults", id);
|
return deleteResource("faults", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteTask(id: ObjectID): Promise<void> {
|
export function deleteTask(id: ObjectId): Promise<void> {
|
||||||
return deleteResource("tasks", id);
|
return deleteResource("tasks", id);
|
||||||
}
|
}
|
||||||
@@ -26,10 +26,10 @@ interface Revisions<V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class VersionedMap<K, V> {
|
export default class VersionedMap<K, V> {
|
||||||
private _sizeDiff: number[];
|
private declare _sizeDiff: number[];
|
||||||
private _revision: number;
|
private declare _revision: number;
|
||||||
private map: Map<K, (V | symbol)[]>;
|
private declare map: Map<K, (V | symbol)[]>;
|
||||||
public dirty: number;
|
public declare dirty: number;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this._sizeDiff = [0];
|
this._sizeDiff = [0];
|
||||||
@@ -168,6 +168,7 @@ 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;
|
||||||
|
|||||||
Generated
+10307
-4750
File diff suppressed because it is too large.
Load diff
+48
-51
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "genieacs",
|
"name": "genieacs",
|
||||||
"version": "1.2.3",
|
"version": "1.2.9",
|
||||||
"description": "A TR-069 Auto Configuration Server (ACS)",
|
"description": "A TR-069 Auto Configuration Server (ACS)",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -14,73 +14,70 @@
|
|||||||
],
|
],
|
||||||
"author": "Zaid Abdulla <zaid@genieacs.com>",
|
"author": "Zaid Abdulla <zaid@genieacs.com>",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"preferGlobal": true,
|
"private": true,
|
||||||
"directories": {
|
"directories": {
|
||||||
"bin": "bin",
|
"bin": "bin"
|
||||||
"tools": "tools"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"espresso-iisojs": "^1.0.3",
|
"@breejs/later": "^4.1.0",
|
||||||
"iconv-lite": "^0.6.2",
|
"espresso-iisojs": "^1.0.4",
|
||||||
"ipaddr.js": "^2.0.0",
|
"iconv-lite": "^0.6.3",
|
||||||
|
"ipaddr.js": "^2.0.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"koa": "^2.13.0",
|
"koa": "^2.13.4",
|
||||||
"koa-bodyparser": "^4.3.0",
|
"koa-bodyparser": "^4.3.0",
|
||||||
"koa-compress": "^5.0.1",
|
"koa-compress": "^5.1.0",
|
||||||
"koa-jwt": "^4.0.0",
|
"koa-jwt": "^4.0.3",
|
||||||
"koa-router": "^9.4.0",
|
"koa-router": "^12.0.0",
|
||||||
"koa-static": "^5.0.0",
|
"koa-static": "^5.0.0",
|
||||||
"later": "^1.2.0",
|
"mongodb": "^4.9.0",
|
||||||
"mongodb": "^3.6.2",
|
"parsimmon": "^1.18.1",
|
||||||
"parsimmon": "^1.16.0",
|
"seedrandom": "^3.0.5"
|
||||||
"seedrandom": "^3.0.5",
|
|
||||||
"yaml": "^1.10.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^16.0.0",
|
"@rollup/plugin-commonjs": "^22.0.2",
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
"@rollup/plugin-node-resolve": "^10.0.0",
|
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||||
"@rollup/plugin-typescript": "^6.0.0",
|
"@rollup/plugin-typescript": "^8.3.4",
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/codemirror": "^5.60.5",
|
||||||
"@types/koa": "^2.11.6",
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
"@types/koa-compress": "^4.0.1",
|
"@types/koa": "^2.13.5",
|
||||||
"@types/koa-router": "^7.4.1",
|
"@types/koa-compress": "^4.0.3",
|
||||||
"@types/koa-static": "^4.0.1",
|
"@types/koa-router": "^7.4.4",
|
||||||
"@types/mithril": "^2.0.5",
|
"@types/koa-static": "^4.0.2",
|
||||||
"@types/mongodb": "^3.5.31",
|
"@types/mithril": "^2.0.11",
|
||||||
"@types/node": "^14.14.5",
|
"@types/node": "^18.7.9",
|
||||||
"@types/parsimmon": "^1.10.3",
|
"@types/parsimmon": "^1.10.6",
|
||||||
"@types/seedrandom": "^2.4.28",
|
"@types/seedrandom": "^3.0.2",
|
||||||
"@types/svgo": "^1.3.3",
|
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
"@typescript-eslint/parser": "^5.33.1",
|
||||||
"@typescript-eslint/parser": "^4.6.0",
|
"ava": "^3.15.0",
|
||||||
"ava": "^3.13.0",
|
"codemirror": "^5.65.8",
|
||||||
"codemirror": "^5.58.2",
|
"cssnano": "^5.1.13",
|
||||||
"cssnano": "^4.1.10",
|
"eslint": "^8.22.0",
|
||||||
"eslint": "^7.12.1",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-config-prettier": "^6.14.0",
|
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"jsbi": "^3.1.4",
|
"mithril": "^2.2.2",
|
||||||
"mithril": "^2.0.4",
|
"postcss": "^8.4.16",
|
||||||
"postcss": "^8.1.4",
|
"postcss-color-mod-function": "^3.0.3",
|
||||||
"postcss-import": "^13.0.0",
|
"postcss-import": "^14.1.0",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^7.8.0",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.7.1",
|
||||||
"rollup": "^2.32.1",
|
"rollup": "^2.78.1",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"source-map-loader": "^1.1.1",
|
"sql.js": "^1.7.0",
|
||||||
"svgo": "^1.3.2",
|
"svgo": "^2.8.0",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.0.5",
|
"typescript": "^4.7.4",
|
||||||
"webpack": "^5.2.0"
|
"yaml": "^1.10.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.13.0"
|
"node": ">=12.13.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "ava --verbose --color",
|
"test": "ava --verbose --color",
|
||||||
"lint-eslint": "eslint --fix \"bin/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/*.ts\"",
|
"lint-eslint": "eslint --fix \"bin/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\"",
|
||||||
"lint-prettier": "prettier --parser typescript --write \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"",
|
"lint-prettier": "prettier --prose-wrap always --write .",
|
||||||
"lint-tsc": "tsc --noEmit",
|
"lint-tsc": "tsc --noEmit",
|
||||||
"lint": "npm run lint-eslint && npm run lint-prettier && npm run lint-tsc",
|
"lint": "npm run lint-eslint && npm run lint-prettier && npm run lint-tsc",
|
||||||
"build": "node -r esm -r ts-node/register/transpile-only build/build"
|
"build": "node -r esm -r ts-node/register/transpile-only build/build"
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import ava, { TestInterface } from "ava";
|
||||||
|
import { minimize, unionDiff } from "../lib/common/boolean-expression";
|
||||||
|
import { parse, stringify } from "../lib/common/expression-parser";
|
||||||
|
import initSqlJs from "sql.js/dist/sql-asm.js";
|
||||||
|
|
||||||
|
const test = ava as TestInterface<{
|
||||||
|
query: (filter: string) => Promise<Set<number>>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const STRING_VALUES = [null, "", "a", "ab", "ab10", "ab-10"];
|
||||||
|
const DECIMAL_VALUES = [null, 0, -10, 10];
|
||||||
|
|
||||||
|
test.before(async (t) => {
|
||||||
|
const sql = await initSqlJs();
|
||||||
|
const db = new sql.Database();
|
||||||
|
|
||||||
|
t.context.query = async (filter: string): Promise<Set<number>> => {
|
||||||
|
const res = db.exec(`SELECT id FROM test WHERE ${filter}`);
|
||||||
|
if (!res.length) return new Set();
|
||||||
|
return new Set(res[0].values.flat());
|
||||||
|
};
|
||||||
|
|
||||||
|
db.run(
|
||||||
|
"CREATE TABLE test (id INTEGER PRIMARY KEY, string STRING, decimal DECIMAL(4,2))"
|
||||||
|
);
|
||||||
|
|
||||||
|
const stmt = db.prepare("INSERT INTO test (string, decimal) VALUES (?, ?)");
|
||||||
|
const count = STRING_VALUES.length * DECIMAL_VALUES.length;
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
const str = i % STRING_VALUES.length;
|
||||||
|
const dec = Math.trunc(i / STRING_VALUES.length) % DECIMAL_VALUES.length;
|
||||||
|
stmt.run([STRING_VALUES[str], DECIMAL_VALUES[dec]]);
|
||||||
|
}
|
||||||
|
stmt.free();
|
||||||
|
});
|
||||||
|
|
||||||
|
function setsEqual(set1: Set<number>, set2: Set<number>): boolean {
|
||||||
|
if (set1.size !== set2.size) return false;
|
||||||
|
for (const s of set1) if (!set2.has(s)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPermutations(...arrs: any[][]): any[][] {
|
||||||
|
const count = arrs.reduce((total, arr) => total * arr.length, 1);
|
||||||
|
const res = [];
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
let j = i;
|
||||||
|
const row = [];
|
||||||
|
for (const arr of arrs) {
|
||||||
|
const v = arr[j % arr.length];
|
||||||
|
j = Math.trunc(j / arr.length);
|
||||||
|
row.push(v);
|
||||||
|
}
|
||||||
|
res.push(row);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("minimize", async (t) => {
|
||||||
|
const cases: string[] = [];
|
||||||
|
|
||||||
|
cases.push("null");
|
||||||
|
cases.push("false");
|
||||||
|
cases.push("true");
|
||||||
|
cases.push("string");
|
||||||
|
cases.push("(string + decimal) IS NULL");
|
||||||
|
cases.push("(string + decimal) = NULL");
|
||||||
|
cases.push("COALESCE(string, decimal) = 0");
|
||||||
|
|
||||||
|
for (const [s1, s2, s3, op1, op2] of getPermutations(
|
||||||
|
STRING_VALUES.filter((s) => s),
|
||||||
|
STRING_VALUES.filter((s) => s),
|
||||||
|
STRING_VALUES.filter((s) => s),
|
||||||
|
[">", "=", "<"],
|
||||||
|
["<>", ">="]
|
||||||
|
)) {
|
||||||
|
cases.push(
|
||||||
|
`string ${op1} "${s1}" OR string ='${s2}' OR NOT string ${op2} '${s3}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [s1, s2] of getPermutations(
|
||||||
|
STRING_VALUES.filter((s) => s),
|
||||||
|
STRING_VALUES.filter((s) => s)
|
||||||
|
))
|
||||||
|
cases.push(`string > "${s1}" AND string < '${s2}'`);
|
||||||
|
|
||||||
|
for (const c of cases) {
|
||||||
|
const res1 = await t.context.query(c);
|
||||||
|
const min = stringify(minimize(parse(c), true));
|
||||||
|
const res2 = await t.context.query(min);
|
||||||
|
t.true(setsEqual(res1, res2));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unionDiff", async (t) => {
|
||||||
|
const cases = [
|
||||||
|
"true",
|
||||||
|
"decimal > 0",
|
||||||
|
"decimal > 10",
|
||||||
|
"UPPER(string || decimal) LIKE 'AB10'",
|
||||||
|
"COALESCE(string, decimal) = 0",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [c1, c2] of getPermutations(cases, cases)) {
|
||||||
|
const res1 = await t.context.query(c1);
|
||||||
|
const res2 = await t.context.query(c2);
|
||||||
|
const [union, diff] = unionDiff(parse(c1), parse(c2));
|
||||||
|
const res3 = await t.context.query(stringify(union));
|
||||||
|
const res4 = await t.context.query(stringify(diff));
|
||||||
|
|
||||||
|
const unionSet = new Set([...res1, ...res2]);
|
||||||
|
const diffSet = new Set(Array.from(res2).filter((r) => !res1.has(r)));
|
||||||
|
|
||||||
|
t.true(setsEqual(res3, unionSet));
|
||||||
|
t.true(setsEqual(res4, diffSet));
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -49,8 +49,16 @@ ava("convertOldPrecondition", (t) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
t.plan(tests.length + 2 * shouldFailTests.length);
|
t.plan(tests.length + 2 * shouldFailTests.length);
|
||||||
for (const test of tests)
|
for (const test of tests) {
|
||||||
t.is(stringify(mongodbFunctions.convertOldPrecondition(test[0])), test[1]);
|
t.is(
|
||||||
|
stringify(
|
||||||
|
mongodbFunctions.convertOldPrecondition(
|
||||||
|
test[0] as Record<string, unknown>
|
||||||
|
)
|
||||||
|
),
|
||||||
|
test[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const test of shouldFailTests) {
|
for (const test of shouldFailTests) {
|
||||||
const func = (): void => {
|
const func = (): void => {
|
||||||
@@ -72,6 +80,24 @@ ava("filterToMongoQuery", async (t) => {
|
|||||||
["Tags.tag1 = 123", { "Tags.tag1": 123 }],
|
["Tags.tag1 = 123", { "Tags.tag1": 123 }],
|
||||||
["Param1 = 'value1'", { "Param1._value": "value1" }],
|
["Param1 = 'value1'", { "Param1._value": "value1" }],
|
||||||
["Param1 <> 'value1'", { "Param1._value": { $nin: ["value1", null] } }],
|
["Param1 <> 'value1'", { "Param1._value": { $nin: ["value1", null] } }],
|
||||||
|
[
|
||||||
|
"Param1 <> 1657844103524",
|
||||||
|
{
|
||||||
|
$and: [
|
||||||
|
{ "Param1._value": { $nin: [1657844103524, null] } },
|
||||||
|
{ "Param1._value": { $nin: [new Date(1657844103524), null] } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Param1 = 1657844103524",
|
||||||
|
{
|
||||||
|
$or: [
|
||||||
|
{ "Param1._value": 1657844103524 },
|
||||||
|
{ "Param1._value": new Date(1657844103524) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
["Param1 > 'value'", { "Param1._value": { $gt: "value" } }],
|
["Param1 > 'value'", { "Param1._value": { $gt: "value" } }],
|
||||||
["Param1 IS NOT NULL", { "Param1._value": { $ne: null } }],
|
["Param1 IS NOT NULL", { "Param1._value": { $ne: null } }],
|
||||||
["Param1 LIKE 'value'", { "Param1._value": /^value$/ }],
|
["Param1 LIKE 'value'", { "Param1._value": /^value$/ }],
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
import ava from "ava";
|
import ava from "ava";
|
||||||
import Path from "../lib/common/path";
|
import Path from "../lib/common/path";
|
||||||
|
|
||||||
|
ava("parse", (t) => {
|
||||||
|
t.throws(() => Path.parse("."));
|
||||||
|
t.throws(() => Path.parse("a "));
|
||||||
|
t.throws(() => Path.parse(".a"));
|
||||||
|
t.throws(() => Path.parse("a."));
|
||||||
|
t.throws(() => Path.parse("a..b"));
|
||||||
|
t.throws(() => Path.parse("b*"));
|
||||||
|
t.throws(() => Path.parse("*b"));
|
||||||
|
t.throws(() => Path.parse("a.b c.d"));
|
||||||
|
t.throws(() => Path.parse("a["));
|
||||||
|
t.throws(() => Path.parse("a[b"));
|
||||||
|
t.throws(() => Path.parse("a[b:"));
|
||||||
|
t.throws(() => Path.parse('a[b:"waef]'));
|
||||||
|
t.notThrows(() => Path.parse("[]"));
|
||||||
|
t.notThrows(() => Path.parse("[a:]"));
|
||||||
|
t.notThrows(() => Path.parse("*"));
|
||||||
|
t.notThrows(() => Path.parse(""));
|
||||||
|
});
|
||||||
|
|
||||||
ava("toString", (t) => {
|
ava("toString", (t) => {
|
||||||
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
|
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
|
||||||
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
|
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import ava from "ava";
|
||||||
|
import { parsePing } from "../lib/ping";
|
||||||
|
|
||||||
|
ava("linux Case-1", (t) => {
|
||||||
|
const platform = "linux";
|
||||||
|
const stdout =
|
||||||
|
"PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data\n64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=6.66 ms\n64 bytes from 8.8.8.8: icmp_seq=2 ttl=118 time=6.50 ms\n64 bytes from 8.8.8.8: icmp_seq=3 ttl=118 time=6.38 ms\n\n--- 8.8.8.8 ping statistics ---\n3 packets transmitted, 3 received, 0% packet loss, time 402ms\nrtt min/avg/max/mdev = 6.381/6.511/6.656/0.112 ms";
|
||||||
|
const parsedResult = {
|
||||||
|
packetsTransmitted: 3,
|
||||||
|
packetsReceived: 3,
|
||||||
|
packetLoss: 0,
|
||||||
|
min: 6.381,
|
||||||
|
avg: 6.511,
|
||||||
|
max: 6.656,
|
||||||
|
mdev: 0.112,
|
||||||
|
};
|
||||||
|
const parsed = parsePing(platform, stdout);
|
||||||
|
t.deepEqual(parsedResult, parsed);
|
||||||
|
});
|
||||||
|
|
||||||
|
ava("linux Case-2", (t) => {
|
||||||
|
const platform = "linux";
|
||||||
|
const stdout =
|
||||||
|
"PING 10.251.9.108 (10.251.9.108): 56 data bytes\n64 bytes from 10.251.9.108: icmp_seq=0 ttl=57 time=36.758 ms\n64 bytes from 10.251.9.108: icmp_seq=1 ttl=57 time=69.094 ms\n64 bytes from 10.251.9.108: icmp_seq=2 ttl=57 time=28.868 ms\n--- 10.251.9.108 ping statistics ---3 packets transmitted, 3 packets received, 0% packet loss\nround-trip min/avg/max/stddev = 28.868/44.907/69.094/17.404 ms";
|
||||||
|
const parsedResult = {
|
||||||
|
packetsTransmitted: 3,
|
||||||
|
packetsReceived: 3,
|
||||||
|
packetLoss: 0,
|
||||||
|
min: 28.868,
|
||||||
|
avg: 44.907,
|
||||||
|
max: 69.094,
|
||||||
|
mdev: 17.404,
|
||||||
|
};
|
||||||
|
const parsed = parsePing(platform, stdout);
|
||||||
|
t.deepEqual(parsedResult, parsed);
|
||||||
|
});
|
||||||
@@ -0,0 +1,861 @@
|
|||||||
|
[
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Mark McGwire",
|
||||||
|
"hr": 65,
|
||||||
|
"avg": 0.278
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sammy Sosa",
|
||||||
|
"hr": 63,
|
||||||
|
"avg": 0.288
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"top1": {
|
||||||
|
"key1": "scalar1"
|
||||||
|
},
|
||||||
|
"top2": {
|
||||||
|
"key2": "scalar2"
|
||||||
|
},
|
||||||
|
"top3": {
|
||||||
|
"scalar1": "scalar3"
|
||||||
|
},
|
||||||
|
"top4": {
|
||||||
|
"scalar2": "scalar4"
|
||||||
|
},
|
||||||
|
"top5": "scalar5",
|
||||||
|
"top6": {
|
||||||
|
"key6": "scalar6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"text",
|
||||||
|
["a", "b", 42, "d"],
|
||||||
|
{
|
||||||
|
"a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~": "safe",
|
||||||
|
"?foo": "safe question mark",
|
||||||
|
":foo": "safe colon",
|
||||||
|
"-foo": "safe dash",
|
||||||
|
"this is#not": "a comment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"": "a"
|
||||||
|
},
|
||||||
|
"foo",
|
||||||
|
{
|
||||||
|
"key": "value",
|
||||||
|
"foo": "key"
|
||||||
|
},
|
||||||
|
[1, -2, 33],
|
||||||
|
{
|
||||||
|
"plain": "a b\nc"
|
||||||
|
},
|
||||||
|
[["s1_i1", "s1_i2"], "s2"],
|
||||||
|
{
|
||||||
|
"First occurrence": "Foo",
|
||||||
|
"Second occurrence": "Foo",
|
||||||
|
"Override anchor": "Bar",
|
||||||
|
"Reuse anchor": "Bar"
|
||||||
|
},
|
||||||
|
"k:#foo &a !t s",
|
||||||
|
["a"],
|
||||||
|
{
|
||||||
|
"escaped slash": "a/b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plain": "This unquoted scalar spans many lines.",
|
||||||
|
"quoted": "So does this quoted scalar.\n"
|
||||||
|
},
|
||||||
|
"here's to \"quotes\"",
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
},
|
||||||
|
"ab cd\nef\n\ngh\n",
|
||||||
|
["detected\n", "\n\n# detected\n", " explicit\n", "detected\n"],
|
||||||
|
"foo: bar\": baz",
|
||||||
|
"plain\\value\\with\\backslashes",
|
||||||
|
{
|
||||||
|
"plain": "text lines",
|
||||||
|
"quoted": "text lines",
|
||||||
|
"block": "text\n \tlines\n"
|
||||||
|
},
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
"foo": "you",
|
||||||
|
"bar": "far"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sequence": ["entry", ["nested"]],
|
||||||
|
"mapping": {
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"literal": "some\ntext\n",
|
||||||
|
"folded": "some text\n"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"one": "two",
|
||||||
|
"three": "four"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"five": "six",
|
||||||
|
"seven": "eight"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"Folding": "Empty line\nas a line feed",
|
||||||
|
"Chomping": "Clipped empty lines\n"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
["one", "two"],
|
||||||
|
["three", "four"]
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"explicit key": null,
|
||||||
|
"block key\n": ["one", "two"]
|
||||||
|
},
|
||||||
|
["foo"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
},
|
||||||
|
["baz", "baz"]
|
||||||
|
],
|
||||||
|
"ab\n\n \n",
|
||||||
|
{
|
||||||
|
"foo: bar\\": "baz'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Not indented": {
|
||||||
|
"By one space": "By four\n spaces\n",
|
||||||
|
"Flow style": ["By two", "Also by two", "Still by two"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"\\//||\\/||\n// || ||__\n",
|
||||||
|
{
|
||||||
|
"foo": [
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": null,
|
||||||
|
"b": null
|
||||||
|
},
|
||||||
|
"foo",
|
||||||
|
{
|
||||||
|
"a": "b",
|
||||||
|
"": "a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo\nbar:baz\tx \\$%^&*()x": 23,
|
||||||
|
"x\\ny:z\\tx $%^&*()x": 24
|
||||||
|
},
|
||||||
|
"Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n",
|
||||||
|
" foo\nbar\nbaz ",
|
||||||
|
{ "matches %": 20 },
|
||||||
|
[
|
||||||
|
"flow in block",
|
||||||
|
"Block scalar\n",
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"a": "b",
|
||||||
|
"c": 42,
|
||||||
|
"e": "f",
|
||||||
|
"g": "h",
|
||||||
|
"23": false
|
||||||
|
},
|
||||||
|
"ab",
|
||||||
|
" 1st non-empty\n2nd non-empty 3rd non-empty ",
|
||||||
|
{
|
||||||
|
"top1": {
|
||||||
|
"key1": "one"
|
||||||
|
},
|
||||||
|
"top2": {
|
||||||
|
"key2": "two"
|
||||||
|
},
|
||||||
|
"top3": {
|
||||||
|
"key3": "three"
|
||||||
|
},
|
||||||
|
"top4": {
|
||||||
|
"key4": "four"
|
||||||
|
},
|
||||||
|
"top5": {
|
||||||
|
"key5": "five"
|
||||||
|
},
|
||||||
|
"top6": "six",
|
||||||
|
"top7": "seven"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hr": ["Mark McGwire", "Sammy Sosa"],
|
||||||
|
"rbi": ["Sammy Sosa", "Ken Griffey"]
|
||||||
|
},
|
||||||
|
"\nfolded line\nnext line\n * bullet\n\n * list\n * lines\n\nlast line\n",
|
||||||
|
["word1", "word2"],
|
||||||
|
{
|
||||||
|
"a": null,
|
||||||
|
"b": null,
|
||||||
|
"c": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"nested sequences": [[[[]]], [[{}]]],
|
||||||
|
"key1": [],
|
||||||
|
"key2": {}
|
||||||
|
},
|
||||||
|
"---word1 word2",
|
||||||
|
{
|
||||||
|
"implicit block key": [
|
||||||
|
{
|
||||||
|
"implicit flow key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key ends with two colons::": "value"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"single line": null,
|
||||||
|
"a": "b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multi line": null,
|
||||||
|
"a": "b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
"key": ["item1", "item2"]
|
||||||
|
},
|
||||||
|
[
|
||||||
|
"double quoted",
|
||||||
|
"single quoted",
|
||||||
|
"plain text",
|
||||||
|
["nested"],
|
||||||
|
{
|
||||||
|
"single": "pair"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
["unicode anchor"],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "value",
|
||||||
|
"key2": "value2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key3": "value3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"trimmed\n\n\nas space",
|
||||||
|
"Mark McGwire's year was crippled by a knee injury.\n",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"single line": null,
|
||||||
|
"a": "b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multi line": null,
|
||||||
|
"a": "b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"a": {
|
||||||
|
"b": {
|
||||||
|
"c": "d"
|
||||||
|
},
|
||||||
|
"e": {
|
||||||
|
"f": "g"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"h": "i"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": {
|
||||||
|
"bar": "baz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"single line": "value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multi line": "value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"single": "text",
|
||||||
|
"double": "text"
|
||||||
|
},
|
||||||
|
" 1st non-empty\n2nd non-empty 3rd non-empty ",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item": "Super Hoop",
|
||||||
|
"quantity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item": "Basketball",
|
||||||
|
"quantity": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item": "Big Shoes",
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"a b c d\ne",
|
||||||
|
{
|
||||||
|
"a": ["b", ["c", "d"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"strip": "text",
|
||||||
|
"clip": "text\n",
|
||||||
|
"keep": "text\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "b c",
|
||||||
|
"d": "e f"
|
||||||
|
},
|
||||||
|
["single multiline - sequence entry"],
|
||||||
|
{
|
||||||
|
"1": [2, 3],
|
||||||
|
"4": 5
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"bla\"keks": "foo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bla]keks": "foo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"folded text\n",
|
||||||
|
"foo",
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"a": "b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"adjacent": "value",
|
||||||
|
"readable": "value",
|
||||||
|
"empty": null
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"a": "b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c": "d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"e": "f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"g": "h"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"tab": "\tstring"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"foo bar": "baz"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"anchored": "value",
|
||||||
|
"alias": "value"
|
||||||
|
},
|
||||||
|
["explicit indent and chomp", "chomp and explicit indent"],
|
||||||
|
{
|
||||||
|
"a": ["b", "c"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
"::vector",
|
||||||
|
": - ()",
|
||||||
|
"Up, up, and away!",
|
||||||
|
-123,
|
||||||
|
"http://example.com/foo#bar",
|
||||||
|
[
|
||||||
|
"::vector",
|
||||||
|
": - ()",
|
||||||
|
"Up, up and away!",
|
||||||
|
-123,
|
||||||
|
"http://example.com/foo#bar"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"a": "b",
|
||||||
|
"seq": ["a"],
|
||||||
|
"c": "d"
|
||||||
|
},
|
||||||
|
["foo", "bar", 42],
|
||||||
|
"line1 # no comment line3\n",
|
||||||
|
"\n\nliteral\n \n\ntext\n",
|
||||||
|
{
|
||||||
|
"a": "b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": ["a", "b"]
|
||||||
|
},
|
||||||
|
"a b c d\ne",
|
||||||
|
"---word1 word2",
|
||||||
|
["a", 2, 4, "d"],
|
||||||
|
{
|
||||||
|
"a": [
|
||||||
|
"b",
|
||||||
|
"c",
|
||||||
|
{
|
||||||
|
"d": ["e", "f"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": " more indented\nregular\n",
|
||||||
|
"b": "\n\n more indented\nregular\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"strip": "# text",
|
||||||
|
"clip": "# text\n",
|
||||||
|
"keep": "# text\n\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"safe": "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~ !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~",
|
||||||
|
"safe question mark": "?foo",
|
||||||
|
"safe colon": ":foo",
|
||||||
|
"safe dash": "-foo"
|
||||||
|
},
|
||||||
|
"line1 line2 line3\n",
|
||||||
|
["Mark McGwire", "Sammy Sosa", "Ken Griffey"],
|
||||||
|
["a"],
|
||||||
|
["a", ["b", "c"]],
|
||||||
|
{
|
||||||
|
"unicode": "Sosa did fine.☺",
|
||||||
|
"control": "\b1998\t1999\t2000\n",
|
||||||
|
"hex esc": "\r\n is \r\n",
|
||||||
|
"single": "\"Howdy!\" he cried.",
|
||||||
|
"quoted": " # Not a 'comment'.",
|
||||||
|
"tie-fighter": "|\\-*-/|"
|
||||||
|
},
|
||||||
|
[["-", "-"]],
|
||||||
|
"folded text\n",
|
||||||
|
{
|
||||||
|
"a": 13,
|
||||||
|
"1.5": "d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": 1,
|
||||||
|
"bar": 2,
|
||||||
|
"text": "a\n \nb\n\nc\n\nd\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wanted": "love ♥ and peace ☮"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mark McGwire",
|
||||||
|
"accomplishment": "Mark set a major league home run record in 1998.\n",
|
||||||
|
"stats": "65 Home Runs\n0.278 Batting Average\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "foo"
|
||||||
|
},
|
||||||
|
"1st non-empty\n2nd non-empty 3rd non-empty",
|
||||||
|
{
|
||||||
|
"quoted": "Quoted \t",
|
||||||
|
"block": "void main() {\n\tprintf(\"Hello, world!\\n\");\n}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": "blue",
|
||||||
|
"bar": "arrr",
|
||||||
|
"baz": "jazz"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"Mark McGwire": 65
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Sammy Sosa": 63
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Ken Griffy": 58
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"1": 2,
|
||||||
|
"3": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hr": ["Mark McGwire", "Sammy Sosa"],
|
||||||
|
"rbi": ["Sammy Sosa", "Ken Griffey"]
|
||||||
|
},
|
||||||
|
"k:#foo &a !t s",
|
||||||
|
{
|
||||||
|
"block sequence": [
|
||||||
|
"one",
|
||||||
|
{
|
||||||
|
"two": "three"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"First occurrence": "Value",
|
||||||
|
"Second occurrence": "Value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a true": "null d",
|
||||||
|
"e 42": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
},
|
||||||
|
["foo", "bar", 42],
|
||||||
|
"trimmed\n\n\nas space",
|
||||||
|
"scalar",
|
||||||
|
{
|
||||||
|
"strip": "",
|
||||||
|
"clip": "",
|
||||||
|
"keep": "\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": {
|
||||||
|
"bar": 1
|
||||||
|
},
|
||||||
|
"baz": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": 47,
|
||||||
|
"c": "d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"implicit block key": [
|
||||||
|
{
|
||||||
|
"implicit flow key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
["a", "b", "c", "c", ""],
|
||||||
|
[
|
||||||
|
["a", "b", "c"],
|
||||||
|
{
|
||||||
|
"a": "b",
|
||||||
|
"c": "d",
|
||||||
|
"e": "f"
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"implicit block key": [
|
||||||
|
{
|
||||||
|
"implicit flow key": "value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "ab\n\ncd\nef\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"literal": "value\n",
|
||||||
|
"folded": "value\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": [
|
||||||
|
"b",
|
||||||
|
"c",
|
||||||
|
{
|
||||||
|
"d": ["e", "f"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"literal\n\ttext\n",
|
||||||
|
"foo \n\n\t bar\n\nbaz\n",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"a": "b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ab",
|
||||||
|
["plain", "double quoted", "single quoted", "block\n", "plain again"],
|
||||||
|
{
|
||||||
|
"a": " ",
|
||||||
|
"b": " ",
|
||||||
|
"c": " ",
|
||||||
|
"d": " ",
|
||||||
|
"e": "\n",
|
||||||
|
"f": "\n",
|
||||||
|
"g": "\n\n",
|
||||||
|
"h": "\n\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value with\ntabs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"": null
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"single line": "value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"multi line": "value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"folded to a space,\nto a line feed, or \t \tnon-content",
|
||||||
|
["literal\n", " folded\n", "keep\n\n", " strip"],
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"american": ["Boston Red Sox", "Detroit Tigers", "New York Yankees"],
|
||||||
|
"national": ["New York Mets", "Chicago Cubs", "Atlanta Braves"]
|
||||||
|
},
|
||||||
|
" 1st non-empty\n2nd non-empty 3rd non-empty ",
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
["a", "b"],
|
||||||
|
{
|
||||||
|
"a": "b"
|
||||||
|
},
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
"folded to a space,\nto a line feed, or \t \tnon-content",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
["detected\n", "\n\n# detected\n", " explicit\n", "\t\ndetected\n"],
|
||||||
|
{
|
||||||
|
"top1": [
|
||||||
|
"item1",
|
||||||
|
{
|
||||||
|
"key2": "value2"
|
||||||
|
},
|
||||||
|
"item3"
|
||||||
|
],
|
||||||
|
"top2": "value2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": [42],
|
||||||
|
"bar": [44]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"23": "d",
|
||||||
|
"a": 4.2
|
||||||
|
},
|
||||||
|
"Document",
|
||||||
|
{
|
||||||
|
"Date": "2001-11-23 15:03:17 -5",
|
||||||
|
"User": "ed",
|
||||||
|
"Fatal": "Unknown variable \"bar\"",
|
||||||
|
"Stack": [
|
||||||
|
{
|
||||||
|
"file": "TopClass.py",
|
||||||
|
"line": 23,
|
||||||
|
"code": "x = MoreObject(\"345\\n\")\n"
|
||||||
|
},
|
||||||
|
{ "file": "MoreClass.py", "line": 58, "code": "foo = bar" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plain key": "in-line value",
|
||||||
|
"": null,
|
||||||
|
"quoted key": ["entry"]
|
||||||
|
},
|
||||||
|
["12", 12, "12"],
|
||||||
|
{
|
||||||
|
"aaa": "bbb"
|
||||||
|
},
|
||||||
|
[":,"],
|
||||||
|
{
|
||||||
|
"sequence": ["one", "two"],
|
||||||
|
"mapping": {
|
||||||
|
"sky": "blue",
|
||||||
|
"sea": "green"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"seq": ["a", "b"]
|
||||||
|
},
|
||||||
|
"here's to \"quotes\"",
|
||||||
|
{
|
||||||
|
"hr": 65,
|
||||||
|
"avg": 0.278,
|
||||||
|
"rbi": 147
|
||||||
|
},
|
||||||
|
"\n\nliteral\n \n\ntext\n",
|
||||||
|
" 1st non-empty\n2nd non-empty 3rd non-empty ",
|
||||||
|
"literal\n\ttext\n",
|
||||||
|
{
|
||||||
|
"block mapping": {
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
" foo\nbar\nbaz ",
|
||||||
|
"ab cd\nef\n\ngh\n",
|
||||||
|
"foo",
|
||||||
|
{
|
||||||
|
"top1": {
|
||||||
|
"key1": "one"
|
||||||
|
},
|
||||||
|
"top2": {
|
||||||
|
"key2": "two"
|
||||||
|
},
|
||||||
|
"top3": {
|
||||||
|
"key3": "three"
|
||||||
|
},
|
||||||
|
"top4": {
|
||||||
|
"key4": "four"
|
||||||
|
},
|
||||||
|
"top5": {
|
||||||
|
"key5": "five"
|
||||||
|
},
|
||||||
|
"top6": "six",
|
||||||
|
"top7": "seven"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"url": "http://example.org"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"sequence": ["one", "two"],
|
||||||
|
"mapping": {
|
||||||
|
"sky": "blue",
|
||||||
|
"sea": "green"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invoice": 34843,
|
||||||
|
"date": "2001-01-23",
|
||||||
|
"bill-to": {
|
||||||
|
"given": "Chris",
|
||||||
|
"family": "Dumars",
|
||||||
|
"address": {
|
||||||
|
"lines": "458 Walkman Dr.\nSuite #292\n",
|
||||||
|
"city": "Royal Oak",
|
||||||
|
"state": "MI",
|
||||||
|
"postal": 48046
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ship-to": {
|
||||||
|
"given": "Chris",
|
||||||
|
"family": "Dumars",
|
||||||
|
"address": {
|
||||||
|
"lines": "458 Walkman Dr.\nSuite #292\n",
|
||||||
|
"city": "Royal Oak",
|
||||||
|
"state": "MI",
|
||||||
|
"postal": 48046
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"product": [
|
||||||
|
{
|
||||||
|
"sku": "BL394D",
|
||||||
|
"quantity": 4,
|
||||||
|
"description": "Basketball",
|
||||||
|
"price": 450
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sku": "BL4438H",
|
||||||
|
"quantity": 1,
|
||||||
|
"description": "Super Hoop",
|
||||||
|
"price": 2392
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tax": 251.42,
|
||||||
|
"total": 4443.52,
|
||||||
|
"comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338."
|
||||||
|
},
|
||||||
|
["a", "b", "a", "b"],
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
"block node\n",
|
||||||
|
["one", "two"],
|
||||||
|
{
|
||||||
|
"one": "two"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"a": "scalar a",
|
||||||
|
"b": "scalar a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo": "",
|
||||||
|
"": "bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
},
|
||||||
|
"scalar %YAML 1.2",
|
||||||
|
{
|
||||||
|
"Folding": "Empty line\nas a line feed",
|
||||||
|
"Chomping": "Clipped empty lines\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
["name", "hr", "avg"],
|
||||||
|
["Mark McGwire", 65, 0.278],
|
||||||
|
["Sammy Sosa", 63, 0.288]
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"literal": "value\n",
|
||||||
|
"folded": "value\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Mark McGwire": {
|
||||||
|
"hr": 65,
|
||||||
|
"avg": 0.278
|
||||||
|
},
|
||||||
|
"Sammy Sosa": {
|
||||||
|
"hr": 63,
|
||||||
|
"avg": 0.288
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": "b",
|
||||||
|
"c": "d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": [[["value"]]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"a": 1,
|
||||||
|
"b": null,
|
||||||
|
"c": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event": "outgoing HTTP response",
|
||||||
|
"timestamp": "2021-01-21T18:44:57.116Z",
|
||||||
|
"remoteAddress": "127.0.0.1",
|
||||||
|
"deviceId": "202BC1-BM632w-000000",
|
||||||
|
"connection": "2021-01-21T18:44:57.105Z",
|
||||||
|
"statusCode": 200,
|
||||||
|
"headers": {
|
||||||
|
"content-length": 528,
|
||||||
|
"server": "GenieACS/1.2.3+20210121000910",
|
||||||
|
"soapserver": "GenieACS/1.2.3+20210121000910",
|
||||||
|
"content-type": "text/xml; charset=\"utf-8\"",
|
||||||
|
"set-cookie": "session=ea31d97ccf6832fb"
|
||||||
|
},
|
||||||
|
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<soap-env:Envelope xmlns:soap-enc=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:cwmp=\"urn:dslforum-org:cwmp-1-0\"><soap-env:Header><cwmp:ID soap-env:mustUnderstand=\"1\">wdsx50vq</cwmp:ID></soap-env:Header><soap-env:Body><cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse></soap-env:Body></soap-env:Envelope>"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import ava from "ava";
|
||||||
|
import * as yaml from "yaml";
|
||||||
|
import { stringify } from "../lib/common/yaml";
|
||||||
|
|
||||||
|
import testCases from "./yaml-tests.json";
|
||||||
|
|
||||||
|
ava("stringify", (t) => {
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
let str = stringify(testCase);
|
||||||
|
if (str.startsWith(">2")) str = ">3" + str.slice(2);
|
||||||
|
if (str.startsWith("|2")) str = "|3" + str.slice(2);
|
||||||
|
t.deepEqual(yaml.parse(yaml.stringify(testCase)), yaml.parse(str));
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright 2013-2019 GenieACS Inc.
|
|
||||||
*
|
|
||||||
* This file is part of GenieACS.
|
|
||||||
*
|
|
||||||
* GenieACS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* GenieACS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import { connect, devicesCollection } from "../lib/db";
|
|
||||||
|
|
||||||
const outputFilename = path.resolve(`data_model_${process.argv[2]}.json`);
|
|
||||||
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
|
|
||||||
|
|
||||||
const dataModel = {};
|
|
||||||
|
|
||||||
function recursive(object, p = ""): void {
|
|
||||||
for (const key in object) {
|
|
||||||
if (key[0] === "_" || !object[key]) continue;
|
|
||||||
|
|
||||||
const isObject = object[key]["_object"];
|
|
||||||
const isValue = "_value" in object[key];
|
|
||||||
|
|
||||||
if ((isObject && isValue) || (!isObject && !isValue))
|
|
||||||
throw new Error("Unexpected object");
|
|
||||||
|
|
||||||
if (isValue) {
|
|
||||||
let value = object[key]["_value"];
|
|
||||||
if (typeof value === "boolean") value = +value;
|
|
||||||
|
|
||||||
const values = [object[key]["_writable"] || false, value.toString()];
|
|
||||||
if (object[key]["_type"]) values.push(object[key]["_type"]);
|
|
||||||
|
|
||||||
dataModel[`${p}${key}`] = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isObject) {
|
|
||||||
const values = [object[key]["_writable"] || false];
|
|
||||||
dataModel[`${p}${key}.`] = values;
|
|
||||||
recursive(object[key], `${p}${key}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connect()
|
|
||||||
.then(() => {
|
|
||||||
devicesCollection.findOne(
|
|
||||||
{
|
|
||||||
_id: process.argv[2],
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
(err, device) => {
|
|
||||||
if (err) throw err;
|
|
||||||
|
|
||||||
if (!device) throw new Error("Device not found");
|
|
||||||
|
|
||||||
recursive(device);
|
|
||||||
|
|
||||||
fs.writeFile(outputFilename, JSON.stringify(dataModel, null, 2), () => {
|
|
||||||
if (err) throw err;
|
|
||||||
|
|
||||||
process.stdout.write(`${outputFilename} saved\n`);
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
+4
-7
@@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": ["ES2019", "dom"],
|
||||||
"esnext", "dom"
|
"module": "ES2020",
|
||||||
],
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "esnext",
|
"target": "ES2019",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
@@ -18,7 +16,6 @@
|
|||||||
"./lib/**/*",
|
"./lib/**/*",
|
||||||
"./ui/**/*",
|
"./ui/**/*",
|
||||||
"./test/**/*",
|
"./test/**/*",
|
||||||
"./build/**/*",
|
"./build/**/*"
|
||||||
"./tools/*.ts"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -64,50 +64,37 @@ const adminPages = [
|
|||||||
|
|
||||||
let state;
|
let state;
|
||||||
|
|
||||||
let lastRenderTimestamp = 0;
|
|
||||||
let pageVisitTimestamp = 0;
|
|
||||||
let fulfillTimeout = null;
|
|
||||||
|
|
||||||
function fulfill(): void {
|
|
||||||
clearTimeout(fulfillTimeout);
|
|
||||||
fulfillTimeout = setTimeout(() => {
|
|
||||||
store.fulfill(lastRenderTimestamp, pageVisitTimestamp, (updated) => {
|
|
||||||
if (updated) m.redraw();
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pagify(pageName, page): RouteResolver {
|
function pagify(pageName, page): RouteResolver {
|
||||||
const component: RouteResolver = {
|
const component: RouteResolver = {
|
||||||
render: () => {
|
render: () => {
|
||||||
lastRenderTimestamp = Date.now();
|
const lastRenderTimestamp = Date.now();
|
||||||
let p;
|
let p;
|
||||||
if (state && state.error) p = m(errorPage.component, state);
|
if (state?.error) p = m(errorPage.component, state);
|
||||||
else p = m(contextifyComponent(page.component), state);
|
else p = m(contextifyComponent(page.component), state);
|
||||||
fulfill();
|
|
||||||
const attrs = {};
|
const attrs = {};
|
||||||
attrs["page"] = pageName;
|
attrs["page"] = pageName;
|
||||||
|
attrs["oncreate"] = attrs["onupdate"] = () => {
|
||||||
|
store.fulfill(lastRenderTimestamp);
|
||||||
|
};
|
||||||
return m(layout, attrs, p);
|
return m(layout, attrs, p);
|
||||||
},
|
},
|
||||||
onmatch: null,
|
onmatch: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
component.onmatch = (args, requestedPath) => {
|
component.onmatch = (args, requestedPath) => {
|
||||||
pageVisitTimestamp = Date.now();
|
store.setTimestamp(Date.now());
|
||||||
if (!page.init) {
|
if (!page.init) {
|
||||||
state = null;
|
state = null;
|
||||||
fulfill();
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
page
|
page
|
||||||
.init(args)
|
.init(args)
|
||||||
.then((st) => {
|
.then((st) => {
|
||||||
if (!st) return void m.route.set("/");
|
if (!st) return void m.route.set("/");
|
||||||
state = st;
|
state = st;
|
||||||
resolve();
|
resolve();
|
||||||
fulfill();
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!window.username && err.message.indexOf("authorized") >= 0) {
|
if (!window.username && err.message.indexOf("authorized") >= 0) {
|
||||||
|
|||||||
+24
-12
@@ -23,13 +23,13 @@ type AutocompleteCallback = (
|
|||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export default class Autocomplete {
|
export default class Autocomplete {
|
||||||
private callback: AutocompleteCallback;
|
private declare callback: AutocompleteCallback;
|
||||||
private element: HTMLInputElement;
|
private declare element: HTMLInputElement;
|
||||||
private hideTimeout: NodeJS.Timeout;
|
private declare hideTimeout: NodeJS.Timeout;
|
||||||
private visible: boolean;
|
private declare visible: boolean;
|
||||||
private default: string;
|
private declare default: string;
|
||||||
private selection: number;
|
private declare selection: number;
|
||||||
private container: HTMLElement;
|
private declare container: HTMLElement;
|
||||||
|
|
||||||
public constructor(className: string, callback: AutocompleteCallback) {
|
public constructor(className: string, callback: AutocompleteCallback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
@@ -51,11 +51,8 @@ export default class Autocomplete {
|
|||||||
|
|
||||||
el.addEventListener("focus", () => {
|
el.addEventListener("focus", () => {
|
||||||
this.element = el;
|
this.element = el;
|
||||||
const domRect = el.getBoundingClientRect();
|
|
||||||
this.container.style.left = `${domRect.left + window.pageXOffset}px`;
|
|
||||||
this.container.style.width = `${domRect.width}px`;
|
|
||||||
this.container.style.top = `${domRect.bottom + window.pageYOffset}px`;
|
|
||||||
this.update();
|
this.update();
|
||||||
|
this.reposition();
|
||||||
});
|
});
|
||||||
|
|
||||||
el.addEventListener("blur", () => {
|
el.addEventListener("blur", () => {
|
||||||
@@ -71,7 +68,8 @@ export default class Autocomplete {
|
|||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
if (this.default != null) {
|
if (this.default != null) {
|
||||||
el.value = this.default;
|
el.value = this.default;
|
||||||
e.preventDefault();
|
e.stopImmediatePropagation();
|
||||||
|
el.dispatchEvent(new InputEvent("input"));
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
} else if (e.key === "ArrowDown") {
|
} else if (e.key === "ArrowDown") {
|
||||||
@@ -93,6 +91,19 @@ export default class Autocomplete {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public reposition(): void {
|
||||||
|
if (!this.element) return;
|
||||||
|
const domRect = this.element.getBoundingClientRect();
|
||||||
|
if (!domRect.width) {
|
||||||
|
// Element has been removed
|
||||||
|
if (this.visible) this.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.container.style.left = `${domRect.left + window.scrollX}px`;
|
||||||
|
this.container.style.width = `${domRect.width}px`;
|
||||||
|
this.container.style.top = `${domRect.bottom + window.scrollY}px`;
|
||||||
|
}
|
||||||
|
|
||||||
private hide(): void {
|
private hide(): void {
|
||||||
this.container.style.opacity = "0";
|
this.container.style.opacity = "0";
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
@@ -159,6 +170,7 @@ export default class Autocomplete {
|
|||||||
e.addEventListener("mousedown", (ev) => {
|
e.addEventListener("mousedown", (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
el.value = suggestion.value;
|
el.value = suggestion.value;
|
||||||
|
el.dispatchEvent(new InputEvent("input"));
|
||||||
if (this.element === el) this.update();
|
if (this.element === el) this.update();
|
||||||
});
|
});
|
||||||
this.container.appendChild(e);
|
this.container.appendChild(e);
|
||||||
|
|||||||
@@ -17,17 +17,23 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { VnodeDOM, ClosureComponent, Component } from "mithril";
|
import { VnodeDOM, ClosureComponent } from "mithril";
|
||||||
import * as notifications from "./notifications";
|
import * as notifications from "./notifications";
|
||||||
import { changePassword } from "./store";
|
import { changePassword } from "./store";
|
||||||
import { m } from "./components";
|
import { m } from "./components";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
interface Attrs {
|
||||||
|
noAuth?: boolean;
|
||||||
|
username?: string;
|
||||||
|
onPasswordChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const component: ClosureComponent<Attrs> = () => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const onPasswordChange = vnode.attrs["onPasswordChange"];
|
const onPasswordChange = vnode.attrs.onPasswordChange;
|
||||||
const enforceAuth = !vnode.attrs["noAuth"];
|
const enforceAuth = !vnode.attrs.noAuth;
|
||||||
const username = vnode.attrs["username"];
|
const username = vnode.attrs.username;
|
||||||
|
|
||||||
if (username) vnode.state["username"] = username;
|
if (username) vnode.state["username"] = username;
|
||||||
|
|
||||||
|
|||||||
+29
-16
@@ -17,41 +17,54 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ClosureComponent, Component } from "mithril";
|
import { Editor } from "codemirror";
|
||||||
|
import { ClosureComponent } from "mithril";
|
||||||
import { m } from "./components";
|
import { m } from "./components";
|
||||||
import { codeMirror } from "./dynamic-loader";
|
import { codeMirror } from "./dynamic-loader";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
interface Attrs {
|
||||||
|
id: string;
|
||||||
|
value: string;
|
||||||
|
mode: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
focus?: boolean;
|
||||||
|
onSubmit?: (dom: Element) => void;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onReady?: (editor: Editor) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const component: ClosureComponent<Attrs> = () => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return m("textarea", {
|
return m("textarea", {
|
||||||
name: vnode.attrs["id"],
|
name: vnode.attrs.id,
|
||||||
value: vnode.attrs["value"],
|
value: vnode.attrs.value,
|
||||||
oncreate: (_vnode) => {
|
oncreate: (_vnode) => {
|
||||||
const editor = codeMirror.fromTextArea(_vnode.dom, {
|
const editor = codeMirror.fromTextArea(
|
||||||
mode: vnode.attrs["mode"],
|
_vnode.dom as HTMLTextAreaElement,
|
||||||
|
{
|
||||||
|
mode: vnode.attrs.mode,
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
readOnly: vnode.attrs["readOnly"],
|
readOnly: vnode.attrs.readOnly,
|
||||||
extraKeys: {
|
extraKeys: {
|
||||||
"Ctrl-Enter": () => {
|
"Ctrl-Enter": () => {
|
||||||
if (vnode.attrs["onSubmit"])
|
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
|
||||||
vnode.attrs["onSubmit"](_vnode.dom);
|
|
||||||
},
|
},
|
||||||
"Cmd-Enter": () => {
|
"Cmd-Enter": () => {
|
||||||
if (vnode.attrs["onSubmit"])
|
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
|
||||||
vnode.attrs["onSubmit"](_vnode.dom);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (vnode.attrs["onChange"]) {
|
if (vnode.attrs.onChange) {
|
||||||
editor.on("change", (e) => {
|
editor.on("change", (e) => {
|
||||||
vnode.attrs["onChange"](e.getValue());
|
vnode.attrs.onChange(e.getValue());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vnode.attrs["focus"]) editor.focus();
|
if (vnode.attrs.focus) editor.focus();
|
||||||
if (vnode.attrs["onReady"]) vnode.attrs["onReady"](editor);
|
if (vnode.attrs.onReady) vnode.attrs.onReady(editor);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
+17
-4
@@ -22,7 +22,7 @@ import m, {
|
|||||||
Attributes,
|
Attributes,
|
||||||
Children,
|
Children,
|
||||||
ComponentTypes,
|
ComponentTypes,
|
||||||
Lifecycle,
|
CommonAttributes,
|
||||||
ClosureComponent,
|
ClosureComponent,
|
||||||
Vnode,
|
Vnode,
|
||||||
} from "mithril";
|
} from "mithril";
|
||||||
@@ -39,6 +39,7 @@ import tags from "./components/tags";
|
|||||||
import ping from "./components/ping";
|
import ping from "./components/ping";
|
||||||
import deviceLink from "./components/device-link";
|
import deviceLink from "./components/device-link";
|
||||||
import longTextComponent from "./long-text-component";
|
import longTextComponent from "./long-text-component";
|
||||||
|
import loading from "./components/loading";
|
||||||
|
|
||||||
const comps = {
|
const comps = {
|
||||||
parameter,
|
parameter,
|
||||||
@@ -54,6 +55,7 @@ const comps = {
|
|||||||
ping,
|
ping,
|
||||||
"device-link": deviceLink,
|
"device-link": deviceLink,
|
||||||
"long-text": longTextComponent,
|
"long-text": longTextComponent,
|
||||||
|
loading: loading,
|
||||||
};
|
};
|
||||||
|
|
||||||
const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
|
const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
|
||||||
@@ -79,7 +81,7 @@ interface MC extends Static {
|
|||||||
<Attrs, State>(
|
<Attrs, State>(
|
||||||
ctx: Attributes,
|
ctx: Attributes,
|
||||||
component: ComponentTypes<Attrs, State>,
|
component: ComponentTypes<Attrs, State>,
|
||||||
attributes: Attrs & Lifecycle<Attrs, State> & { key?: string | number },
|
attributes: Attrs & CommonAttributes<Attrs, State>,
|
||||||
...args: Children[]
|
...args: Children[]
|
||||||
): Vnode<Attrs, State>;
|
): Vnode<Attrs, State>;
|
||||||
};
|
};
|
||||||
@@ -114,7 +116,7 @@ function applyContext(vnode, parentContext): void {
|
|||||||
vnodeContext.set(vnode, vc);
|
vnodeContext.set(vnode, vc);
|
||||||
vnode.attrs = Object.assign({}, vc, vnode.attrs);
|
vnode.attrs = Object.assign({}, vc, vnode.attrs);
|
||||||
}
|
}
|
||||||
if (vnode.children && vnode.children.length)
|
if (vnode.children?.length)
|
||||||
for (const c of vnode.children) applyContext(c, vc);
|
for (const c of vnode.children) applyContext(c, vc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,15 +133,26 @@ export function contextifyComponent(component: ComponentTypes): ComponentTypes {
|
|||||||
applyContext(res, context);
|
applyContext(res, context);
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
} else if (!component.prototype || !component.prototype.view) {
|
} else if (!component.prototype?.view) {
|
||||||
c = (initialNode) => {
|
c = (initialNode) => {
|
||||||
const state = (component as ClosureComponent)(initialNode);
|
const state = (component as ClosureComponent)(initialNode);
|
||||||
const view = state.view;
|
const view = state.view;
|
||||||
state.view = function (vnode) {
|
state.view = function (vnode) {
|
||||||
const context = vnodeContext.get(vnode) || {};
|
const context = vnodeContext.get(vnode) || {};
|
||||||
|
try {
|
||||||
const res = Reflect.apply(view, this, [vnode]);
|
const res = Reflect.apply(view, this, [vnode]);
|
||||||
applyContext(res, context);
|
applyContext(res, context);
|
||||||
return res;
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
return m(
|
||||||
|
"p.error",
|
||||||
|
{
|
||||||
|
title: "Click to print stack trace to console",
|
||||||
|
onclick: () => console.error(err),
|
||||||
|
},
|
||||||
|
"Error!"
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import * as taskQueue from "../task-queue";
|
|||||||
import { parse } from "../../lib/common/expression-parser";
|
import { parse } from "../../lib/common/expression-parser";
|
||||||
import memoize from "../../lib/common/memoize";
|
import memoize from "../../lib/common/memoize";
|
||||||
import { getIcon } from "../icons";
|
import { getIcon } from "../icons";
|
||||||
|
import { evaluateExpression } from "../store";
|
||||||
|
import debounce from "../../lib/common/debounce";
|
||||||
|
|
||||||
const memoizedParse = memoize(parse);
|
const memoizedParse = memoize(parse);
|
||||||
|
|
||||||
@@ -51,27 +53,31 @@ function orderKeysByDepth(device: Record<string, unknown>): string[][] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
|
let queryString: string;
|
||||||
|
const formQueryString = debounce((args: string[]) => {
|
||||||
|
queryString = args[args.length - 1];
|
||||||
|
m.redraw();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
|
|
||||||
|
const limit = evaluateExpression(vnode.attrs["limit"], device) || 100;
|
||||||
|
|
||||||
const search = m("input", {
|
const search = m("input", {
|
||||||
type: "text",
|
type: "text",
|
||||||
placeholder: "Search parameters",
|
placeholder: "Search parameters",
|
||||||
oninput: (e) => {
|
oninput: (e) => {
|
||||||
vnode.state["searchString"] = e.target.value;
|
formQueryString(e.target.value);
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
clearTimeout(vnode.state["timeout"]);
|
|
||||||
vnode.state["timeout"] = setTimeout(m.redraw, 500);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const instanceRegex = /\.[0-9]+$/;
|
const instanceRegex = /\.[0-9]+$/;
|
||||||
let re;
|
let re;
|
||||||
if (vnode.state["searchString"]) {
|
if (queryString) {
|
||||||
const keywords = vnode.state["searchString"]
|
const keywords = queryString.split(" ").filter((s) => s);
|
||||||
.split(" ")
|
|
||||||
.filter((s) => s);
|
|
||||||
if (keywords.length)
|
if (keywords.length)
|
||||||
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
|
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
|
||||||
}
|
}
|
||||||
@@ -83,10 +89,10 @@ const component: ClosureComponent = (): Component => {
|
|||||||
let c = 0;
|
let c = 0;
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
const p = device[k];
|
const p = device[k];
|
||||||
const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k;
|
const str = p.value?.[0] ? `${k} ${p.value[0]}` : k;
|
||||||
if (re && !re.test(str)) continue;
|
if (re && !re.test(str)) continue;
|
||||||
++c;
|
++c;
|
||||||
if (count < 100) filteredKeys.push(k);
|
if (count < limit) filteredKeys.push(k);
|
||||||
}
|
}
|
||||||
count += c;
|
count += c;
|
||||||
}
|
}
|
||||||
@@ -169,6 +175,9 @@ const component: ClosureComponent = (): Component => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
|
"loading",
|
||||||
|
{ queries: [vnode.attrs["deviceQuery"]] },
|
||||||
|
m(
|
||||||
".all-parameters",
|
".all-parameters",
|
||||||
m(
|
m(
|
||||||
"a.download-csv",
|
"a.download-csv",
|
||||||
@@ -190,6 +199,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
`Displaying ${filteredKeys.length} out of ${count} parameters.`
|
`Displaying ${filteredKeys.length} out of ${count} parameters.`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+59
-12
@@ -17,29 +17,76 @@
|
|||||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ClosureComponent, Component } from "mithril";
|
import { Attributes, ClosureComponent, Component } from "mithril";
|
||||||
|
import { map } from "../../lib/common/expression-parser";
|
||||||
|
import memoize from "../../lib/common/memoize";
|
||||||
|
import { Expression } from "../../lib/types";
|
||||||
import { m } from "../components";
|
import { m } from "../components";
|
||||||
import * as store from "../store";
|
import { evaluateExpression, getTimestamp } from "../store";
|
||||||
|
|
||||||
|
const evaluateAttributes = memoize(
|
||||||
|
(
|
||||||
|
attrs: Record<string, Expression>,
|
||||||
|
obj: Record<string, unknown>,
|
||||||
|
now: number // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
|
): Attributes => {
|
||||||
|
const res: Attributes = {};
|
||||||
|
for (const [k, v] of Object.entries(attrs)) {
|
||||||
|
const vv = map(v, (e) => {
|
||||||
|
if (
|
||||||
|
Array.isArray(e) &&
|
||||||
|
e[0] === "FUNC" &&
|
||||||
|
e[1] === "ENCODEURICOMPONENT"
|
||||||
|
) {
|
||||||
|
const a = evaluateExpression(e[2], obj);
|
||||||
|
if (a == null) return null;
|
||||||
|
return encodeURIComponent(a as string);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
res[k] = evaluateExpression(vv, obj);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
if (vnode.attrs["filter"]) {
|
const device = vnode.attrs["device"];
|
||||||
if (
|
if ("filter" in vnode.attrs) {
|
||||||
!store.evaluateExpression(
|
if (!evaluateExpression(vnode.attrs["filter"], device || {}))
|
||||||
vnode.attrs["filter"],
|
|
||||||
vnode.attrs["device"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
||||||
|
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
|
||||||
if (typeof c !== "object") return `${c}`;
|
if (typeof c !== "object") return `${c}`;
|
||||||
return m(c["type"], c);
|
const type = evaluateExpression(c["type"], device || {});
|
||||||
|
if (!type) return null;
|
||||||
|
return m(type as string, c);
|
||||||
});
|
});
|
||||||
if (vnode.attrs["element"]) return m(vnode.attrs["element"], children);
|
|
||||||
else return children;
|
let el = vnode.attrs["element"];
|
||||||
|
|
||||||
|
if (el == null) return children;
|
||||||
|
|
||||||
|
let attrs: Attributes;
|
||||||
|
if (Array.isArray(el)) {
|
||||||
|
el = evaluateExpression(el, device || {});
|
||||||
|
} else if (typeof el === "object") {
|
||||||
|
if (el["attributes"] != null) {
|
||||||
|
attrs = evaluateAttributes(
|
||||||
|
el["attributes"],
|
||||||
|
device || {},
|
||||||
|
getTimestamp()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
el = evaluateExpression(el["tag"], device || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return m(el, attrs, children);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { m } from "../components";
|
|||||||
import * as store from "../store";
|
import * as store from "../store";
|
||||||
import * as notifications from "../notifications";
|
import * as notifications from "../notifications";
|
||||||
import { getIcon } from "../icons";
|
import { getIcon } from "../icons";
|
||||||
|
import { stringify } from "../../lib/common/yaml";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
@@ -38,6 +39,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
"Channel",
|
"Channel",
|
||||||
"Code",
|
"Code",
|
||||||
"Message",
|
"Message",
|
||||||
|
"Detail",
|
||||||
"Retries",
|
"Retries",
|
||||||
"Timestamp",
|
"Timestamp",
|
||||||
].map((l) => m("th", l));
|
].map((l) => m("th", l));
|
||||||
@@ -48,7 +50,9 @@ const component: ClosureComponent = (): Component => {
|
|||||||
rows.push([
|
rows.push([
|
||||||
m("td", f["channel"]),
|
m("td", f["channel"]),
|
||||||
m("td", f["code"]),
|
m("td", f["code"]),
|
||||||
m("td", f["message"]),
|
m("td", m("long-text", { text: f["message"] })),
|
||||||
|
m("td", m("long-text", { text: stringify(f["detail"]) })),
|
||||||
|
|
||||||
m("td", f["retries"]),
|
m("td", f["retries"]),
|
||||||
m("td", new Date(f["timestamp"]).toLocaleString()),
|
m("td", new Date(f["timestamp"]).toLocaleString()),
|
||||||
m(
|
m(
|
||||||
@@ -63,12 +67,12 @@ const component: ClosureComponent = (): Component => {
|
|||||||
.deleteResource("faults", f["_id"])
|
.deleteResource("faults", f["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Fault deleted");
|
notifications.push("success", "Fault deleted");
|
||||||
store.fulfill(Date.now(), Date.now());
|
store.setTimestamp(Date.now());
|
||||||
m.redraw();
|
m.redraw();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.fulfill(Date.now(), Date.now());
|
store.setTimestamp(Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -91,7 +95,11 @@ const component: ClosureComponent = (): Component => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return m("table.table", thead, tbody);
|
return m(
|
||||||
|
"loading",
|
||||||
|
{ queries: [faults] },
|
||||||
|
m("table.table", thead, tbody)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,18 +19,22 @@
|
|||||||
|
|
||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent, Component } from "mithril";
|
||||||
import { m } from "../components";
|
import { m } from "../components";
|
||||||
|
import { evaluateExpression } from "../store";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
let deviceId;
|
let deviceId;
|
||||||
if (vnode.attrs["device"])
|
const device = vnode.attrs["device"];
|
||||||
deviceId = vnode.attrs["device"]["DeviceID.ID"].value[0];
|
if (device) deviceId = device["DeviceID.ID"].value[0];
|
||||||
|
|
||||||
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
||||||
|
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
|
||||||
if (typeof c !== "object") return `${c}`;
|
if (typeof c !== "object") return `${c}`;
|
||||||
|
const type = evaluateExpression(c["type"], device || {});
|
||||||
|
if (!type) return null;
|
||||||
const attrs = Object.assign({}, vnode.attrs, c);
|
const attrs = Object.assign({}, vnode.attrs, c);
|
||||||
return m(attrs["type"], attrs);
|
return m(type as string, attrs);
|
||||||
});
|
});
|
||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
return m(
|
return m(
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2021 GenieACS Inc.
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { VnodeDOM, ClosureComponent, Component } from "mithril";
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
let overlay: HTMLElement;
|
||||||
|
let dom: Element;
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
function apply(vnode: VnodeDOM): void {
|
||||||
|
if (!loading) {
|
||||||
|
if (overlay) overlay.parentElement.remove();
|
||||||
|
if (dom) dom.classList.remove("loading");
|
||||||
|
overlay = null;
|
||||||
|
dom = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dom && dom !== vnode.dom) dom.classList.remove("loading");
|
||||||
|
|
||||||
|
dom = vnode.dom;
|
||||||
|
dom.classList.add("loading");
|
||||||
|
|
||||||
|
if (!overlay) {
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.style.position = "relative";
|
||||||
|
wrapper.style.pointerEvents = "none";
|
||||||
|
overlay = document.createElement("div");
|
||||||
|
overlay.classList.add("loading-overlay");
|
||||||
|
overlay.style.position = "absolute";
|
||||||
|
wrapper.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = overlay.parentElement;
|
||||||
|
if (wrapper.parentElement !== dom.parentElement)
|
||||||
|
dom.parentNode.appendChild(wrapper);
|
||||||
|
|
||||||
|
const wrapperRect = wrapper.getBoundingClientRect();
|
||||||
|
const domRect = dom.getBoundingClientRect();
|
||||||
|
overlay.style.width = `${dom.scrollWidth}px`;
|
||||||
|
overlay.style.height = `${dom.scrollHeight}px`;
|
||||||
|
overlay.style.left = `${domRect.left - wrapperRect.left}px`;
|
||||||
|
overlay.style.top = `${domRect.top - wrapperRect.top}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: (vnode) => {
|
||||||
|
const queries = vnode.attrs["queries"];
|
||||||
|
loading = queries.some((q) => q.fulfilling);
|
||||||
|
return vnode.children;
|
||||||
|
},
|
||||||
|
oncreate: apply,
|
||||||
|
onupdate: apply,
|
||||||
|
onremove: () => {
|
||||||
|
if (overlay) overlay.parentElement.remove();
|
||||||
|
if (dom) dom.classList.remove("loading");
|
||||||
|
overlay = null;
|
||||||
|
dom = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent, Component } from "mithril";
|
||||||
import { m } from "../components";
|
import { m } from "../components";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import * as store from "../store";
|
import { evaluateExpression } from "../store";
|
||||||
|
|
||||||
const CHARTS = config.ui.overview.charts;
|
const CHARTS = config.ui.overview.charts;
|
||||||
|
|
||||||
@@ -28,11 +28,12 @@ const component: ClosureComponent = (): Component => {
|
|||||||
return {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
const chart = CHARTS[vnode.attrs["chart"]] as Record<string, unknown>;
|
const chartName = evaluateExpression(vnode.attrs["chart"], device || {});
|
||||||
|
const chart = CHARTS[chartName as string] as Record<string, unknown>;
|
||||||
if (!chart) return null;
|
if (!chart) return null;
|
||||||
for (const slice of Object.values(chart.slices)) {
|
for (const slice of Object.values(chart.slices)) {
|
||||||
const filter = slice["filter"];
|
const filter = slice["filter"];
|
||||||
if (store.evaluateExpression(filter, device)) {
|
if (evaluateExpression(filter, device || {})) {
|
||||||
const dot = m(
|
const dot = m(
|
||||||
"svg",
|
"svg",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
import { ClosureComponent, Component, VnodeDOM } from "mithril";
|
import { ClosureComponent, Component, VnodeDOM } from "mithril";
|
||||||
import { m } from "../components";
|
import { m } from "../components";
|
||||||
|
import { evaluateExpression } from "../store";
|
||||||
|
|
||||||
const component: ClosureComponent = (): Component => {
|
const component: ClosureComponent = (): Component => {
|
||||||
return {
|
return {
|
||||||
@@ -26,18 +27,24 @@ const component: ClosureComponent = (): Component => {
|
|||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
|
|
||||||
const rows = Object.values(vnode.attrs["parameters"]).map((parameter) => {
|
const rows = Object.values(vnode.attrs["parameters"]).map((parameter) => {
|
||||||
|
const type = evaluateExpression(parameter["type"], device);
|
||||||
const p = m.context(
|
const p = m.context(
|
||||||
{
|
{
|
||||||
device: device,
|
device: device,
|
||||||
parameter: parameter["parameter"],
|
parameter: parameter["parameter"],
|
||||||
},
|
},
|
||||||
parameter["type"] || "parameter",
|
(type as string) || "parameter",
|
||||||
parameter
|
parameter
|
||||||
);
|
);
|
||||||
|
|
||||||
return m(
|
return m(
|
||||||
"tr",
|
"tr",
|
||||||
{
|
{
|
||||||
|
oncreate: (vn) => {
|
||||||
|
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
||||||
|
? ""
|
||||||
|
: "none";
|
||||||
|
},
|
||||||
onupdate: (vn) => {
|
onupdate: (vn) => {
|
||||||
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
(vn.dom as HTMLElement).style.display = (p as VnodeDOM).dom
|
||||||
? ""
|
? ""
|
||||||
@@ -49,7 +56,11 @@ const component: ClosureComponent = (): Component => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return m("table.parameter-list", rows);
|
return m(
|
||||||
|
"loading",
|
||||||
|
{ queries: [vnode.attrs["deviceQuery"]] },
|
||||||
|
m("table.parameter-list", rows)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
import { ClosureComponent, Component } from "mithril";
|
import { ClosureComponent, Component } from "mithril";
|
||||||
import { m } from "../components";
|
import { m } from "../components";
|
||||||
import * as taskQueue from "../task-queue";
|
import * as taskQueue from "../task-queue";
|
||||||
import * as store from "../store";
|
import { evaluateExpression } from "../store";
|
||||||
import * as expressionParser from "../../lib/common/expression-parser";
|
import * as expressionParser from "../../lib/common/expression-parser";
|
||||||
import { getIcon } from "../icons";
|
import { getIcon } from "../icons";
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
const object = store.evaluateExpression(vnode.state["object"], device);
|
const object = evaluateExpression(vnode.state["object"], device);
|
||||||
const parameters = vnode.state["parameters"];
|
const parameters = vnode.state["parameters"];
|
||||||
|
|
||||||
if (typeof object !== "string" || !device[object]) return null;
|
if (typeof object !== "string" || !device[object]) return null;
|
||||||
@@ -56,8 +56,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const i of instances) {
|
for (const i of instances) {
|
||||||
let filter =
|
let filter = "filter" in vnode.attrs ? vnode.attrs["filter"] : true;
|
||||||
vnode.attrs["filter"] != null ? vnode.attrs["filter"] : true;
|
|
||||||
|
|
||||||
filter = expressionParser.map(filter, (e) => {
|
filter = expressionParser.map(filter, (e) => {
|
||||||
if (Array.isArray(e) && e[0] === "PARAM")
|
if (Array.isArray(e) && e[0] === "PARAM")
|
||||||
@@ -65,7 +64,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
return e;
|
return e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!store.evaluateExpression(filter, device)) continue;
|
if (!evaluateExpression(filter, device)) continue;
|
||||||
|
|
||||||
const row = parameters.map((p) => {
|
const row = parameters.map((p) => {
|
||||||
const param = expressionParser.map(p.parameter, (e) => {
|
const param = expressionParser.map(p.parameter, (e) => {
|
||||||
@@ -147,9 +146,18 @@ const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let label;
|
let label;
|
||||||
if (vnode.attrs["label"]) label = m("h2", vnode.attrs["label"]);
|
|
||||||
|
|
||||||
return [label, m("table.table", thead, m("tbody", rows))];
|
const l = evaluateExpression(vnode.attrs["label"], device);
|
||||||
|
if (l != null) label = m("h2", l);
|
||||||
|
|
||||||
|
return [
|
||||||
|
label,
|
||||||
|
m(
|
||||||
|
"loading",
|
||||||
|
{ queries: [vnode.attrs["deviceQuery"]] },
|
||||||
|
m("table.table", thead, m("tbody", rows))
|
||||||
|
),
|
||||||
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const evaluateParam = memoize((exp, obj, now: number) => {
|
|||||||
) {
|
) {
|
||||||
let v = null;
|
let v = null;
|
||||||
const p = obj[e[i][1]];
|
const p = obj[e[i][1]];
|
||||||
if (p && p.value) {
|
if (p?.value) {
|
||||||
v = p.value[0];
|
v = p.value[0];
|
||||||
timestamp = Math.min(timestamp, p.valueTimestamp);
|
timestamp = Math.min(timestamp, p.valueTimestamp);
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,12 @@ const evaluateParam = memoize((exp, obj, now: number) => {
|
|||||||
value = exp;
|
value = exp;
|
||||||
} else if (exp[0] === "PARAM") {
|
} else if (exp[0] === "PARAM") {
|
||||||
const p = obj[exp[1]];
|
const p = obj[exp[1]];
|
||||||
if (p && p.value) {
|
if (p?.value) {
|
||||||
timestamp = p.valueTimestamp;
|
timestamp = p.valueTimestamp;
|
||||||
value = p.value[0];
|
value = p.value[0];
|
||||||
parameter = exp[1];
|
parameter = exp[1];
|
||||||
|
if (p.value[1] === "xsd:dateTime" && typeof value === "number")
|
||||||
|
value = new Date(value).toLocaleString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,14 +77,14 @@ const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
const { value, timestamp, parameter } = evaluateParam(
|
const { value, timestamp, parameter } = evaluateParam(
|
||||||
vnode.attrs["parameter"],
|
vnode.attrs["parameter"],
|
||||||
vnode.attrs["device"],
|
device,
|
||||||
store.getTimestamp()
|
store.getTimestamp() + store.getClockSkew()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
|
|
||||||
let edit;
|
let edit;
|
||||||
if (device[parameter] && device[parameter].writable) {
|
if (device[parameter]?.writable) {
|
||||||
edit = m(
|
edit = m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
@@ -115,7 +117,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
// Don't update any child element
|
// Don't update any child element
|
||||||
if (e.target === (el as VnodeDOM).dom) {
|
if (e.target === (el as VnodeDOM).dom) {
|
||||||
const now = Date.now();
|
const now = Date.now() + store.getClockSkew();
|
||||||
const localeString = new Date(timestamp).toLocaleString();
|
const localeString = new Date(timestamp).toLocaleString();
|
||||||
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
|
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,9 +63,11 @@ const component: ClosureComponent = (vn): Component => {
|
|||||||
param = device["Device.ManagementServer.ConnectionRequestURL"];
|
param = device["Device.ManagementServer.ConnectionRequestURL"];
|
||||||
|
|
||||||
let h;
|
let h;
|
||||||
if (param && param.value) {
|
try {
|
||||||
const url = new URL(param.value[0]);
|
const url = new URL(param.value[0]);
|
||||||
h = url.hostname;
|
h = url.hostname;
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host !== h) {
|
if (host !== h) {
|
||||||
@@ -77,7 +79,7 @@ const component: ClosureComponent = (vn): Component => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m("div", `Pinging ${host}:`);
|
return m("div", host ? `Pinging ${host}:` : "");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ const component: ClosureComponent = (): Component => {
|
|||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const device = vnode.attrs["device"];
|
const device = vnode.attrs["device"];
|
||||||
let writable = true;
|
let writable = true;
|
||||||
if (vnode.attrs["writable"] != null) writable = vnode.attrs["writable"];
|
if ("writable" in vnode.attrs)
|
||||||
|
writable = !!store.evaluateExpression(vnode.attrs["writable"], device);
|
||||||
|
|
||||||
const tags = [];
|
const tags = [];
|
||||||
for (const p of Object.keys(device))
|
for (const p of Object.keys(device))
|
||||||
@@ -64,7 +65,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${deviceId}: Tags updated`
|
`${deviceId}: Tags updated`
|
||||||
);
|
);
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
@@ -99,7 +100,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
notifications.push("success", `${deviceId}: Tags updated`);
|
notifications.push("success", `${deviceId}: Tags updated`);
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
e.target.disabled = false;
|
e.target.disabled = false;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function flattenConfig(config: Record<string, unknown>): any {
|
|||||||
// Order keys such that nested objects come last
|
// Order keys such that nested objects come last
|
||||||
function orderKeys(config: any): number {
|
function orderKeys(config: any): number {
|
||||||
let res = 1;
|
let res = 1;
|
||||||
if (typeof config !== "object") return res;
|
if (config == null || typeof config !== "object") return res;
|
||||||
if (Array.isArray(config)) {
|
if (Array.isArray(config)) {
|
||||||
for (const c of config) res += orderKeys(c);
|
for (const c of config) res += orderKeys(c);
|
||||||
return res;
|
return res;
|
||||||
@@ -79,7 +79,7 @@ export function structureConfig(config: Config[]): any {
|
|||||||
let ref = _config;
|
let ref = _config;
|
||||||
while (keys.length > 1) {
|
while (keys.length > 1) {
|
||||||
const k = keys.shift();
|
const k = keys.shift();
|
||||||
if (typeof ref[k] !== "object") ref[k] = {};
|
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
|
||||||
ref = ref[k];
|
ref = ref[k];
|
||||||
}
|
}
|
||||||
ref[keys[0]] = c.value;
|
ref[keys[0]] = c.value;
|
||||||
@@ -137,7 +137,7 @@ export function diffConfig(
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(target))
|
for (const [k, v] of Object.entries(target))
|
||||||
if (!current[k] || current[k] !== v) diff.add.push({ _id: k, value: v });
|
if (v && current[k] !== v) diff.add.push({ _id: k, value: v });
|
||||||
|
|
||||||
for (const k of Object.keys(current)) if (!target[k]) diff.remove.push(k);
|
for (const k of Object.keys(current)) if (!target[k]) diff.remove.push(k);
|
||||||
|
|
||||||
|
|||||||
+17
-13
@@ -60,12 +60,12 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
|||||||
.resourceExists("config", id)
|
.resourceExists("config", id)
|
||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
if (exists && isNew) {
|
if (exists && isNew) {
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
return void resolve({ _id: "Config already exists" });
|
return void resolve({ _id: "Config already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exists && !isNew) {
|
if (!exists && !isNew) {
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
return void resolve({ _id: "Config does not exist" });
|
return void resolve({ _id: "Config does not exist" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +76,8 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
|||||||
"success",
|
"success",
|
||||||
`Config ${exists ? "updated" : "created"}`
|
`Config ${exists ? "updated" : "created"}`
|
||||||
);
|
);
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
resolve();
|
resolve(null);
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
})
|
})
|
||||||
@@ -87,11 +87,11 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
|||||||
.deleteResource("config", object["_id"])
|
.deleteResource("config", object["_id"])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", "Config deleted");
|
notifications.push("success", "Config deleted");
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
resolve();
|
resolve(null);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -154,7 +154,7 @@ function renderTable(confsResponse, searchString): Children {
|
|||||||
{
|
{
|
||||||
base: conf,
|
base: conf,
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
putActionHandler(action, object, false)
|
putActionHandler(action, object, false)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const ErrorList = errors ? Object.values(errors) : [];
|
const ErrorList = errors ? Object.values(errors) : [];
|
||||||
@@ -255,7 +255,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
actionHandler: (action, object) => {
|
actionHandler: (action, object) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
putActionHandler(action, object, true)
|
putActionHandler(action, object, true)
|
||||||
.then((errors) => {
|
.then((errors) => {
|
||||||
const errorList = errors
|
const errorList = errors
|
||||||
@@ -267,7 +267,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
} else {
|
} else {
|
||||||
overlay.close(cb);
|
overlay.close(cb);
|
||||||
}
|
}
|
||||||
resolve();
|
resolve(null);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
@@ -322,7 +322,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
uiConfigComponent,
|
uiConfigComponent,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
onUpdate: (errs: string[]) => {
|
onUpdate: (errs: Record<string, string>) => {
|
||||||
const errors = errs ? Object.values(errs) : [];
|
const errors = errs ? Object.values(errs) : [];
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
for (const err of errors)
|
for (const err of errors)
|
||||||
@@ -337,11 +337,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
);
|
);
|
||||||
overlay.close(cb);
|
overlay.close(cb);
|
||||||
}
|
}
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
overlay.close(cb);
|
overlay.close(cb);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -365,6 +365,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing config"),
|
m("h1", "Listing config"),
|
||||||
|
m(
|
||||||
|
"loading",
|
||||||
|
{ queries: [confs] },
|
||||||
m(
|
m(
|
||||||
".all-parameters",
|
".all-parameters",
|
||||||
search,
|
search,
|
||||||
@@ -374,6 +377,7 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
renderTable(confs, vnode.state["searchString"])
|
renderTable(confs, vnode.state["searchString"])
|
||||||
),
|
),
|
||||||
m(".actions-bar", [newConfig].concat(subs))
|
m(".actions-bar", [newConfig].concat(subs))
|
||||||
|
)
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
+76
-6
@@ -46,6 +46,7 @@ button,
|
|||||||
select,
|
select,
|
||||||
option,
|
option,
|
||||||
textarea {
|
textarea {
|
||||||
|
outline-color: var(--color4);
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
color: var(--color5);
|
color: var(--color5);
|
||||||
}
|
}
|
||||||
@@ -86,7 +87,7 @@ i {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -168,6 +169,7 @@ body {
|
|||||||
|
|
||||||
& input {
|
& input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
padding: 0.3em;
|
padding: 0.3em;
|
||||||
margin-top: 0.3em;
|
margin-top: 0.3em;
|
||||||
margin-bottom: 0.3em;
|
margin-bottom: 0.3em;
|
||||||
@@ -285,9 +287,22 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > .logo {
|
& > .logo {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
& > img {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
display: inline;
|
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .version {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
right: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .user-menu {
|
& > .user-menu {
|
||||||
@@ -329,12 +344,18 @@ body {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
|
|
||||||
|
/* Workaround for Chrome ignoring bottom margin when flex container overflows */
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table {
|
table.table {
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
& table.table {
|
& table.table {
|
||||||
@@ -562,6 +583,7 @@ span.na {
|
|||||||
.all-parameters {
|
.all-parameters {
|
||||||
& > input {
|
& > input {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
width: 700px;
|
width: 700px;
|
||||||
@@ -569,6 +591,10 @@ span.na {
|
|||||||
|
|
||||||
& > .parameter-list {
|
& > .parameter-list {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
& button {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
|
|
||||||
@@ -622,7 +648,7 @@ span.na {
|
|||||||
|
|
||||||
span.tag {
|
span.tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="190" height="16"><path d="M6.59 15.42l-6-6a2 2 0 0 1 0-2.83l6-6A2 2 0 0 1 8 0h180a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-1.41-.58zM4.35 6.59A2 2 0 1 0 7.17 9.4 2 2 0 0 0 4.35 6.6z" fill="#e8d5b2"></path></svg>');
|
background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="190" height="16"><path d="M6.59 15.42l-6-6a2 2 0 0 1 0-2.83l6-6A2 2 0 0 1 8 0h180a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H8a2 2 0 0 1-1.41-.58zM4.35 6.59A2 2 0 1 0 7.17 9.4 2 2 0 0 0 4.35 6.6z" fill="%23e8d5b2"></path></svg>');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
border-radius: 0.2em;
|
border-radius: 0.2em;
|
||||||
@@ -706,6 +732,7 @@ span.long-text-overflowed {
|
|||||||
|
|
||||||
textarea.long-text {
|
textarea.long-text {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
min-width: 10vw;
|
min-width: 10vw;
|
||||||
min-height: 10vh;
|
min-height: 10vh;
|
||||||
}
|
}
|
||||||
@@ -727,8 +754,7 @@ span.parameter-value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: inline-block;
|
display: block;
|
||||||
vertical-align: middle;
|
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
fill: none;
|
fill: none;
|
||||||
@@ -740,3 +766,47 @@ span.parameter-value {
|
|||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
&::after {
|
||||||
|
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTEwIDUwYTQwIDQwIDAgMDA4MCAwIDQwIDQyIDAgMDEtODAgMCIgZmlsbD0iI2IxMmQ1YyIgLz48L3N2Zz4=);
|
||||||
|
content: "";
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background-color: #eee;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 3px;
|
||||||
|
& > .progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #b12d5c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* To keep consistent style after changing from Quirks to Standards mode */
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013-2021 GenieACS Inc.
|
||||||
|
*
|
||||||
|
* This file is part of GenieACS.
|
||||||
|
*
|
||||||
|
* GenieACS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* GenieACS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import m, { ClosureComponent, Component, Vnode } from "mithril";
|
||||||
|
|
||||||
|
const elements: Map<string, Vnode> = new Map();
|
||||||
|
|
||||||
|
// Source: https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
|
||||||
|
function hash(str: string): number {
|
||||||
|
let res = 0;
|
||||||
|
for (let i = 0; i < str.length; ++i) {
|
||||||
|
const c = str.charCodeAt(i);
|
||||||
|
res = (res << 5) - res + c;
|
||||||
|
res |= 0;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDatalistId(options: string[]): string {
|
||||||
|
const id = "datalist" + options.reduce((acc, cur) => acc ^ hash(cur), 0);
|
||||||
|
if (!elements.has(id)) {
|
||||||
|
const n = m(
|
||||||
|
"datalist",
|
||||||
|
{ id },
|
||||||
|
options.map((o) => m("option", { value: o }))
|
||||||
|
);
|
||||||
|
elements.set(id, n);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const component: ClosureComponent = (): Component => {
|
||||||
|
return {
|
||||||
|
view: () => {
|
||||||
|
return [...elements.values()];
|
||||||
|
},
|
||||||
|
onupdate: () => {
|
||||||
|
for (const id of elements.keys()) {
|
||||||
|
const used = document.querySelector(`[list='${id}']`);
|
||||||
|
if (!used) elements.delete(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
+16
-4
@@ -42,13 +42,25 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
|
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
|
||||||
|
|
||||||
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]).value;
|
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]);
|
||||||
if (!dev.length) return "Loading";
|
if (!dev.value.length) {
|
||||||
|
if (!dev.fulfilling)
|
||||||
|
return m("p.error", `No such device ${vnode.attrs["deviceId"]}`);
|
||||||
|
return m(
|
||||||
|
"loading",
|
||||||
|
{ queries: [dev] },
|
||||||
|
m("div", { style: "height: 100px;" })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const conf = config.ui.device;
|
const conf = config.ui.device;
|
||||||
const cmps = [];
|
const cmps = [];
|
||||||
|
|
||||||
for (const c of Object.values(conf))
|
for (const c of Object.values(conf)) {
|
||||||
cmps.push(m.context({ device: dev[0] }, c["type"], c));
|
cmps.push(
|
||||||
|
m.context({ device: dev.value[0], deviceQuery: dev }, c["type"], c)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return [m("h1", vnode.attrs["deviceId"]), cmps];
|
return [m("h1", vnode.attrs["deviceId"]), cmps];
|
||||||
},
|
},
|
||||||
|
|||||||
+21
-23
@@ -154,14 +154,14 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
.deleteResource("devices", id)
|
.deleteResource("devices", id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", `${id}: Deleted`);
|
notifications.push("success", `${id}: Deleted`);
|
||||||
if (--counter === 0) store.fulfill(0, Date.now());
|
if (--counter === 0) store.setTimestamp(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", `${id}: ${err.message}`);
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
if (--counter === 0) store.fulfill(0, Date.now());
|
if (--counter === 0) store.setTimestamp(Date.now());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (--counter === 0) store.fulfill(0, Date.now());
|
if (--counter === 0) store.setTimestamp(Date.now());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Delete"
|
"Delete"
|
||||||
@@ -186,14 +186,14 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
.updateTags(id, { [tag]: true })
|
.updateTags(id, { [tag]: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", `${id}: Tags updated`);
|
notifications.push("success", `${id}: Tags updated`);
|
||||||
if (--counter === 0) store.fulfill(0, Date.now());
|
if (--counter === 0) store.setTimestamp(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", `${id}: ${err.message}`);
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
if (--counter === 0) store.fulfill(0, Date.now());
|
if (--counter === 0) store.setTimestamp(Date.now());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (--counter === 0) store.fulfill(0, Date.now());
|
if (--counter === 0) store.setTimestamp(Date.now());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Tag"
|
"Tag"
|
||||||
@@ -220,14 +220,14 @@ function renderActions(selected: Set<string>): Children {
|
|||||||
.updateTags(id, { [tag]: false })
|
.updateTags(id, { [tag]: false })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notifications.push("success", `${id}: Tags updated`);
|
notifications.push("success", `${id}: Tags updated`);
|
||||||
if (--counter === 0) store.fulfill(0, Date.now());
|
if (--counter === 0) store.setTimestamp(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", `${id}: ${err.message}`);
|
notifications.push("error", `${id}: ${err.message}`);
|
||||||
if (--counter === 0) store.fulfill(0, Date.now());
|
if (--counter === 0) store.setTimestamp(Date.now());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (--counter === 0) store.fulfill(0, Date.now());
|
if (--counter === 0) store.setTimestamp(Date.now());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Untag"
|
"Untag"
|
||||||
@@ -268,16 +268,13 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSortChange(sortedAttrs): void {
|
function onSortChange(sortedAttrs): void {
|
||||||
const _sort = Object.assign({}, sort);
|
const _sort = {};
|
||||||
for (const [index, direction] of Object.entries(sortedAttrs)) {
|
for (const index of sortedAttrs) {
|
||||||
const param = memoizedGetSortable(attributes[index].parameter);
|
const param = memoizedGetSortable(
|
||||||
if (param) {
|
attributes[Math.abs(index) - 1].parameter
|
||||||
// Changing the priority of columns
|
);
|
||||||
delete _sort[param];
|
_sort[param] = Math.sign(index);
|
||||||
_sort[param] = direction;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/devices", ops);
|
m.route.set("/devices", ops);
|
||||||
@@ -328,15 +325,16 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
if (window.authorizer.hasAccess("devices", 3))
|
if (window.authorizer.hasAccess("devices", 3))
|
||||||
attrs["actionsCallback"] = renderActions;
|
attrs["actionsCallback"] = renderActions;
|
||||||
|
|
||||||
const filterAttrs = {};
|
const filterAttrs = {
|
||||||
filterAttrs["resource"] = "devices";
|
resource: "devices",
|
||||||
filterAttrs["filter"] = vnode.attrs["filter"];
|
filter: vnode.attrs["filter"],
|
||||||
filterAttrs["onChange"] = onFilterChanged;
|
onChange: onFilterChanged,
|
||||||
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing devices"),
|
m("h1", "Listing devices"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(indexTableComponent, attrs),
|
m("loading", { queries: [devs, count] }, m(indexTableComponent, attrs)),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-10
@@ -76,13 +76,13 @@ function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
const type = task.parameterValues[0][2];
|
||||||
|
let value = task.parameterValues[0][1];
|
||||||
|
if (type === "xsd:dateTime" && typeof value === "number")
|
||||||
|
value = new Date(value).toJSON() || value;
|
||||||
input = m("input", {
|
input = m("input", {
|
||||||
type: ["xsd:dateTime", "xsd:int", "xsd:unsignedInt"].includes(
|
type: ["xsd:int", "xsd:unsignedInt"].includes(type) ? "number" : "text",
|
||||||
task.parameterValues[0][2]
|
value: value,
|
||||||
)
|
|
||||||
? "number"
|
|
||||||
: "text",
|
|
||||||
value: task.parameterValues[0][1],
|
|
||||||
oninput: (e) => {
|
oninput: (e) => {
|
||||||
e.redraw = false;
|
e.redraw = false;
|
||||||
task.parameterValues[0][1] = input.dom.value;
|
task.parameterValues[0][1] = input.dom.value;
|
||||||
@@ -121,12 +121,15 @@ function renderStagingDownload(task: StageTask): Children {
|
|||||||
if (productClass) productClass = decodeURIComponent(productClass);
|
if (productClass) productClass = decodeURIComponent(productClass);
|
||||||
|
|
||||||
const typesList = [
|
const typesList = [
|
||||||
|
...new Set([
|
||||||
"",
|
"",
|
||||||
"1 Firmware Upgrade Image",
|
"1 Firmware Upgrade Image",
|
||||||
"2 Web Content",
|
"2 Web Content",
|
||||||
"3 Vendor Configuration File",
|
"3 Vendor Configuration File",
|
||||||
"4 Tone File",
|
"4 Tone File",
|
||||||
"5 Ringer File",
|
"5 Ringer File",
|
||||||
|
...files.value.map((f) => f["metadata.fileType"]).filter((f) => f),
|
||||||
|
]),
|
||||||
].map((t) =>
|
].map((t) =>
|
||||||
m(
|
m(
|
||||||
"option",
|
"option",
|
||||||
@@ -355,7 +358,7 @@ function renderNotifications(notifs): Child[] {
|
|||||||
},
|
},
|
||||||
onbeforeremove: (vnode) => {
|
onbeforeremove: (vnode) => {
|
||||||
(vnode.dom as HTMLDivElement).style.opacity = "0";
|
(vnode.dom as HTMLDivElement).style.opacity = "0";
|
||||||
return new Promise((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve();
|
resolve();
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -385,7 +388,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
function repositionNotifications(): void {
|
function repositionNotifications(): void {
|
||||||
let top = 10;
|
let top = 10;
|
||||||
for (const c of notificationElements as VnodeDOM[]) {
|
for (const c of notificationElements as VnodeDOM[]) {
|
||||||
(c.dom as HTMLDivElement).style.top = `${top}`;
|
(c.dom as HTMLDivElement).style.top = `${top}px`;
|
||||||
top += (c.dom as HTMLDivElement).offsetHeight + 10;
|
top += (c.dom as HTMLDivElement).offsetHeight + 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,7 +408,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
for (const c of drawerElement.children)
|
for (const c of drawerElement.children)
|
||||||
height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight);
|
height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight);
|
||||||
}
|
}
|
||||||
drawerElement.dom.style.height = height;
|
drawerElement.dom.style.height = height + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stagingElements.length + queueElements.length) {
|
if (stagingElements.length + queueElements.length) {
|
||||||
@@ -465,7 +468,7 @@ const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
import * as notifications from "./notifications";
|
import * as notifications from "./notifications";
|
||||||
|
|
||||||
export let codeMirror;
|
export let codeMirror: typeof import("codemirror");
|
||||||
export let yaml;
|
export let yaml: typeof import("yaml");
|
||||||
|
|
||||||
let note;
|
let note;
|
||||||
|
|
||||||
@@ -43,11 +43,9 @@ export function loadCodeMirror(): Promise<void> {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const promises = [
|
const promises = [
|
||||||
import(/* webpackChunkName: "codemirror" */ "codemirror"),
|
import("codemirror"),
|
||||||
import(
|
import("codemirror/mode/javascript/javascript"),
|
||||||
/* webpackChunkName: "codemirror" */ "codemirror/mode/javascript/javascript"
|
import("codemirror/mode/yaml/yaml"),
|
||||||
),
|
|
||||||
import(/* webpackChunkName: "codemirror" */ "codemirror/mode/yaml/yaml"),
|
|
||||||
];
|
];
|
||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then((modules) => {
|
.then((modules) => {
|
||||||
@@ -65,7 +63,7 @@ export function loadYaml(): Promise<void> {
|
|||||||
if (yaml) return Promise.resolve();
|
if (yaml) return Promise.resolve();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
import(/* webpackChunkName: "yaml" */ "yaml")
|
import("yaml")
|
||||||
.then((module) => {
|
.then((module) => {
|
||||||
yaml = module;
|
yaml = module;
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
+21
-23
@@ -27,7 +27,7 @@ import * as notifications from "./notifications";
|
|||||||
import memoize from "../lib/common/memoize";
|
import memoize from "../lib/common/memoize";
|
||||||
import * as smartQuery from "./smart-query";
|
import * as smartQuery from "./smart-query";
|
||||||
import { map, parse, stringify } from "../lib/common/expression-parser";
|
import { map, parse, stringify } from "../lib/common/expression-parser";
|
||||||
import { loadYaml, yaml } from "./dynamic-loader";
|
import { stringify as yamlStringify } from "../lib/common/yaml";
|
||||||
|
|
||||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||||
|
|
||||||
@@ -76,13 +76,7 @@ export function init(
|
|||||||
|
|
||||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||||
return new Promise((resolve, reject) => {
|
return Promise.resolve({ filter, sort });
|
||||||
loadYaml()
|
|
||||||
.then(() => {
|
|
||||||
resolve({ filter, sort });
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ClosureComponent = (): Component => {
|
export const component: ClosureComponent = (): Component => {
|
||||||
@@ -113,13 +107,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSortChange(sortAttrs): void {
|
function onSortChange(sortAttrs): void {
|
||||||
const _sort = Object.assign({}, sort);
|
const _sort = {};
|
||||||
for (const [index, direction] of Object.entries(sortAttrs)) {
|
for (const index of sortAttrs)
|
||||||
// Changing the priority of columns
|
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||||
delete _sort[attributes[index].id];
|
|
||||||
_sort[attributes[index].id] = direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/faults", ops);
|
m.route.set("/faults", ops);
|
||||||
@@ -147,8 +137,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
return m("a", { href: deviceHref }, fault["device"]);
|
return m("a", { href: deviceHref }, fault["device"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attr.id === "message")
|
||||||
|
return m("long-text", { text: fault["message"] });
|
||||||
|
|
||||||
if (attr.id === "detail")
|
if (attr.id === "detail")
|
||||||
return m("long-text", { text: yaml.stringify(fault["detail"]) });
|
return m("long-text", { text: yamlStringify(fault["detail"]) });
|
||||||
|
|
||||||
if (attr.id === "timestamp")
|
if (attr.id === "timestamp")
|
||||||
return new Date(fault["timestamp"]).toLocaleString();
|
return new Date(fault["timestamp"]).toLocaleString();
|
||||||
@@ -190,11 +183,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${res.length} faults deleted`
|
`${res.length} faults deleted`
|
||||||
);
|
);
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -203,15 +196,20 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterAttrs = {};
|
const filterAttrs = {
|
||||||
filterAttrs["resource"] = "faults";
|
resource: "faults",
|
||||||
filterAttrs["filter"] = vnode.attrs["filter"];
|
filter: vnode.attrs["filter"],
|
||||||
filterAttrs["onChange"] = onFilterChanged;
|
onChange: onFilterChanged,
|
||||||
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing faults"),
|
m("h1", "Listing faults"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(indexTableComponent, attrs),
|
m(
|
||||||
|
"loading",
|
||||||
|
{ queries: [faults, count] },
|
||||||
|
m(indexTableComponent, attrs)
|
||||||
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+101
-83
@@ -45,7 +45,6 @@ const attributes: {
|
|||||||
{
|
{
|
||||||
id: "metadata.fileType",
|
id: "metadata.fileType",
|
||||||
label: "Type",
|
label: "Type",
|
||||||
type: "combo",
|
|
||||||
options: [
|
options: [
|
||||||
"1 Firmware Upgrade Image",
|
"1 Firmware Upgrade Image",
|
||||||
"2 Web Content",
|
"2 Web Content",
|
||||||
@@ -74,58 +73,32 @@ const unpackSmartQuery = memoize((query) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ValidationErrors {
|
function upload(
|
||||||
[prop: string]: string;
|
file: File,
|
||||||
}
|
headers: Record<string, string>,
|
||||||
|
abortSignal?: AbortSignal,
|
||||||
function putActionHandler(action, _object): Promise<ValidationErrors> {
|
progressListener?: (e: ProgressEvent) => void
|
||||||
return new Promise((resolve, reject) => {
|
): Promise<void> {
|
||||||
const object = Object.assign({}, _object);
|
headers = Object.assign(
|
||||||
if (action === "save") {
|
|
||||||
const file = object["file"] ? object["file"][0] : null;
|
|
||||||
delete object["file"];
|
|
||||||
|
|
||||||
if (!file) return void resolve({ file: "File not selected" });
|
|
||||||
|
|
||||||
const id = file.name;
|
|
||||||
|
|
||||||
store
|
|
||||||
.resourceExists("files", id)
|
|
||||||
.then((exists) => {
|
|
||||||
if (exists) {
|
|
||||||
store.fulfill(0, Date.now());
|
|
||||||
return void resolve({ file: "File already exists" });
|
|
||||||
}
|
|
||||||
const headers = Object.assign(
|
|
||||||
{
|
{
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
Accept: "application/octet-stream",
|
|
||||||
},
|
},
|
||||||
object
|
headers
|
||||||
);
|
);
|
||||||
|
return store.xhrRequest({
|
||||||
store
|
|
||||||
.xhrRequest({
|
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
url: `api/files/${encodeURIComponent(id)}`,
|
url: `api/files/${encodeURIComponent(file.name)}`,
|
||||||
serialize: (body) => body, // Identity function to prevent JSON.parse on blob data
|
serialize: (body) => body, // Identity function to prevent JSON.parse on blob data
|
||||||
body: file,
|
body: file,
|
||||||
})
|
config: (xhr) => {
|
||||||
.then(() => {
|
if (progressListener)
|
||||||
notifications.push(
|
xhr.upload.addEventListener("progress", progressListener);
|
||||||
"success",
|
if (abortSignal) {
|
||||||
`File ${exists ? "updated" : "created"}`
|
if (abortSignal.aborted) xhr.abort();
|
||||||
);
|
abortSignal.addEventListener("abort", () => xhr.abort());
|
||||||
store.fulfill(0, Date.now());
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
} else {
|
|
||||||
reject(new Error("Undefined action"));
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,13 +151,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
sortAttributes[i] = sort[attributes[i].id] || 0;
|
sortAttributes[i] = sort[attributes[i].id] || 0;
|
||||||
|
|
||||||
function onSortChange(sortAttrs): void {
|
function onSortChange(sortAttrs): void {
|
||||||
const _sort = Object.assign({}, sort);
|
const _sort = {};
|
||||||
for (const [index, direction] of Object.entries(sortAttrs)) {
|
for (const index of sortAttrs)
|
||||||
// Changing the priority of columns
|
_sort[attributes[Math.abs(index) - 1].id] = Math.sign(index);
|
||||||
delete _sort[attributes[index].id];
|
|
||||||
_sort[attributes[index].id] = direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ops = { sort: JSON.stringify(_sort) };
|
const ops = { sort: JSON.stringify(_sort) };
|
||||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||||
m.route.set("/admin/files", ops);
|
m.route.set("/admin/files", ops);
|
||||||
@@ -212,6 +181,9 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
attrs["sortAttributes"] = sortAttributes;
|
attrs["sortAttributes"] = sortAttributes;
|
||||||
attrs["onSortChange"] = onSortChange;
|
attrs["onSortChange"] = onSortChange;
|
||||||
attrs["downloadUrl"] = downloadUrl;
|
attrs["downloadUrl"] = downloadUrl;
|
||||||
|
attrs["recordActionsCallback"] = (file) => {
|
||||||
|
return [m("a", { href: "api/blob/files/" + file["_id"] }, "Download")];
|
||||||
|
};
|
||||||
|
|
||||||
if (window.authorizer.hasAccess("files", 3)) {
|
if (window.authorizer.hasAccess("files", 3)) {
|
||||||
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
attrs["actionsCallback"] = (selected: Set<string>): Children => {
|
||||||
@@ -222,42 +194,83 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
title: "Create new file",
|
title: "Create new file",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
let cb: () => Children = null;
|
let cb: () => Children = null;
|
||||||
|
const abortController = new AbortController();
|
||||||
|
let progress = -1;
|
||||||
const comp = m(
|
const comp = m(
|
||||||
putFormComponent,
|
putFormComponent,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
actionHandler: (action, object) => {
|
actionHandler: async (action, obj) => {
|
||||||
return new Promise((resolve) => {
|
if (action !== "save")
|
||||||
putActionHandler(action, object)
|
throw new Error("Undefined action");
|
||||||
.then((errors) => {
|
const file = obj["file"]?.[0];
|
||||||
const errorList = errors
|
|
||||||
? Object.values(errors)
|
// nginx strips out headers with dot, so replace with dash
|
||||||
: [];
|
const headers = {
|
||||||
if (errorList.length) {
|
"metadata-fileType": obj["metadata.fileType"] || "",
|
||||||
for (const err of errorList)
|
"metadata-oui": obj["metadata.oui"] || "",
|
||||||
notifications.push("error", err);
|
"metadata-productclass":
|
||||||
} else {
|
obj["metadata.productClass"] || "",
|
||||||
overlay.close(cb);
|
"metadata-version": obj["metadata.version"] || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
notifications.push("error", "File not selected");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
resolve();
|
|
||||||
})
|
if (await store.resourceExists("files", file.name)) {
|
||||||
.catch((err) => {
|
store.setTimestamp(Date.now());
|
||||||
|
notifications.push("error", "File already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressListener = (e: ProgressEvent): void => {
|
||||||
|
progress = e.loaded / e.total;
|
||||||
|
m.redraw();
|
||||||
|
};
|
||||||
|
|
||||||
|
progress = 0;
|
||||||
|
try {
|
||||||
|
await upload(
|
||||||
|
file,
|
||||||
|
headers,
|
||||||
|
abortController.signal,
|
||||||
|
progressListener
|
||||||
|
);
|
||||||
|
store.setTimestamp(Date.now());
|
||||||
|
notifications.push("success", "File created");
|
||||||
|
overlay.close(cb);
|
||||||
|
} catch (err) {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
resolve();
|
}
|
||||||
});
|
progress = -1;
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
formData
|
formData
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
cb = () => comp;
|
cb = () => {
|
||||||
overlay.open(
|
if (progress < 0) return [null, comp];
|
||||||
cb,
|
return [
|
||||||
() =>
|
m(
|
||||||
!comp.state["current"]["modified"] ||
|
"div.progress",
|
||||||
confirm("You have unsaved changes. Close anyway?")
|
m("div.progress-bar", {
|
||||||
);
|
style: `width: ${Math.trunc(progress * 100)}%`,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
comp,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
overlay.open(cb, () => {
|
||||||
|
if (
|
||||||
|
comp.state["current"]["modified"] &&
|
||||||
|
!confirm("You have unsaved changes. Close anyway?")
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
abortController.abort();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"New"
|
"New"
|
||||||
@@ -285,11 +298,11 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
"success",
|
"success",
|
||||||
`${res.length} files deleted`
|
`${res.length} files deleted`
|
||||||
);
|
);
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
notifications.push("error", err.message);
|
notifications.push("error", err.message);
|
||||||
store.fulfill(0, Date.now());
|
store.setTimestamp(Date.now());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -299,15 +312,20 @@ export const component: ClosureComponent = (): Component => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterAttrs = {};
|
const filterAttrs = {
|
||||||
filterAttrs["resource"] = "files";
|
resource: "files",
|
||||||
filterAttrs["filter"] = vnode.attrs["filter"];
|
filter: vnode.attrs["filter"],
|
||||||
filterAttrs["onChange"] = onFilterChanged;
|
onChange: onFilterChanged,
|
||||||
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("h1", "Listing files"),
|
m("h1", "Listing files"),
|
||||||
m(filterComponent, filterAttrs),
|
m(filterComponent, filterAttrs),
|
||||||
m(indexTableComponent, attrs),
|
m(
|
||||||
|
"loading",
|
||||||
|
{ queries: [files, count] },
|
||||||
|
m(indexTableComponent, attrs)
|
||||||
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user