Compare commits
215
Commits
v1.2.0-beta.0
...
v1.2.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
d29448e3a7 | ||
|
|
a91dbe5021 | ||
|
|
63cd8dd2de | ||
|
|
bded263adf | ||
|
|
fa1e40e370 | ||
|
|
30bea8ac57 | ||
|
|
d787f66e3f | ||
|
|
a9b0e3a1f7 | ||
|
|
bd55d6f512 | ||
|
|
e64001cddc | ||
|
|
60d8dcb682 | ||
|
|
e0d648ea3e | ||
|
|
6ba67c3fb6 | ||
|
|
04a7c08a20 | ||
|
|
92091b52a2 | ||
|
|
a43bba2405 | ||
|
|
c8a6eb4c72 | ||
|
|
36798cfb5f | ||
|
|
c52ff6f6ec | ||
|
|
dfab724515 | ||
|
|
4fd919ccfa | ||
|
|
62849e8081 | ||
|
|
9f4b421b59 | ||
|
|
44a2fe11ee | ||
|
|
91d7152ca7 | ||
|
|
3fccc1d8a4 | ||
|
|
5bb6f60c4d | ||
|
|
c2f002b95a | ||
|
|
c55a256d52 | ||
|
|
3fc0b1740d | ||
|
|
1956441d52 | ||
|
|
01e01dae69 | ||
|
|
f14e6ceb97 | ||
|
|
82124d471e | ||
|
|
9286bd6aa0 | ||
|
|
328bfa03e0 | ||
|
|
de65ca0921 | ||
|
|
50e9731920 | ||
|
|
97c817f199 | ||
|
|
58bb8fdb14 | ||
|
|
a6f9e3d0de | ||
|
|
0b60406b43 | ||
|
|
ad682d0b69 | ||
|
|
c6f4f9480c | ||
|
|
dd9cd01681 | ||
|
|
c17d2eae2f | ||
|
|
a8e770fea5 | ||
|
|
cfa989d346 | ||
|
|
b02c1df3b2 | ||
|
|
dfc4ab81aa | ||
|
|
e48829a04c | ||
|
|
e532f99625 | ||
|
|
2780e3970e | ||
|
|
04b19eb05f | ||
|
|
1760207217 | ||
|
|
495e7b0264 | ||
|
|
2411db6b36 | ||
|
|
c9eb5d1327 | ||
|
|
cce525cb07 | ||
|
|
49daf5adf2 | ||
|
|
19c5bb03ea | ||
|
|
ec44ccfc07 | ||
|
|
bf9517c652 | ||
|
|
6385f54888 | ||
|
|
fef4ddfbba | ||
|
|
36046a6fac | ||
|
|
2a85d768a5 | ||
|
|
6cbd59c6ec | ||
|
|
e3ef177842 | ||
|
|
00ccf99be6 | ||
|
|
03840ea7ea | ||
|
|
ac980116bd | ||
|
|
5a9e03db2c | ||
|
|
c2c24f5c24 | ||
|
|
e516d5338c | ||
|
|
41f3987db4 | ||
|
|
65bf6d7d6e | ||
|
|
603f5c79ac | ||
|
|
e80109de93 | ||
|
|
ce955e34c1 | ||
|
|
1e6bb5f450 | ||
|
|
f301d70339 | ||
|
|
feb3d7f244 | ||
|
|
dd907a67b0 | ||
|
|
37d8222479 | ||
|
|
a915b581cc | ||
|
|
add4c84132 | ||
|
|
75ff56f3ae | ||
|
|
f786be7493 | ||
|
|
07a5c96e2a | ||
|
|
612563f171 | ||
|
|
27fb235d20 | ||
|
|
c66739c420 | ||
|
|
140acaa491 | ||
|
|
765f0be7bc | ||
|
|
ab27ebfaef | ||
|
|
f135be403c | ||
|
|
c07f57af1f | ||
|
|
df5e4cf454 | ||
|
|
85b5e12c94 | ||
|
|
8533ee7f2e | ||
|
|
77563fc85e | ||
|
|
0e02b7c7ea | ||
|
|
f966194e22 | ||
|
|
306015f022 | ||
|
|
20857549d0 | ||
|
|
e31d74bd45 | ||
|
|
5c6d85025c | ||
|
|
209c0f7620 | ||
|
|
559aa0fd36 | ||
|
|
ed0aa8ae7a | ||
|
|
4bcee572f1 | ||
|
|
3d38de74d7 | ||
|
|
5b5346f858 | ||
|
|
f57bcae70c | ||
|
|
1c9aa493c6 | ||
|
|
6f0e1d66ce | ||
|
|
b5efe6c578 | ||
|
|
fd34dd8839 | ||
|
|
17889cf41c | ||
|
|
c2e8585801 | ||
|
|
46e009afab | ||
|
|
78eec1b5ca | ||
|
|
59cd930b6c | ||
|
|
db3736e60b | ||
|
|
d10fa2e073 |
No files matched your search
@@ -1,2 +0,0 @@
|
||||
!/bin/.*.ts
|
||||
!/tools/.*.ts
|
||||
+16
-5
@@ -8,19 +8,30 @@
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", "prettier/@typescript-eslint"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"no-shadow": ["error", { "allow": ["err"] }],
|
||||
"handle-callback-err": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"curly": ["error", "multi-or-nest", "consistent"],
|
||||
"no-buffer-constructor": "error",
|
||||
"prefer-const": ["error", {"destructuring": "all"}],
|
||||
"eqeqeq": ["error", "always", {"null": "ignore"}],
|
||||
"prefer-const": ["error", { "destructuring": "all" }],
|
||||
"eqeqeq": ["error", "always", { "null": "ignore" }],
|
||||
"no-return-await": "error",
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/no-use-before-define": ["error", { "functions": false }],
|
||||
"@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }],
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
"error",
|
||||
{ "functions": false }
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"error",
|
||||
{ "allowExpressions": true }
|
||||
],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"require-atomic-updates": 0,
|
||||
"no-prototype-builtins": 0
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
*~
|
||||
node_modules
|
||||
dist
|
||||
bin/.*.ts
|
||||
tools/.*.ts
|
||||
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
|
||||
+186
-52
@@ -1,48 +1,180 @@
|
||||
# Change Log
|
||||
|
||||
## 1.2.7 (2021-09-18)
|
||||
|
||||
- Fix regression causing frequent invalid session errors.
|
||||
- Fix regression breaking digest authentication in connection requests.
|
||||
|
||||
## 1.2.6 (2021-09-16)
|
||||
|
||||
- New config option `cwmp.downloadSuccessOnTimeout` to enable a workaround for
|
||||
CPEs that neglect to send a TransferComplete message after a successful
|
||||
download.
|
||||
- Display a progress bar when uploading new files.
|
||||
- Default to dual stack interface binding (i.e. `::` instead of `0.0.0.0`).
|
||||
Unless the binding interface is explicitly set, this will cause IPv4 addresses
|
||||
in the logs to be displayed as IPv4-mapped IPv6 addresses (e.g.
|
||||
`::ffff:127.0.0.1` instead of `127.0.0.1`).
|
||||
- Detect and correct for client side (browser) clock skew that would otherwise
|
||||
alter the numbers in the pie charts.
|
||||
- Various improvements relating to dealing with buggy TR-069 client
|
||||
implementations.
|
||||
- Fix a bug causing "lock expired" exceptions when a CWMP session remains open
|
||||
for a very long time due to slow clients.
|
||||
- Fix metadata of uploaded files going missing due to nginx stripping away what
|
||||
it considers to be invalid headers. The nginx directive
|
||||
`ignore_invalid_headers` is no longer required.
|
||||
- Fix crash when a CPE is assigned a tag containing a dot.
|
||||
- Fix bug preventing the user from closing the preset pop-up after being
|
||||
presented with unsupported preset message.
|
||||
- Fix exceptions raised from ext scripts manifesting as timeout faults.
|
||||
- Fix download getting triggered repeatedly when the value passed to `Download`
|
||||
parameter is greater than the current timestamp.
|
||||
- Fix crash when passing invalid attributes to `declare`.
|
||||
- Fix crash in NBI when pushing tasks to non-existing devices.
|
||||
- Fix crash in NBI when passing invalid JSON to various API endpoints.
|
||||
- Fix crash when the output from `ping` command cannot be parsed in some rare
|
||||
edge cases.
|
||||
- A number of other fixes and stability improvements.
|
||||
|
||||
## 1.2.5 (2021-03-12)
|
||||
|
||||
- Support specifying custom types when uploading files.
|
||||
- Fix JS compatibility issue with Safari browser.
|
||||
|
||||
## 1.2.4 (2021-02-24)
|
||||
|
||||
- The data model state of a CPE is no longer forgotten after unsuccessful
|
||||
session termination (e.g. timeout). This addresses a number of undesired side
|
||||
effects that arise when a CPE does not terminate the session properly.
|
||||
- Executing tasks that take a long time to complete (e.g. refreshing the entire
|
||||
data model) no longer shows a timeout error while the task is still being
|
||||
processed.
|
||||
- New function `ROUND()` available to expressions. It works similar to the
|
||||
function by the same name in SQLite and PostgreSQL.
|
||||
- Log access events for `genieacs-nbi` service.
|
||||
- Pipe stdout/stderr from extension scripts to the `genieacs-cwmp` process log.
|
||||
- Parameter values of type `xsd:dateTime` are now displayed in the UI and CSV
|
||||
downloads as a date string rather than a numeric value.
|
||||
- Add file download link in the files listing page.
|
||||
- Display spinner loading animation throughout the UI.
|
||||
- Display GenieACS version and build number underneath the logo.
|
||||
- New option to specify how many parameters are displayed at a time in the
|
||||
all-parameters component. Simply set `limit` property in the component config.
|
||||
- Reduce overly strong Brotli compression level which was causing significant
|
||||
page load slowdown when Brotli is used.
|
||||
- Retire dump-data-model tool. `genieacs-sim` can now use a CSV file as its data
|
||||
model.
|
||||
- Reduce the number of concurrent database connections from each process.
|
||||
- Remove dependency on 'hostInfo' MongoDB command which is a privileged action.
|
||||
It is now possible to use shared MongoDB instances with limited privileged.
|
||||
- Fix bug in NBI where querying files returns 404 error.
|
||||
- Fix ping not working for devices with an IPv6 address.
|
||||
- Fix an elusive memory leak in `genieacs-fs` that slowly eats up memory and can
|
||||
go unnoticed for long periods of time.
|
||||
- Fix a rare edge case where a `declare()` call to set a parameter value may not
|
||||
work as intended if the parameter was originally received as part of the
|
||||
Inform message.
|
||||
- A number of other fixes and stability improvements.
|
||||
|
||||
## 1.2.3 (2020-10-26)
|
||||
|
||||
- New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly
|
||||
report writable parameters as non-writable. When set to true, the scripts will
|
||||
no longer respect the 'writable' attribute of the CPE parameters and will send
|
||||
a SetParamteerValues, AddObject, or DeleteObject request anyway.
|
||||
- Tags no longer restrict what characters are allowed. Any character other than
|
||||
alphanumeric characters, hyphen, or underscore is now encoded in the data
|
||||
model (i.e. Tags.\<tag>) using its hex value preceded by "0x".
|
||||
- Ask for a confirmation before closing a pop-up dialog with unsaved changes.
|
||||
- Better XML validation to avoid crashes caused by invalid CPE requests.
|
||||
- Fix confusing 404 error message when the user attempts to modify a resource
|
||||
when they don't have the necessary permissions.
|
||||
- Fix a rare issue where genieacs-cwmp stops accepting new connections after
|
||||
running for a few weeks.
|
||||
- Fix exception when IS NULL operator is used in certain situations.
|
||||
|
||||
## 1.2.2 (2020-10-03)
|
||||
|
||||
- Added button to push files to selected devices from device listing page.
|
||||
- A few minor UI improvements.
|
||||
- Fix exception that can happen and persist after a Download request.
|
||||
- Fix validation bug preventing running refreshObject task on data model root.
|
||||
- Fix invalid arguments fault in refresh preset configuration when upgrading
|
||||
from v1.1.
|
||||
|
||||
## 1.2.1 (2020-09-08)
|
||||
|
||||
- Fix bug causing faults to not be displayed in the UI.
|
||||
- Fix bug where deleting objects does not get reflected immediately in the UI.
|
||||
- Improve conversion between filters written in the expression format and
|
||||
MongoDB queries. There should now be fewer edge cases where the two are not
|
||||
equisatisfiable.
|
||||
|
||||
## 1.2.0 (2020-09-01)
|
||||
|
||||
- Support GetParameterAttributes and SetParameterAttributes TR-069 methods.
|
||||
- Support CASE statement and COALESCE function in expressions.
|
||||
- Provision arguments can now be a list of expressions that are dynamically
|
||||
evaluated.
|
||||
- Support Forwarded HTTP header to display in the logs the correct IP of CPEs
|
||||
behind a reverse proxy. Must be configured using FORWARDED_HEADER option.
|
||||
- Config expressions can now access all available device parameters, not only
|
||||
serial number, product class, and OUI.
|
||||
- Use relative URLs throughout the UI to allow serving from a subdirectory using
|
||||
a reverse proxy.
|
||||
- Make Date.parse() and Date.UTC() available to provision scripts.
|
||||
- libxmljs has been entirely removed in favor of our bespoke XML parser.
|
||||
- Removed the config option CWMP_KEEP_ALIVE_TIMEOUT. SESSION_TIMEOUT is now used
|
||||
to determine the TCP connection timeout.
|
||||
- The all-parameters component now limits the number of parameters displayed for
|
||||
better performance.
|
||||
- The process genieacs-cwmp is now much less likely to throw exceptions as a
|
||||
result of invalid requests from CPE.
|
||||
- A large number of bug fixes and stability improvements.
|
||||
|
||||
## 1.2.0-beta.0 (2019-07-30)
|
||||
|
||||
- A brand new UI superseding genieacs-gui.
|
||||
- New initialization wizard on first run.
|
||||
- New expression/query language used in search filters and preset
|
||||
preconditions.
|
||||
- New expression/query language used in search filters and preset preconditions.
|
||||
- CPE -> ACS authentication is now supported.
|
||||
- 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.
|
||||
- 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
|
||||
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.
|
||||
- The API now validates the structure of task objects before saving.
|
||||
- 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
|
||||
v10.
|
||||
old parser by enabling the config option XML_LIBXMLJS. Requires Node.js v11 or
|
||||
v10.
|
||||
- Performance optimizations. While performance has improved for the majority of
|
||||
use cases, there may be situations where performance has degraded. It's
|
||||
recommended to revisit your hardware requirements.
|
||||
use cases, there may be situations where performance has degraded. It's
|
||||
recommended to revisit your hardware requirements.
|
||||
- Connection request authentication no longer uses 'auth.js' file. Instead, the
|
||||
connection request authentication behavior can now be customized using an
|
||||
'expression'.
|
||||
connection request authentication behavior can now be customized using an
|
||||
'expression'.
|
||||
- The config file (config.json) has been deprecated. System configuration (e.g.
|
||||
listen ports, worker count) are now recommended to be passed as environment.
|
||||
variables. Other general configuration options are stored in the database so as
|
||||
to not require service restart for changes to take effect.
|
||||
listen ports, worker count) are now recommended to be passed as environment.
|
||||
variables. Other general configuration options are stored in the database so
|
||||
as to not require service restart for changes to take effect.
|
||||
- Optional redis dependency has been removed completely.
|
||||
- Tags now allow only alphanumeric characters and underscore.
|
||||
- Supported versions of NodeJs and MongoDB are 10.x and up and 2.6 and up
|
||||
respectively.
|
||||
- Supported versions of Node.js and MongoDB are 10.x and up and 2.6 and up
|
||||
respectively.
|
||||
|
||||
## 1.1.3 (2018-10-23)
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
of returning incorrect results.
|
||||
of returning incorrect results.
|
||||
- When a task expires, any associated fault is also deleted.
|
||||
- API now accepts 'timeout' argument when posting a task.
|
||||
- A number of stability fixes.
|
||||
@@ -52,66 +184,68 @@ of returning incorrect results.
|
||||
- A large number of bug fixes as well as stability and performance improvements.
|
||||
- Three security vulnerabilities disclosed by Maximilian Hils have been patched.
|
||||
- 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
|
||||
values.
|
||||
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
|
||||
a warning message.
|
||||
a warning message.
|
||||
- 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}, {declare values}, {current timestamps}, {current values}.
|
||||
'TIMESTAMPS' and 'VALUES' variables. The content of the args array is:
|
||||
{declare timestamps}, {declare values}, {current timestamps}, {current
|
||||
values}.
|
||||
- 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
|
||||
attributes.
|
||||
attributes.
|
||||
- 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
|
||||
provisioning scripts.
|
||||
provisioning scripts.
|
||||
- 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
|
||||
temporarily when under abnormal load.
|
||||
temporarily when under abnormal load.
|
||||
- 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
|
||||
milliseconds). This can be used to ensure a group of parameters are all
|
||||
refreshed at the same time intervals.
|
||||
milliseconds). This can be used to ensure a group of parameters are all
|
||||
refreshed at the same time intervals.
|
||||
- Only the non-default configuration options are now logged at process start.
|
||||
- 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).
|
||||
- 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)
|
||||
|
||||
- Avoid crashing when connection request credentials are missing.
|
||||
- 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.
|
||||
- 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 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)
|
||||
|
||||
- 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
|
||||
a custom script.
|
||||
a custom script.
|
||||
- Extensions are sandboxed Node.js scripts that are accessible from provision
|
||||
and virtual parameter scripts to facilitate integration with external entities.
|
||||
- Support for UDP/STUN based connection requests for reaching devices behind
|
||||
NAT (TR-069 Annex G).
|
||||
and virtual parameter scripts to facilitate integration with external
|
||||
entities.
|
||||
- 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 tied to specific device events (e.g. boot).
|
||||
- Presets precondition queries no longer support "$or" or other MongoDB logical
|
||||
operators.
|
||||
- Presets precondition queries no longer support "\$or" or other MongoDB logical
|
||||
operators.
|
||||
- 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 in that channel.
|
||||
presets in that channel.
|
||||
- New API CRUD functions for provisions, virtual parameters, and faults.
|
||||
- New config options for XML output.
|
||||
- API responses now include "GenieACS-Version" header.
|
||||
@@ -120,19 +254,19 @@ presets in that channel.
|
||||
- Supported Node.js versions are 6.x and 7.x.
|
||||
- Supported MongoDB versions are 2.6 through 3.4.
|
||||
- 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
|
||||
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
|
||||
access log (stdout). Also added config options to dump logs to files rather than
|
||||
standard streams.
|
||||
access log (stdout). Also added config options to dump logs to files rather
|
||||
than standard streams.
|
||||
- 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
|
||||
supports an optional callback argument.
|
||||
data model if available. config/auth.js is still supported as a fallback and
|
||||
now supports an optional callback argument.
|
||||
- Custom commands have been removed. Use virtual parameters and/or extensions.
|
||||
- Aliases and value normalizers (config/parameters.json) have been removed. Use
|
||||
virtual parameters.
|
||||
virtual parameters.
|
||||
- The API /devices/<device_id>/preset has been removed.
|
||||
- Rarely used RequestDownload method no longer supported.
|
||||
- The TR-069 client simulator has moved to its own repo at
|
||||
https://github.com/zaidka/genieacs-sim
|
||||
https://github.com/zaidka/genieacs-sim
|
||||
+17
-17
@@ -1,22 +1,21 @@
|
||||
# Have a question?
|
||||
|
||||
Please ask in the [Users mailing list][mailing-lists] instead of opening a
|
||||
Github Issue. The Github issue tracker is strictly for bug reports.
|
||||
Please ask in the [forum] instead of opening a Github Issue. The Github issue
|
||||
tracker is strictly for bug reports.
|
||||
|
||||
# Issues
|
||||
|
||||
We use GitHub issues to track bugs. Please ensure your bug description is
|
||||
clear and has sufficient instructions to be able to reproduce the issue.
|
||||
We use GitHub issues to track bugs. Please ensure your bug description is clear
|
||||
and has sufficient instructions to be able to reproduce the issue.
|
||||
|
||||
If you face interoperability issues with your device, more often than not it's
|
||||
a device related issue. Please consult the [Users mailing list][mailing-lists]
|
||||
before submitting an Issue.
|
||||
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.
|
||||
|
||||
# Pull Requests
|
||||
|
||||
Pull requests are welcome. If you have a fix for a bug, go ahead and create a
|
||||
pull request. For other kinds of enhancements, please discuss in the [Users
|
||||
mailing list][mailing-lists] first.
|
||||
pull request. For other kinds of enhancements, please discuss in the [forum]
|
||||
first.
|
||||
|
||||
Please follow the guidelines [here][commit-guidelines] when committing your
|
||||
changes.
|
||||
@@ -27,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-
|
||||
exclusive, no-charge, royalty-free, irrevocable license to reproduce, prepare
|
||||
derivative works of, publicly display, publicly perform, sublicense, and
|
||||
distribute your contributions and such derivative works under the AGPLv3
|
||||
license or any other license terms, including, but not limited to, proprietary
|
||||
or commercial license terms.
|
||||
distribute your contributions and such derivative works under the AGPLv3 license
|
||||
or any other license terms, including, but not limited to, proprietary or
|
||||
commercial license terms.
|
||||
|
||||
You confirm that you own or have rights to distribute and sublicense the
|
||||
source code contained therein, and that your content does not infringe upon
|
||||
the intellectual property rights of a third party.
|
||||
You confirm that you own or have rights to distribute and sublicense the source
|
||||
code contained therein, and that your content does not infringe upon the
|
||||
intellectual property rights of a third party.
|
||||
|
||||
[commit-guidelines]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
||||
[mailing-lists]: http://lists.genieacs.com
|
||||
[commit-guidelines]:
|
||||
http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
||||
[forum]: https://forum.genieacs.com
|
||||
@@ -8,35 +8,30 @@ millions of concurrent devices.
|
||||
|
||||
## Quick Start
|
||||
|
||||
*Important: This is a pre-release branch. Use
|
||||
[v1.1](https://github.com/genieacs/genieacs/tree/v1.1) for production
|
||||
deployments.*
|
||||
|
||||
Install [Node.js](http://nodejs.org/) and [MongoDB](http://www.mongodb.org/).
|
||||
Refer to their corresponding documentation for installation instructions. The
|
||||
supported versions are:
|
||||
|
||||
- Node.js: 10.x through 12.x
|
||||
- MongoDB: 2.6 through 4.1
|
||||
- Node.js: 12.3+
|
||||
- MongoDB: 3.6+
|
||||
|
||||
Clone this repo or download the source archive then *cd* into the source
|
||||
directory and install the required dependencies:
|
||||
Install GenieACS from NPM:
|
||||
|
||||
sudo npm install -g genieacs
|
||||
|
||||
To build from source instead, clone this repo or download the source archive
|
||||
then _cd_ into the source directory then run:
|
||||
|
||||
npm install
|
||||
|
||||
Now build:
|
||||
|
||||
npm run build
|
||||
|
||||
Finally, run the following services from the build that's generated under
|
||||
'dist' directory:
|
||||
Finally, run the following services (found under `./dist/bin/` if building from
|
||||
source):
|
||||
|
||||
### genieacs-cwmp
|
||||
|
||||
This is the service that the CPEs will communicate with. It listens on port
|
||||
7547 by default. Configure the ACS URL in your devices accordingly.
|
||||
|
||||
./dist/bin/genieacs-cwmp
|
||||
This is the service that the CPEs will communicate with. It listens on port 7547
|
||||
by default. Configure the ACS URL in your devices accordingly.
|
||||
|
||||
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.
|
||||
@@ -47,40 +42,35 @@ This is the northbound interface module. It exposes a REST API on port 7557 by
|
||||
default. This one is only required if you have an external system integrating
|
||||
with GenieACS using this API.
|
||||
|
||||
./dist/bin/genieacs-nbi
|
||||
|
||||
### genieacs-fs
|
||||
|
||||
This is the file server from which the CPEs will download firmware images and
|
||||
such.
|
||||
|
||||
./dist/bin/genieacs-fs
|
||||
such. It listens on port 7567 by default.
|
||||
|
||||
### genieacs-ui
|
||||
|
||||
This serves the web based user interface. It listens on port 3000 by default.
|
||||
You must pass _--ui-jwt-secret_ argument to supply the secret key used for
|
||||
signing browser cookies:
|
||||
|
||||
./dist/bin/genieacs-ui --ui-jwt-secret secret
|
||||
|
||||
The argument *--ui-jwt-secret* supplies the key used for signing browser
|
||||
cookies.
|
||||
genieacs-ui --ui-jwt-secret secret
|
||||
|
||||
The UI has plenty of configuration options. When you open GenieACS's UI in a
|
||||
browser you'll be greeted with a database initialization wizard to help you
|
||||
populate some initial configuration.
|
||||
|
||||
Visit [docs.genieacs.com](https://docs.genieacs.com) for more documentation and
|
||||
a complete installation guide for production deployments.
|
||||
|
||||
## Support
|
||||
|
||||
The [forum](https://forum.genieacs.com) is a good place to get guidance and
|
||||
help from the community. Head on over and join the conversation! In addition,
|
||||
the [wiki](https://github.com/genieacs/genieacs/wiki) provides useful
|
||||
documentation and tips from GenieACS users.
|
||||
The [forum](https://forum.genieacs.com) is a good place to get guidance and help
|
||||
from the community. Head on over and join the conversation!
|
||||
|
||||
For commercial support options and professional services, please visit
|
||||
For commercial support options, please visit
|
||||
[genieacs.com](https://genieacs.com/support/).
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013-2019 GenieACS Inc. GenieACS is released under the [AGPLv3
|
||||
license
|
||||
terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
|
||||
Copyright 2013-2021 GenieACS Inc. GenieACS is released under the
|
||||
[AGPLv3 license terms](https://raw.githubusercontent.com/genieacs/genieacs/master/LICENSE).
|
||||
+2
-4
@@ -1,6 +1,4 @@
|
||||
export default {
|
||||
"compileEnhancements": false,
|
||||
"extensions": ["ts"],
|
||||
"require": ["esm", "ts-node/register/transpile-only"]
|
||||
extensions: ["ts"],
|
||||
require: ["esm", "ts-node/register/transpile-only"],
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||
|
||||
/**
|
||||
* Copyright 2013-2019 GenieACS Inc.
|
||||
*
|
||||
@@ -26,34 +24,35 @@ import * as server from "../lib/server";
|
||||
import * as cwmp from "../lib/cwmp";
|
||||
import * as db from "../lib/db";
|
||||
import * as extensions from "../lib/extensions";
|
||||
import * as cache from "../lib/cache";
|
||||
import { version as VERSION } from "../package.json";
|
||||
|
||||
logger.init("cwmp", VERSION);
|
||||
|
||||
const SERVICE_ADDRESS = config.get("CWMP_INTERFACE");
|
||||
const SERVICE_PORT = config.get("CWMP_PORT");
|
||||
const SERVICE_ADDRESS = config.get("CWMP_INTERFACE") as string;
|
||||
const SERVICE_PORT = config.get("CWMP_PORT") as number;
|
||||
|
||||
function exitWorkerGracefully(): void {
|
||||
db.disconnect();
|
||||
cache.disconnect();
|
||||
extensions.killAll();
|
||||
cluster.worker.disconnect();
|
||||
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||
Promise.all([
|
||||
db.disconnect(),
|
||||
extensions.killAll(),
|
||||
cluster.worker.disconnect(),
|
||||
]).catch(exitWorkerUngracefully);
|
||||
}
|
||||
|
||||
function exitWorkerUngracefully(): void {
|
||||
extensions.killAll().then(() => {
|
||||
extensions.killAll().finally(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
if (!cluster.worker) {
|
||||
const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES");
|
||||
const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES") as number;
|
||||
|
||||
logger.info({
|
||||
message: `genieacs-cwmp starting`,
|
||||
pid: process.pid,
|
||||
version: VERSION
|
||||
version: VERSION,
|
||||
});
|
||||
|
||||
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||
@@ -61,7 +60,7 @@ if (!cluster.worker) {
|
||||
process.on("SIGINT", () => {
|
||||
logger.info({
|
||||
message: "Received signal SIGINT, exiting",
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
|
||||
cluster.stop();
|
||||
@@ -70,60 +69,54 @@ if (!cluster.worker) {
|
||||
process.on("SIGTERM", () => {
|
||||
logger.info({
|
||||
message: "Received signal SIGTERM, exiting",
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
|
||||
cluster.stop();
|
||||
});
|
||||
} else {
|
||||
const ssl = {
|
||||
key: config.get("CWMP_SSL_KEY"),
|
||||
cert: config.get("CWMP_SSL_CERT")
|
||||
const key = config.get("CWMP_SSL_KEY") as string;
|
||||
const cert = config.get("CWMP_SSL_CERT") as string;
|
||||
|
||||
const options = {
|
||||
port: SERVICE_PORT,
|
||||
host: SERVICE_ADDRESS,
|
||||
ssl: key && cert ? { key, cert } : null,
|
||||
onConnection: cwmp.onConnection,
|
||||
timeout: 30000,
|
||||
keepAliveTimeout: 0,
|
||||
};
|
||||
|
||||
const keepAliveTimeout = config.get("CWMP_KEEP_ALIVE_TIMEOUT") as number;
|
||||
|
||||
process.on("uncaughtException", err => {
|
||||
process.on("uncaughtException", (err) => {
|
||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||
logger.error({
|
||||
message: "Uncaught exception",
|
||||
exception: err,
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
Promise.all([db.connect(), cache.connect()])
|
||||
const initPromise = db
|
||||
.connect()
|
||||
.then(() => {
|
||||
server.start(
|
||||
SERVICE_PORT,
|
||||
SERVICE_ADDRESS,
|
||||
ssl,
|
||||
cwmp.listener,
|
||||
cwmp.onConnection,
|
||||
keepAliveTimeout
|
||||
);
|
||||
server.start(options, cwmp.listener);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
initPromise.finally(() => {
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
initPromise.finally(() => {
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||
|
||||
/**
|
||||
* Copyright 2013-2019 GenieACS Inc.
|
||||
*
|
||||
@@ -35,8 +33,8 @@ function errorToFault(err: Error): Fault {
|
||||
message: err.message,
|
||||
detail: {
|
||||
name: err.name,
|
||||
message: err.message
|
||||
}
|
||||
message: err.message,
|
||||
},
|
||||
};
|
||||
|
||||
if (err.stack) {
|
||||
@@ -56,16 +54,16 @@ function errorToFault(err: Error): Fault {
|
||||
return fault;
|
||||
}
|
||||
|
||||
process.on("uncaughtException", err => {
|
||||
process.on("uncaughtException", (err) => {
|
||||
const fault = errorToFault(err);
|
||||
jobs.forEach(jobId => {
|
||||
jobs.forEach((jobId) => {
|
||||
process.send([jobId, fault, null]);
|
||||
});
|
||||
jobs.clear();
|
||||
process.disconnect();
|
||||
});
|
||||
|
||||
process.on("message", message => {
|
||||
process.on("message", (message) => {
|
||||
jobs.add(message[0]);
|
||||
|
||||
if (!script) {
|
||||
@@ -79,7 +77,7 @@ process.on("message", message => {
|
||||
if (!script[funcName]) {
|
||||
const fault = {
|
||||
code: "ext",
|
||||
message: `No such function '${funcName}' in extension '${fileName}'`
|
||||
message: `No such function '${funcName}' in extension '${fileName}'`,
|
||||
};
|
||||
process.send([message[0], fault, null]);
|
||||
return;
|
||||
@@ -93,4 +91,6 @@ process.on("message", message => {
|
||||
});
|
||||
|
||||
// Ignore SIGINT
|
||||
process.on("SIGINT", () => {});
|
||||
process.on("SIGINT", () => {
|
||||
// Ignore
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||
|
||||
/**
|
||||
* Copyright 2013-2019 GenieACS Inc.
|
||||
*
|
||||
@@ -25,18 +23,18 @@ import * as cluster from "../lib/cluster";
|
||||
import * as server from "../lib/server";
|
||||
import { listener } from "../lib/fs";
|
||||
import * as db from "../lib/db";
|
||||
import * as cache from "../lib/cache";
|
||||
import { version as VERSION } from "../package.json";
|
||||
|
||||
logger.init("fs", VERSION);
|
||||
|
||||
const SERVICE_ADDRESS = config.get("FS_INTERFACE");
|
||||
const SERVICE_PORT = config.get("FS_PORT");
|
||||
const SERVICE_ADDRESS = config.get("FS_INTERFACE") as string;
|
||||
const SERVICE_PORT = config.get("FS_PORT") as number;
|
||||
|
||||
function exitWorkerGracefully(): void {
|
||||
db.disconnect();
|
||||
cache.disconnect();
|
||||
cluster.worker.disconnect();
|
||||
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||
Promise.all([db.disconnect(), cluster.worker.disconnect()]).catch(
|
||||
exitWorkerUngracefully
|
||||
);
|
||||
}
|
||||
|
||||
function exitWorkerUngracefully(): void {
|
||||
@@ -44,12 +42,12 @@ function exitWorkerUngracefully(): void {
|
||||
}
|
||||
|
||||
if (!cluster.worker) {
|
||||
const WORKER_COUNT = config.get("FS_WORKER_PROCESSES");
|
||||
const WORKER_COUNT = config.get("FS_WORKER_PROCESSES") as number;
|
||||
|
||||
logger.info({
|
||||
message: `genieacs-fs starting`,
|
||||
pid: process.pid,
|
||||
version: VERSION
|
||||
version: VERSION,
|
||||
});
|
||||
|
||||
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||
@@ -57,7 +55,7 @@ if (!cluster.worker) {
|
||||
process.on("SIGINT", () => {
|
||||
logger.info({
|
||||
message: "Received signal SIGINT, exiting",
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
|
||||
cluster.stop();
|
||||
@@ -66,51 +64,61 @@ if (!cluster.worker) {
|
||||
process.on("SIGTERM", () => {
|
||||
logger.info({
|
||||
message: "Received signal SIGTERM, exiting",
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
|
||||
cluster.stop();
|
||||
});
|
||||
} else {
|
||||
const ssl = {
|
||||
key: config.get("FS_SSL_KEY"),
|
||||
cert: config.get("FS_SSL_CERT")
|
||||
const key = config.get("FS_SSL_KEY") as string;
|
||||
const cert = config.get("FS_SSL_CERT") as string;
|
||||
const options = {
|
||||
port: SERVICE_PORT,
|
||||
host: SERVICE_ADDRESS,
|
||||
ssl: key && cert ? { key, cert } : null,
|
||||
timeout: 30000,
|
||||
};
|
||||
|
||||
process.on("uncaughtException", err => {
|
||||
let stopping = false;
|
||||
|
||||
process.on("uncaughtException", (err) => {
|
||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||
logger.error({
|
||||
message: "Uncaught exception",
|
||||
exception: err,
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
stopping = true;
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
});
|
||||
const _listener = (req, res): void => {
|
||||
if (stopping) res.setHeader("Connection", "close");
|
||||
void listener(req, res);
|
||||
};
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
Promise.all([db.connect(), cache.connect()])
|
||||
const initPromise = db
|
||||
.connect()
|
||||
.then(() => {
|
||||
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
||||
server.start(options, _listener);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
stopping = true;
|
||||
initPromise.finally(() => {
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
stopping = true;
|
||||
initPromise.finally(() => {
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||
|
||||
/**
|
||||
* Copyright 2013-2019 GenieACS Inc.
|
||||
*
|
||||
@@ -26,34 +24,35 @@ import * as server from "../lib/server";
|
||||
import { listener } from "../lib/nbi";
|
||||
import * as db from "../lib/db";
|
||||
import * as extensions from "../lib/extensions";
|
||||
import * as cache from "../lib/cache";
|
||||
import { version as VERSION } from "../package.json";
|
||||
|
||||
logger.init("nbi", VERSION);
|
||||
|
||||
const SERVICE_ADDRESS = config.get("NBI_INTERFACE");
|
||||
const SERVICE_PORT = config.get("NBI_PORT");
|
||||
const SERVICE_ADDRESS = config.get("NBI_INTERFACE") as string;
|
||||
const SERVICE_PORT = config.get("NBI_PORT") as number;
|
||||
|
||||
function exitWorkerGracefully(): void {
|
||||
db.disconnect();
|
||||
cache.disconnect();
|
||||
extensions.killAll();
|
||||
cluster.worker.disconnect();
|
||||
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||
Promise.all([
|
||||
db.disconnect(),
|
||||
extensions.killAll(),
|
||||
cluster.worker.disconnect(),
|
||||
]).catch(exitWorkerUngracefully);
|
||||
}
|
||||
|
||||
function exitWorkerUngracefully(): void {
|
||||
extensions.killAll().then(() => {
|
||||
extensions.killAll().finally(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
if (!cluster.worker) {
|
||||
const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES");
|
||||
const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES") as number;
|
||||
|
||||
logger.info({
|
||||
message: `genieacs-nbi starting`,
|
||||
pid: process.pid,
|
||||
version: VERSION
|
||||
version: VERSION,
|
||||
});
|
||||
|
||||
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||
@@ -61,7 +60,7 @@ if (!cluster.worker) {
|
||||
process.on("SIGINT", () => {
|
||||
logger.info({
|
||||
message: "Received signal SIGINT, exiting",
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
|
||||
cluster.stop();
|
||||
@@ -70,51 +69,61 @@ if (!cluster.worker) {
|
||||
process.on("SIGTERM", () => {
|
||||
logger.info({
|
||||
message: "Received signal SIGTERM, exiting",
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
|
||||
cluster.stop();
|
||||
});
|
||||
} else {
|
||||
const ssl = {
|
||||
key: config.get("NBI_SSL_KEY"),
|
||||
cert: config.get("NBI_SSL_CERT")
|
||||
const key = config.get("NBI_SSL_KEY") as string;
|
||||
const cert = config.get("NBI_SSL_CERT") as string;
|
||||
const options = {
|
||||
port: SERVICE_PORT,
|
||||
host: SERVICE_ADDRESS,
|
||||
ssl: key && cert ? { key, cert } : null,
|
||||
timeout: 30000,
|
||||
};
|
||||
|
||||
process.on("uncaughtException", err => {
|
||||
let stopping = false;
|
||||
|
||||
process.on("uncaughtException", (err) => {
|
||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||
logger.error({
|
||||
message: "Uncaught exception",
|
||||
exception: err,
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
stopping = true;
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
});
|
||||
const _listener = (req, res): void => {
|
||||
if (stopping) res.setHeader("Connection", "close");
|
||||
listener(req, res);
|
||||
};
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
Promise.all([db.connect(), cache.connect()])
|
||||
const initPromise = db
|
||||
.connect()
|
||||
.then(() => {
|
||||
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
|
||||
server.start(options, _listener);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
stopping = true;
|
||||
initPromise.finally(() => {
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
stopping = true;
|
||||
initPromise.finally(() => {
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||
|
||||
/**
|
||||
* Copyright 2013-2019 GenieACS Inc.
|
||||
*
|
||||
@@ -25,37 +23,36 @@ import * as cluster from "../lib/cluster";
|
||||
import * as server from "../lib/server";
|
||||
import { listener } from "../lib/ui";
|
||||
import * as extensions from "../lib/extensions";
|
||||
import * as db from "../lib/ui/db";
|
||||
import * as db2 from "../lib/db";
|
||||
import * as cache from "../lib/cache";
|
||||
import * as db from "../lib/db";
|
||||
import { version as VERSION } from "../package.json";
|
||||
|
||||
logger.init("ui", VERSION);
|
||||
|
||||
const SERVICE_ADDRESS = config.get("UI_INTERFACE");
|
||||
const SERVICE_PORT = config.get("UI_PORT");
|
||||
const SERVICE_ADDRESS = config.get("UI_INTERFACE") as string;
|
||||
const SERVICE_PORT = config.get("UI_PORT") as number;
|
||||
|
||||
function exitWorkerGracefully(): void {
|
||||
db.disconnect();
|
||||
db2.disconnect();
|
||||
cache.disconnect();
|
||||
extensions.killAll();
|
||||
cluster.worker.disconnect();
|
||||
setTimeout(exitWorkerUngracefully, 5000).unref();
|
||||
Promise.all([
|
||||
db.disconnect(),
|
||||
extensions.killAll(),
|
||||
cluster.worker.disconnect(),
|
||||
]).catch(exitWorkerUngracefully);
|
||||
}
|
||||
|
||||
function exitWorkerUngracefully(): void {
|
||||
extensions.killAll().then(() => {
|
||||
extensions.killAll().finally(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
if (!cluster.worker) {
|
||||
const WORKER_COUNT = config.get("UI_WORKER_PROCESSES");
|
||||
const WORKER_COUNT = config.get("UI_WORKER_PROCESSES") as number;
|
||||
|
||||
logger.info({
|
||||
message: `genieacs-ui starting`,
|
||||
pid: process.pid,
|
||||
version: VERSION
|
||||
version: VERSION,
|
||||
});
|
||||
|
||||
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
|
||||
@@ -63,7 +60,7 @@ if (!cluster.worker) {
|
||||
process.on("SIGINT", () => {
|
||||
logger.info({
|
||||
message: "Received signal SIGINT, exiting",
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
|
||||
cluster.stop();
|
||||
@@ -72,47 +69,32 @@ if (!cluster.worker) {
|
||||
process.on("SIGTERM", () => {
|
||||
logger.info({
|
||||
message: "Received signal SIGTERM, exiting",
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
|
||||
cluster.stop();
|
||||
});
|
||||
} else {
|
||||
const ssl = {
|
||||
key: config.get("UI_SSL_KEY"),
|
||||
cert: config.get("UI_SSL_CERT")
|
||||
const key = config.get("UI_SSL_KEY") as string;
|
||||
const cert = config.get("UI_SSL_CERT") as string;
|
||||
const options = {
|
||||
port: SERVICE_PORT,
|
||||
host: SERVICE_ADDRESS,
|
||||
ssl: key && cert ? { key, cert } : null,
|
||||
timeout: 30000,
|
||||
};
|
||||
|
||||
let stopping = false;
|
||||
|
||||
process.on("uncaughtException", err => {
|
||||
process.on("uncaughtException", (err) => {
|
||||
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
|
||||
logger.error({
|
||||
message: "Uncaught exception",
|
||||
exception: err,
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
stopping = true;
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
stopping = true;
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
stopping = true;
|
||||
server
|
||||
.stop()
|
||||
.then(exitWorkerGracefully)
|
||||
.catch(exitWorkerUngracefully);
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
|
||||
const _listener = (req, res): void => {
|
||||
@@ -120,13 +102,28 @@ if (!cluster.worker) {
|
||||
listener(req, res);
|
||||
};
|
||||
|
||||
Promise.all([db2.connect(), cache.connect()])
|
||||
const initPromise = db
|
||||
.connect()
|
||||
.then(() => {
|
||||
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
|
||||
server.start(options, _listener);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
stopping = true;
|
||||
initPromise.finally(() => {
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
stopping = true;
|
||||
initPromise.finally(() => {
|
||||
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
|
||||
});
|
||||
});
|
||||
}
|
||||
+81
-132
@@ -19,18 +19,17 @@
|
||||
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { promisify } from "util";
|
||||
import { rollup, WarningHandler } from "rollup";
|
||||
import rollupReplace from "rollup-plugin-replace";
|
||||
import rollupJson from "rollup-plugin-json";
|
||||
import typescript from "rollup-plugin-typescript";
|
||||
import rollupJson from "@rollup/plugin-json";
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import webpack from "webpack";
|
||||
import nodeResolve from "@rollup/plugin-node-resolve";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import postcss from "postcss";
|
||||
import postcssImport from "postcss-import";
|
||||
import postcssPresetEnv from "postcss-preset-env";
|
||||
import cssnano from "cssnano";
|
||||
import SVGO from "svgo";
|
||||
import { optimize } from "svgo";
|
||||
import * as xmlParser from "../lib/xml-parser";
|
||||
|
||||
const MODE = process.env["NODE_ENV"] || "production";
|
||||
@@ -43,7 +42,7 @@ const BUILD_METADATA = new Date()
|
||||
const INPUT_DIR = path.resolve(__dirname, "..");
|
||||
const OUTPUT_DIR = path.resolve(__dirname, "../dist");
|
||||
|
||||
const externals = [
|
||||
const builtins = [
|
||||
"path",
|
||||
"fs",
|
||||
"cluster",
|
||||
@@ -54,30 +53,13 @@ const externals = [
|
||||
"zlib",
|
||||
"crypto",
|
||||
"util",
|
||||
"mongodb",
|
||||
"libxmljs",
|
||||
"vm",
|
||||
"later",
|
||||
"parsimmon",
|
||||
"seedrandom",
|
||||
"querystring",
|
||||
"child_process",
|
||||
"dgram",
|
||||
"url",
|
||||
"koa",
|
||||
"koa-router",
|
||||
"koa-compress",
|
||||
"koa-bodyparser",
|
||||
"koa-jwt",
|
||||
"koa-static",
|
||||
"jsonwebtoken",
|
||||
"readline",
|
||||
"stream",
|
||||
"mithril",
|
||||
"parsimmon",
|
||||
"yaml",
|
||||
"codemirror",
|
||||
"codemirror/mode/javascript/javascript",
|
||||
"codemirror/mode/yaml/yaml"
|
||||
];
|
||||
|
||||
function rmDirSync(dirPath): void {
|
||||
@@ -122,14 +104,12 @@ function generateSymbol(id: string, svgStr: string): string {
|
||||
}
|
||||
}
|
||||
const symbolBody = xml.children[0].children
|
||||
.map(c => {
|
||||
return xmlTostring(c);
|
||||
})
|
||||
.map((c) => xmlTostring(c))
|
||||
.join("");
|
||||
return `<symbol id="icon-${id}" ${viewBox}>${symbolBody}</symbol>`;
|
||||
}
|
||||
|
||||
async function init(): Promise<void> {
|
||||
async function init(): Promise<string[]> {
|
||||
// Delete any old output directory
|
||||
rmDirSync(OUTPUT_DIR);
|
||||
|
||||
@@ -137,16 +117,16 @@ async function init(): Promise<void> {
|
||||
fs.mkdirSync(OUTPUT_DIR);
|
||||
fs.mkdirSync(OUTPUT_DIR + "/bin");
|
||||
fs.mkdirSync(OUTPUT_DIR + "/public");
|
||||
fs.mkdirSync(OUTPUT_DIR + "/tools");
|
||||
|
||||
// Create package.json
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.resolve(INPUT_DIR, "package.json")).toString()
|
||||
);
|
||||
delete packageJson["devDependencies"];
|
||||
delete packageJson["private"];
|
||||
packageJson["scripts"] = {
|
||||
install: packageJson["scripts"].install,
|
||||
configure: packageJson["scripts"].configure
|
||||
configure: packageJson["scripts"].configure,
|
||||
};
|
||||
packageJson["version"] = `${packageJson["version"]}+${BUILD_METADATA}`;
|
||||
fs.writeFileSync(
|
||||
@@ -164,6 +144,7 @@ async function init(): Promise<void> {
|
||||
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
|
||||
JSON.stringify(npmShrinkwrapJson, null, 2)
|
||||
);
|
||||
return Object.keys(packageJson["dependencies"]);
|
||||
}
|
||||
|
||||
async function copyStatic(): Promise<void> {
|
||||
@@ -172,7 +153,7 @@ async function copyStatic(): Promise<void> {
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"public/logo.svg",
|
||||
"public/favicon.png"
|
||||
"public/favicon.png",
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
@@ -193,79 +174,38 @@ async function generateCss(): Promise<void> {
|
||||
stage: 3,
|
||||
features: {
|
||||
"nesting-rules": true,
|
||||
"color-mod-function": true
|
||||
}
|
||||
"color-mod-function": true,
|
||||
},
|
||||
}),
|
||||
cssnano
|
||||
cssnano,
|
||||
]).process(cssIn, { from: cssInPath, to: cssOutPath });
|
||||
fs.writeFileSync(cssOutPath, cssOut.css);
|
||||
}
|
||||
|
||||
async function generateToolsJs(): Promise<void> {
|
||||
for (const bin of ["dump-data-model"]) {
|
||||
const inputFile = path.resolve(INPUT_DIR, `tools/${bin}`);
|
||||
const outputFile = path.resolve(OUTPUT_DIR, `tools/${bin}`);
|
||||
const bundle = await rollup({
|
||||
input: inputFile,
|
||||
external: externals,
|
||||
acorn: {
|
||||
allowHashBang: true
|
||||
},
|
||||
plugins: [
|
||||
rollupReplace({
|
||||
delimiters: ["", ""],
|
||||
"#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": ""
|
||||
}),
|
||||
typescript({
|
||||
tsconfig: "./tsconfig.json",
|
||||
include: [`tools/${bin}`, "lib/**/*.ts"]
|
||||
}),
|
||||
MODE === "production" ? terser() : null
|
||||
]
|
||||
});
|
||||
|
||||
await bundle.write({
|
||||
format: "cjs",
|
||||
preferConst: true,
|
||||
sourcemap: "inline",
|
||||
sourcemapExcludeSources: true,
|
||||
banner: "#!/usr/bin/env node",
|
||||
file: outputFile
|
||||
});
|
||||
|
||||
// Mark as executable
|
||||
const mode = fs.statSync(outputFile).mode;
|
||||
fs.chmodSync(outputFile, mode | 73);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateBackendJs(): Promise<void> {
|
||||
async function generateBackendJs(externals: string[]): Promise<void> {
|
||||
for (const bin of [
|
||||
"genieacs-cwmp",
|
||||
"genieacs-ext",
|
||||
"genieacs-nbi",
|
||||
"genieacs-fs",
|
||||
"genieacs-ui"
|
||||
"genieacs-ui",
|
||||
]) {
|
||||
const inputFile = path.resolve(INPUT_DIR, `bin/${bin}`);
|
||||
const inputFile = path.resolve(INPUT_DIR, `bin/${bin}.ts`);
|
||||
const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`);
|
||||
const bundle = await rollup({
|
||||
input: inputFile,
|
||||
external: externals,
|
||||
external: [...builtins, ...externals],
|
||||
acorn: {
|
||||
allowHashBang: true
|
||||
allowHashBang: true,
|
||||
},
|
||||
treeshake: {
|
||||
propertyReadSideEffects: false,
|
||||
pureExternalModules: true
|
||||
moduleSideEffects: false,
|
||||
},
|
||||
plugins: [
|
||||
rollupReplace({
|
||||
delimiters: ["", ""],
|
||||
"#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": ""
|
||||
}),
|
||||
rollupJson({ preferConst: true }),
|
||||
{
|
||||
name: "",
|
||||
resolveId: (importee, importer) => {
|
||||
if (importee.endsWith("/package.json")) {
|
||||
const p = path.resolve(path.dirname(importer), importee);
|
||||
@@ -273,14 +213,14 @@ async function generateBackendJs(): Promise<void> {
|
||||
return path.resolve(OUTPUT_DIR, "package.json");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
typescript({
|
||||
tsconfig: "./tsconfig.json",
|
||||
include: [`bin/${bin}`, "lib/**/*.ts"]
|
||||
include: [`bin/${bin}.ts`, "lib/**/*.ts"],
|
||||
}),
|
||||
MODE === "production" ? terser() : null
|
||||
]
|
||||
MODE === "production" ? terser() : null,
|
||||
],
|
||||
});
|
||||
|
||||
await bundle.write({
|
||||
@@ -289,7 +229,7 @@ async function generateBackendJs(): Promise<void> {
|
||||
sourcemap: "inline",
|
||||
sourcemapExcludeSources: true,
|
||||
banner: "#!/usr/bin/env node",
|
||||
file: outputFile
|
||||
file: outputFile,
|
||||
});
|
||||
|
||||
// Mark as executable
|
||||
@@ -298,70 +238,78 @@ 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 outputFile = path.resolve(OUTPUT_DIR, "public/app.js");
|
||||
const outputDir = path.resolve(OUTPUT_DIR, "public");
|
||||
|
||||
const inlineDeps = ["parsimmon", "espresso-iisojs"];
|
||||
const bundle = await rollup({
|
||||
input: inputFile,
|
||||
external: externals,
|
||||
external: [
|
||||
...builtins,
|
||||
...externals.filter((e) => !inlineDeps.includes(e)),
|
||||
],
|
||||
plugins: [
|
||||
rollupJson({ preferConst: true }),
|
||||
typescript({ tsconfig: "./tsconfig.json" })
|
||||
{
|
||||
name: "",
|
||||
resolveId: function (importee, importer) {
|
||||
if (importee.endsWith("/package.json")) {
|
||||
const p = path.resolve(path.dirname(importer), importee);
|
||||
if (p === path.resolve(INPUT_DIR, "package.json"))
|
||||
return path.resolve(OUTPUT_DIR, "package.json");
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
typescript({ tsconfig: "./tsconfig.json" }),
|
||||
nodeResolve(),
|
||||
commonjs(),
|
||||
MODE === "production" ? terser() : null,
|
||||
],
|
||||
inlineDynamicImports: true,
|
||||
preserveEntrySignatures: false,
|
||||
treeshake: {
|
||||
propertyReadSideEffects: false,
|
||||
pureExternalModules: true
|
||||
moduleSideEffects: false,
|
||||
},
|
||||
onwarn: ((warning, warn) => {
|
||||
// Ignore circular dependency warnings
|
||||
if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning);
|
||||
}) as WarningHandler
|
||||
}) as WarningHandler,
|
||||
});
|
||||
|
||||
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,
|
||||
format: "esm",
|
||||
sourcemap: "inline",
|
||||
format: "es",
|
||||
sourcemap: true,
|
||||
sourcemapExcludeSources: true,
|
||||
file: outputFile
|
||||
dir: outputDir,
|
||||
});
|
||||
|
||||
const webpackConf = {
|
||||
mode: MODE,
|
||||
entry: outputFile,
|
||||
resolve: {
|
||||
aliasFields: ["module"]
|
||||
},
|
||||
devtool: "nosources-source-map",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre"
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(OUTPUT_DIR, "public"),
|
||||
filename: "app.js"
|
||||
}
|
||||
};
|
||||
|
||||
const stats = await promisify(webpack)(webpackConf);
|
||||
process.stdout.write(stats.toString({ colors: true }) + "\n");
|
||||
}
|
||||
|
||||
async function generateIconsSprite(): Promise<void> {
|
||||
const svgo = new SVGO({ plugins: [{ removeViewBox: false }] });
|
||||
const symbols = [];
|
||||
const iconsDir = path.resolve(INPUT_DIR, "ui/icons");
|
||||
for (const file of fs.readdirSync(iconsDir)) {
|
||||
const id = path.parse(file).name;
|
||||
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));
|
||||
}
|
||||
fs.writeFileSync(
|
||||
@@ -371,20 +319,21 @@ async function generateIconsSprite(): Promise<void> {
|
||||
}
|
||||
|
||||
init()
|
||||
.then(() => {
|
||||
.then((externals) => {
|
||||
Promise.all([
|
||||
copyStatic(),
|
||||
generateCss(),
|
||||
generateIconsSprite(),
|
||||
generateToolsJs(),
|
||||
generateBackendJs(),
|
||||
generateFrontendJs()
|
||||
generateBackendJs(externals),
|
||||
generateFrontendJs(externals),
|
||||
])
|
||||
.then(() => {})
|
||||
.catch(err => {
|
||||
.then(() => {
|
||||
// Ignore
|
||||
})
|
||||
.catch((err) => {
|
||||
process.stderr.write(err.stack + "\n");
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
process.stderr.write(err.stack + "\n");
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
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
|
||||
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
|
||||
@@ -0,0 +1,64 @@
|
||||
.. _administration-faq:
|
||||
|
||||
Administration FAQ
|
||||
==================
|
||||
|
||||
.. _administration-faq-duplicate-log-entries:
|
||||
|
||||
Duplicate log entries when using :func:`log()` function
|
||||
-------------------------------------------------------
|
||||
|
||||
Because GenieACS uses a full fledged scripting language for device
|
||||
configuration, the only way to guarantee that it has satisfied the 'desired
|
||||
state' is by repeatedly executing the script until there's no more
|
||||
discrepancies with the current device state. Though it may seem like this will
|
||||
cause duplicate requests going to the device, this isn't actually the case
|
||||
because device configuration are stated declaratively and that the scripts
|
||||
themselves are pure functions in the context of a session (e.g. Date.now()
|
||||
always returns the same value within the session).
|
||||
|
||||
To illustrate with an example, consider the following script:
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
log("Executing script");
|
||||
declare("Device.param", null, {value: 1});
|
||||
commit();
|
||||
declare("Device.param", null, {value: 2});
|
||||
|
||||
This will set the value of the 'Device.param' to 1, then to 2. Then as the
|
||||
script is run again the value is set back to 1 and so on. A stable state will
|
||||
never be reached so GenieACS will execute the script a few times until it gives
|
||||
up and throws a fault. This is an edge case that should be avoided. A more
|
||||
typical case is where the script is run once or twice. Essentially if an
|
||||
execution doesn't result in any request to the CPE or a change in the data
|
||||
model then a stable state is deemed to have been reached.
|
||||
|
||||
Configurations not pushed to device after factory reset
|
||||
---------------------------------------------------------
|
||||
|
||||
After a device is reset to its factory default state, the cached data model in
|
||||
GenieACS's database needs to be invalidated to force rediscovery. Ensure the
|
||||
following lines are called on ``0 BOOTSTRAP`` event:
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Clear cached data model to force a refresh
|
||||
clear("Device", now);
|
||||
clear("InternetGatewayDevice", now);
|
||||
|
||||
|
||||
Most device parameters are missing
|
||||
----------------------------------
|
||||
|
||||
For performance reasons (server, client, and network), GenieACS by default only
|
||||
fetches parts of the data model that are necessary to satisfy the declarations
|
||||
in your provision scripts. Create declarations for any parameters you need
|
||||
fetched by default.
|
||||
|
||||
If you're unsure and want to explore the available parameters exposed by the
|
||||
device, refresh the root parameter (e.g. ``InternetGatewayDevice``) from
|
||||
GenieACS's UI. You typically only need to do that one time for a given CPE
|
||||
model.
|
||||
+25
-16
@@ -1,7 +1,7 @@
|
||||
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.
|
||||
|
||||
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::
|
||||
|
||||
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.
|
||||
|
||||
Endpoints
|
||||
@@ -41,11 +41,11 @@ Examples
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
query = {"_id": "202BC1-BM632w-0000000"}
|
||||
query = {"_id": "202BC1-BM632w-000000"}
|
||||
|
||||
.. 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:
|
||||
|
||||
@@ -77,21 +77,21 @@ Examples
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
query = {"device": "202BC1-BM632w-0000000"}
|
||||
query = {"device": "202BC1-BM632w-000000"}
|
||||
|
||||
.. 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:
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
query = {"_id": "202BC1-BM632w-0000000"}
|
||||
query = {"_id": "202BC1-BM632w-000000"}
|
||||
|
||||
.. 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.
|
||||
|
||||
@@ -106,7 +106,7 @@ if the tasks have been queued to be executed at the next inform.
|
||||
*device_id*: The ID of the device.
|
||||
|
||||
*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.
|
||||
|
||||
The response body is the task object as it is inserted in the database. The
|
||||
@@ -120,7 +120,7 @@ Examples
|
||||
|
||||
.. 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 \
|
||||
--data '{"name": "refreshObject", "objectName": ""}'
|
||||
|
||||
@@ -138,7 +138,7 @@ Examples
|
||||
|
||||
.. 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 \
|
||||
--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
|
||||
|
||||
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\>
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -218,7 +218,7 @@ Assign the tag "testing" to a device:
|
||||
|
||||
.. 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\>
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -236,7 +236,7 @@ Remove the tag "testing" from a device:
|
||||
|
||||
.. 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\>
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -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".
|
||||
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.
|
||||
|
||||
@@ -344,7 +344,7 @@ Find files using a query.
|
||||
Tasks
|
||||
-----
|
||||
|
||||
Find the different availabe tasks and their object structure.
|
||||
Find the different available tasks and their object structure.
|
||||
|
||||
``getParameterValues``
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -415,6 +415,15 @@ parameterValues key. For example:
|
||||
-X POST \
|
||||
--data '{"name":"addObject","objectName":"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection"}'
|
||||
|
||||
``deleteObject``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl -i 'http://localhost:7557/devices/00236a-SR552n-SR552NA084%252D0003269/tasks?timeout=3000&connection_request' \
|
||||
-X POST \
|
||||
--data '{"name":"deleteObject","objectName":"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1"}'
|
||||
|
||||
``reboot``
|
||||
~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -3,4 +3,70 @@
|
||||
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
|
||||
@@ -3,4 +3,215 @@
|
||||
Environment Variables
|
||||
=====================
|
||||
|
||||
TODO
|
||||
Configuring GenieACS services can be done through the following environment
|
||||
variables:
|
||||
|
||||
.. attention::
|
||||
|
||||
All GenieACS environment variables must be prefixed with ``GENIEACS_``.
|
||||
|
||||
MONGODB_CONNECTION_URL
|
||||
MongoDB connection string.
|
||||
|
||||
Default: ``mongodb://127.0.0.1/genieacs``
|
||||
|
||||
EXT_DIR
|
||||
The directory from which to look up extension scripts.
|
||||
|
||||
Default: ``<installation dir>/config/ext``
|
||||
|
||||
EXT_TIMEOUT
|
||||
Timeout (in milliseconds) to allow for calls to extensions to return a
|
||||
response.
|
||||
|
||||
Default: ``3000``
|
||||
|
||||
DEBUG_FILE
|
||||
File to dump CPE debug log.
|
||||
|
||||
Default: unset
|
||||
|
||||
DEBUG_FORMAT
|
||||
Debug log format. Valid values are 'yaml' and 'json'.
|
||||
|
||||
Default: ``yaml``
|
||||
|
||||
LOG_FORMAT
|
||||
The format used for the log entries in ``CWMP_LOG_FILE``, ``NBI_LOG_FILE``,
|
||||
``FS_LOG_FILE``, and ``UI_LOG_FILE``. Possible values are ``simple`` and
|
||||
``json``.
|
||||
|
||||
Default: ``simple``
|
||||
|
||||
ACCESS_LOG_FORMAT
|
||||
The format used for the log entries in ``CWMP_ACCESS_LOG_FILE``,
|
||||
``NBI_ACCESS_LOG_FILE``, ``FS_ACCESS_LOG_FILE``, and ``UI_ACCESS_LOG_FILE``.
|
||||
Possible values are ``simple`` and ``json``.
|
||||
|
||||
Default: ``simple``
|
||||
|
||||
CWMP_WORKER_PROCESSES
|
||||
The number of worker processes to spawn for genieacs-cwmp. A value of 0 means
|
||||
as many as there are CPU cores available.
|
||||
|
||||
Default: ``0``
|
||||
|
||||
CWMP_PORT
|
||||
The TCP port that genieacs-cwmp listens on.
|
||||
|
||||
Default: ``7547``
|
||||
|
||||
CWMP_INTERFACE
|
||||
The network interface that genieacs-cwmp binds to.
|
||||
|
||||
Default: ``::``
|
||||
|
||||
CWMP_SSL_CERT
|
||||
Path to certificate file. If omitted, non-secure HTTP will be used.
|
||||
|
||||
Default: unset
|
||||
|
||||
CWMP_SSL_KEY
|
||||
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
||||
|
||||
Default: unset
|
||||
|
||||
CWMP_LOG_FILE
|
||||
File to log process related events for genieacs-cwmp. If omitted, logs will
|
||||
go to stderr.
|
||||
|
||||
Default: unset
|
||||
|
||||
CWMP_ACCESS_LOG_FILE
|
||||
File to log incoming requests for genieacs-cwmp. If omitted, logs will go to
|
||||
stdout.
|
||||
|
||||
Default: unset
|
||||
|
||||
NBI_WORKER_PROCESSES
|
||||
The number of worker processes to spawn for genieacs-nbi. A value of 0 means
|
||||
as many as there are CPU cores available.
|
||||
|
||||
Default: ``0``
|
||||
|
||||
NBI_PORT
|
||||
The TCP port that genieacs-nbi listens on.
|
||||
|
||||
Default: ``7557``
|
||||
|
||||
NBI_INTERFACE
|
||||
The network interface that genieacs-nbi binds to.
|
||||
|
||||
Default: ``::``
|
||||
|
||||
NBI_SSL_CERT
|
||||
Path to certificate file. If omitted, non-secure HTTP will be used.
|
||||
|
||||
Default: unset
|
||||
|
||||
NBI_SSL_KEY
|
||||
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
||||
|
||||
Default: unset
|
||||
|
||||
NBI_LOG_FILE
|
||||
File to log process related events for genieacs-nbi. If omitted, logs will go
|
||||
to stderr.
|
||||
|
||||
Default: unset
|
||||
|
||||
NBI_ACCESS_LOG_FILE
|
||||
File to log incoming requests for genieacs-nbi. If omitted, logs will go to
|
||||
stdout.
|
||||
|
||||
Default: unset
|
||||
|
||||
FS_WORKER_PROCESSES
|
||||
The number of worker processes to spawn for genieacs-fs. A value of 0 means
|
||||
as many as there are CPU cores available.
|
||||
|
||||
Default: ``0``
|
||||
|
||||
FS_PORT
|
||||
The TCP port that genieacs-fs listens on.
|
||||
|
||||
Default: ``7567``
|
||||
|
||||
FS_INTERFACE
|
||||
The network interface that genieacs-fs binds to.
|
||||
|
||||
Default: ``::``
|
||||
|
||||
FS_SSL_CERT
|
||||
Path to certificate file. If omitted, non-secure HTTP will be used.
|
||||
|
||||
Default: unset
|
||||
|
||||
FS_SSL_KEY
|
||||
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
||||
|
||||
Default: unset
|
||||
|
||||
FS_LOG_FILE
|
||||
File to log process related events for genieacs-fs. If omitted, logs will go
|
||||
to stderr.
|
||||
|
||||
Default: unset
|
||||
|
||||
FS_ACCESS_LOG_FILE
|
||||
File to log incoming requests for genieacs-fs. If omitted, logs will go to
|
||||
stdout.
|
||||
|
||||
Default: unset
|
||||
|
||||
FS_URL_PREFIX
|
||||
The URL prefix (e.g. 'https://example.com:7657/') to use when generating the
|
||||
file URL for TR-069 Download requests. Set this if genieacs-fs and
|
||||
genieacs-cwmp are behind a proxy or running on different servers.
|
||||
|
||||
Default: auto generated based on the hostname from the ACS URL, FS_PORT
|
||||
config, and whether or not SSL is enabled for genieacs-fs.
|
||||
|
||||
UI_WORKER_PROCESSES
|
||||
The number of worker processes to spawn for genieacs-ui. A value of 0 means
|
||||
as many as there are CPU cores available.
|
||||
|
||||
Default: ``0``
|
||||
|
||||
UI_PORT
|
||||
The TCP port that genieacs-ui listens on.
|
||||
|
||||
Default: ``3000``
|
||||
|
||||
UI_INTERFACE
|
||||
The network interface that genieacs-ui binds to.
|
||||
|
||||
Default: ``::``
|
||||
|
||||
UI_SSL_CERT
|
||||
Path to certificate file. If omitted, non-secure HTTP will be used.
|
||||
|
||||
Default: unset
|
||||
|
||||
UI_SSL_KEY
|
||||
Path to certificate key file. If omitted, non-secure HTTP will be used.
|
||||
|
||||
Default: unset
|
||||
|
||||
UI_LOG_FILE
|
||||
File to log process related events for genieacs-ui. If omitted, logs will go
|
||||
to stderr.
|
||||
|
||||
Default: unset
|
||||
|
||||
UI_ACCESS_LOG_FILE
|
||||
File to log incoming requests for genieacs-ui. If omitted, logs will go to
|
||||
stdout.
|
||||
|
||||
Default: unset
|
||||
|
||||
UI_JWT_SECRET
|
||||
The key used for signing JWT tokens that are stored in browser cookies. The
|
||||
string can be up to 64 characters in length.
|
||||
|
||||
Default: unset
|
||||
+19
-16
@@ -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.
|
||||
// To install, copy this file to config/ext/iss.js.
|
||||
|
||||
@@ -10,25 +10,28 @@ let cache = null;
|
||||
let cacheExpire = 0;
|
||||
|
||||
function latlong(args, callback) {
|
||||
if (Date.now() < cacheExpire)
|
||||
return callback(null, cache);
|
||||
if (Date.now() < cacheExpire) return callback(null, cache);
|
||||
|
||||
http.get("http://api.open-notify.org/iss-now.json", (res) => {
|
||||
if (res.statusCode !== 200)
|
||||
return callback(new Error(`Request failed (status code: ${res.statusCode})`));
|
||||
http
|
||||
.get("http://api.open-notify.org/iss-now.json", (res) => {
|
||||
if (res.statusCode !== 200)
|
||||
return callback(
|
||||
new Error(`Request failed (status code: ${res.statusCode})`)
|
||||
);
|
||||
|
||||
let rawData = "";
|
||||
res.on("data", (chunk) => rawData += chunk);
|
||||
let rawData = "";
|
||||
res.on("data", (chunk) => (rawData += chunk));
|
||||
|
||||
res.on("end", () => {
|
||||
let pos = JSON.parse(rawData)["iss_position"];
|
||||
cache = [+pos["latitude"], +pos["longitude"]];
|
||||
cacheExpire = Date.now() + 10000;
|
||||
callback(null, cache);
|
||||
res.on("end", () => {
|
||||
let pos = JSON.parse(rawData)["iss_position"];
|
||||
cache = [+pos["latitude"], +pos["longitude"]];
|
||||
cacheExpire = Date.now() + 10000;
|
||||
callback(null, cache);
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
callback(err);
|
||||
});
|
||||
}).on("error", (err) => {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
exports.latlong = latlong;
|
||||
@@ -30,6 +30,7 @@ Welcome to GenieACS's documentation!
|
||||
|
||||
provisions
|
||||
virtual-parameters
|
||||
administration-faq
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
|
||||
+20
-17
@@ -4,7 +4,7 @@ Installation Guide
|
||||
This guide is for installing GenieACS on a single server on any Linux distro
|
||||
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
|
||||
a load-balancing/failover setup.
|
||||
|
||||
@@ -19,7 +19,7 @@ Prerequisites
|
||||
|
||||
.. 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.
|
||||
|
||||
.. topic:: MongoDB
|
||||
@@ -68,11 +68,13 @@ Configure systemd
|
||||
|
||||
.. code:: bash
|
||||
|
||||
GENIEACS_CWMP_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-cmwp-access.log
|
||||
GENIEACS_CWMP_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-cwmp-access.log
|
||||
GENIEACS_NBI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-nbi-access.log
|
||||
GENIEACS_FS_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-fs-access.log
|
||||
GENIEACS_UI_ACCESS_LOG_FILE=/var/log/genieacs/genieacs-ui-access.log
|
||||
GENIEACS_DEBUG_FILE=/var/log/genieacs/genieacs-debug.yaml
|
||||
NODE_OPTIONS=--enable-source-maps
|
||||
GENIEACS_EXT_DIR=/opt/genieacs/ext
|
||||
GENIEACS_UI_JWT_SECRET=secret
|
||||
|
||||
Set file ownership and permissions:
|
||||
@@ -100,11 +102,16 @@ Configure systemd
|
||||
:file:`/var/log/genieacs/` while process logs go to *journald*. Use
|
||||
``journalctl`` command to view process logs.
|
||||
|
||||
.. attention::
|
||||
|
||||
If the command :command:`systemctl edit --force --full` fails, you can
|
||||
create the unit file manually.
|
||||
|
||||
1. Run the following command to create ``genieacs-cwmp`` service:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
sudo systemctl edit --force genieacs-cwmp
|
||||
sudo systemctl edit --force --full genieacs-cwmp
|
||||
|
||||
Then paste the following in the editor and save:
|
||||
|
||||
@@ -117,8 +124,7 @@ Configure systemd
|
||||
[Service]
|
||||
User=genieacs
|
||||
EnvironmentFile=/opt/genieacs/genieacs.env
|
||||
ExecStart=genieacs-cwmp
|
||||
KillMode=process
|
||||
ExecStart=/usr/bin/genieacs-cwmp
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -127,7 +133,7 @@ Configure systemd
|
||||
|
||||
.. code:: bash
|
||||
|
||||
sudo systemctl edit --force genieacs-nbi
|
||||
sudo systemctl edit --force --full genieacs-nbi
|
||||
|
||||
Then paste the following in the editor and save:
|
||||
|
||||
@@ -140,8 +146,7 @@ Configure systemd
|
||||
[Service]
|
||||
User=genieacs
|
||||
EnvironmentFile=/opt/genieacs/genieacs.env
|
||||
ExecStart=genieacs-nbi
|
||||
KillMode=process
|
||||
ExecStart=/usr/bin/genieacs-nbi
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -150,7 +155,7 @@ Configure systemd
|
||||
|
||||
.. code:: bash
|
||||
|
||||
sudo systemctl edit --force genieacs-fs
|
||||
sudo systemctl edit --force --full genieacs-fs
|
||||
|
||||
Then paste the following in the editor and save:
|
||||
|
||||
@@ -163,8 +168,7 @@ Configure systemd
|
||||
[Service]
|
||||
User=genieacs
|
||||
EnvironmentFile=/opt/genieacs/genieacs.env
|
||||
ExecStart=genieacs-fs
|
||||
KillMode=process
|
||||
ExecStart=/usr/bin/genieacs-fs
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -173,7 +177,7 @@ Configure systemd
|
||||
|
||||
.. code:: bash
|
||||
|
||||
sudo systemctl edit --force genieacs-ui
|
||||
sudo systemctl edit --force --full genieacs-ui
|
||||
|
||||
Then paste the following in the editor and save:
|
||||
|
||||
@@ -186,19 +190,18 @@ Configure systemd
|
||||
[Service]
|
||||
User=genieacs
|
||||
EnvironmentFile=/opt/genieacs/genieacs.env
|
||||
ExecStart=genieacs-ui
|
||||
KillMode=process
|
||||
ExecStart=/usr/bin/genieacs-ui
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
.. topic:: Configure log file rotation using logrotate
|
||||
|
||||
Save the following under :file:`/etc/logrotate.d/genieacs`
|
||||
Save the following as :file:`/etc/logrotate.d/genieacs`
|
||||
|
||||
.. code::
|
||||
|
||||
/var/log/genieacs/*.{log,yaml} {
|
||||
/var/log/genieacs/*.log /var/log/genieacs/*.yaml {
|
||||
daily
|
||||
rotate 30
|
||||
compress
|
||||
|
||||
@@ -128,6 +128,14 @@ script filename while second argument is the function name within that script.
|
||||
Any remaining arguments will be passed to that function. See :ref:`extensions`
|
||||
for more details.
|
||||
|
||||
``log(message)``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Prints out a string in genieacs-cwmp's access log. It's meant to be used for
|
||||
debugging. Note that you may see multiple log entries as the script can be
|
||||
executed multiple times in a session. See :ref:`this FAQ
|
||||
<administration-faq-duplicate-log-entries>`.
|
||||
|
||||
.. _path-format:
|
||||
|
||||
Path format
|
||||
|
||||
@@ -18,8 +18,8 @@ differences between the scripts of provisions and virtual parameters are:
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
// [<current attr timestamps>, <current attr values>, <declared attr timestamps, declared attr values>]
|
||||
[{path: 1559840000000, value: 1559840000000}, {value: ["cur val", "xsd:string"]}, {path: 1559849387191, value: 1559849387191}, {value: ["new val", "xsd:string"]}]
|
||||
// [<declared attr timestamps, declared attr values>, <current attr timestamps>, <current attr values>]
|
||||
[{path: 1559849387191, value: 1559849387191}, {value: ["new val", "xsd:string"]}, {path: 1559840000000, value: 1559840000000}, {value: ["cur val", "xsd:string"]}]
|
||||
|
||||
- Virtual parameter scripts must return an object containing the attributes of
|
||||
this parameter.
|
||||
@@ -69,9 +69,9 @@ Expose an external value as a virtual parameter
|
||||
|
||||
// Example: Expose an external value as a virtual parameter
|
||||
let serial = declare("DeviceID.SerialNumber", {value: 1});
|
||||
if (values.value) {
|
||||
ext("example-ext", "set", serial.value[0], values.value[0]);
|
||||
return {writable: true, value: [values.value[0], "xsd:string"]};
|
||||
if (args[1].value) {
|
||||
ext("example-ext", "set", serial.value[0], args[1].value[0]);
|
||||
return {writable: true, value: [args[1].value[0], "xsd:string"]};
|
||||
}
|
||||
else {
|
||||
let v = ext("example-ext", "get", serial.value[0]);
|
||||
@@ -85,8 +85,8 @@ Create an editable virtual parameter for WPA passphrase
|
||||
|
||||
// Example: Create an editable virtual parameter for WPA passphrase
|
||||
let m = "";
|
||||
if (VALUES && VALUES.value) {
|
||||
m = VALUES.value[0];
|
||||
if (args[1].value) {
|
||||
m = args[1].value[0];
|
||||
declare("Device.WiFi.AccessPoint.1.Security.KeyPassphrase", null, {value: m});
|
||||
declare("InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase", null, {value: m});
|
||||
}
|
||||
|
||||
+186
-114
@@ -17,92 +17,114 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { parse } from "url";
|
||||
import * as db from "./db";
|
||||
import * as common from "./common";
|
||||
import * as cache from "./cache";
|
||||
import {
|
||||
getCurrentSnapshot,
|
||||
getConfig,
|
||||
getConfigExpression
|
||||
getConfigExpression,
|
||||
} from "./local-cache";
|
||||
import {
|
||||
httpConnectionRequest,
|
||||
udpConnectionRequest
|
||||
udpConnectionRequest,
|
||||
} from "./connection-request";
|
||||
import { Expression, Task } from "./types";
|
||||
import { flattenDevice } from "./mongodb-functions";
|
||||
import { evaluate } from "./common/expression";
|
||||
|
||||
export async function connectionRequest(deviceId): Promise<void> {
|
||||
const options = {
|
||||
projection: {
|
||||
_deviceId: 1,
|
||||
"Device.ManagementServer.ConnectionRequestURL._value": 1,
|
||||
"Device.ManagementServer.UDPConnectionRequestAddress._value": 1,
|
||||
"Device.ManagementServer.ConnectionRequestUsername._value": 1,
|
||||
"Device.ManagementServer.ConnectionRequestPassword._value": 1,
|
||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL._value": 1,
|
||||
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress._value": 1,
|
||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername._value": 1,
|
||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword._value": 1
|
||||
}
|
||||
};
|
||||
|
||||
const device = await db.devicesCollection.findOne({ _id: deviceId }, options);
|
||||
if (!device) throw new Error("No such device");
|
||||
|
||||
let managementServer,
|
||||
connectionRequestUrl,
|
||||
udpConnectionRequestAddress,
|
||||
username,
|
||||
password;
|
||||
if (device.Device)
|
||||
// TR-181 data model
|
||||
managementServer = device.Device.ManagementServer;
|
||||
// TR-098 data model
|
||||
else managementServer = device.InternetGatewayDevice.ManagementServer;
|
||||
|
||||
if (managementServer.ConnectionRequestURL)
|
||||
connectionRequestUrl = managementServer.ConnectionRequestURL._value;
|
||||
if (managementServer.UDPConnectionRequestAddress) {
|
||||
udpConnectionRequestAddress =
|
||||
managementServer.UDPConnectionRequestAddress._value;
|
||||
export async function connectionRequest(
|
||||
deviceId: string,
|
||||
device?: Record<string, { value?: [boolean | number | string, string] }>
|
||||
): Promise<string> {
|
||||
if (!device) {
|
||||
const res = await db.devicesCollection.findOne({ _id: deviceId });
|
||||
if (!res) throw new Error("No such device");
|
||||
device = flattenDevice(res);
|
||||
}
|
||||
if (managementServer.ConnectionRequestUsername)
|
||||
username = managementServer.ConnectionRequestUsername._value;
|
||||
if (managementServer.ConnectionRequestPassword)
|
||||
password = managementServer.ConnectionRequestPassword._value;
|
||||
|
||||
const context = {
|
||||
id: device["_id"],
|
||||
serialNumber: device["_deviceId"]["SerialNumber"],
|
||||
productClass: device["_deviceId"]["ProductClass"],
|
||||
oui: device["_deviceId"]["OUI"],
|
||||
remoteAddress: connectionRequestUrl
|
||||
? parse(connectionRequestUrl).host
|
||||
: null,
|
||||
username: username || "",
|
||||
password: password || ""
|
||||
let connectionRequestUrl, udpConnectionRequestAddress, username, password;
|
||||
|
||||
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
|
||||
connectionRequestUrl = (device[
|
||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL"
|
||||
].value || [""])[0];
|
||||
udpConnectionRequestAddress = ((
|
||||
device[
|
||||
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
|
||||
] || {}
|
||||
).value || [""])[0];
|
||||
username = ((
|
||||
device[
|
||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername"
|
||||
] || {}
|
||||
).value || [""])[0];
|
||||
password = ((
|
||||
device[
|
||||
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword"
|
||||
] || {}
|
||||
).value || [""])[0];
|
||||
} else {
|
||||
connectionRequestUrl = (device[
|
||||
"Device.ManagementServer.ConnectionRequestURL"
|
||||
].value || [""])[0];
|
||||
udpConnectionRequestAddress = ((
|
||||
device["Device.ManagementServer.UDPConnectionRequestAddress"] || {}
|
||||
).value || [""])[0];
|
||||
username = ((
|
||||
device["Device.ManagementServer.ConnectionRequestUsername"] || {}
|
||||
).value || [""])[0];
|
||||
password = ((
|
||||
device["Device.ManagementServer.ConnectionRequestPassword"] || {}
|
||||
).value || [""])[0];
|
||||
}
|
||||
|
||||
const remoteAddress = new URL(connectionRequestUrl).hostname;
|
||||
|
||||
const evalCallback = (exp): Expression => {
|
||||
if (!Array.isArray(exp)) return exp;
|
||||
if (exp[0] === "PARAM" && typeof exp[1] === "string") {
|
||||
let name = exp[1];
|
||||
if (name === "id") name = "DeviceID.ID";
|
||||
else if (name === "serialNumber") name = "DeviceID.SerialNumber";
|
||||
else if (name === "productClass") name = "DeviceID.ProductClass";
|
||||
else if (name === "oui") name = "DeviceID.OUI";
|
||||
else if (name === "remoteAddress") return remoteAddress;
|
||||
else if (name === "username") return username;
|
||||
else if (name === "password") return password;
|
||||
|
||||
const p = device[name];
|
||||
if (p?.value) return p.value[0];
|
||||
} else if (exp[0] === "FUNC") {
|
||||
if (exp[1] === "REMOTE_ADDRESS") return remoteAddress;
|
||||
else if (exp[1] === "USERNAME") return username;
|
||||
else if (exp[1] === "PASSWORD") return password;
|
||||
}
|
||||
return exp;
|
||||
};
|
||||
|
||||
const snapshot = await getCurrentSnapshot();
|
||||
const now = Date.now();
|
||||
const UDP_CONNECTION_REQUEST_PORT = +getConfig(
|
||||
snapshot,
|
||||
"cwmp.udpConnectionRequestAuth",
|
||||
context,
|
||||
now
|
||||
"cwmp.udpConnectionRequestPort",
|
||||
{},
|
||||
now,
|
||||
evalCallback
|
||||
);
|
||||
const CONNECTION_REQUEST_TIMEOUT = +getConfig(
|
||||
snapshot,
|
||||
"cwmp.connectionRequestTimeout",
|
||||
context,
|
||||
now
|
||||
{},
|
||||
now,
|
||||
evalCallback
|
||||
);
|
||||
const CONNECTION_REQUEST_ALLOW_BASIC_AUTH = !!getConfig(
|
||||
snapshot,
|
||||
"cwmp.connectionRequestAllowBasicAuth",
|
||||
context,
|
||||
now
|
||||
{},
|
||||
now,
|
||||
evalCallback
|
||||
);
|
||||
let authExp: Expression = getConfigExpression(
|
||||
snapshot,
|
||||
@@ -114,64 +136,85 @@ export async function connectionRequest(deviceId): Promise<void> {
|
||||
"FUNC",
|
||||
"AUTH",
|
||||
["PARAM", "username"],
|
||||
["PARAM", "password"]
|
||||
["PARAM", "password"],
|
||||
] as Expression;
|
||||
}
|
||||
|
||||
const debug = !!getConfig(snapshot, "cwmp.debug", context, now);
|
||||
authExp = evaluate(authExp, {}, now, evalCallback);
|
||||
|
||||
let udpProm;
|
||||
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
|
||||
|
||||
let udpProm = Promise.resolve(false);
|
||||
if (udpConnectionRequestAddress) {
|
||||
udpProm = udpConnectionRequest(
|
||||
udpConnectionRequestAddress,
|
||||
authExp,
|
||||
context,
|
||||
UDP_CONNECTION_REQUEST_PORT,
|
||||
debug,
|
||||
deviceId
|
||||
);
|
||||
try {
|
||||
const u = new URL("udp://" + udpConnectionRequestAddress);
|
||||
udpProm = udpConnectionRequest(
|
||||
u.hostname,
|
||||
parseInt(u.port || "80"),
|
||||
authExp,
|
||||
UDP_CONNECTION_REQUEST_PORT,
|
||||
debug,
|
||||
deviceId
|
||||
).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
} catch (err) {
|
||||
// Ignore invalid address
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await httpConnectionRequest(
|
||||
connectionRequestUrl,
|
||||
authExp,
|
||||
context,
|
||||
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
|
||||
CONNECTION_REQUEST_TIMEOUT,
|
||||
debug,
|
||||
deviceId
|
||||
);
|
||||
} catch (err) {
|
||||
if (!udpProm) throw err;
|
||||
await udpProm;
|
||||
}
|
||||
const status = await httpConnectionRequest(
|
||||
connectionRequestUrl,
|
||||
authExp,
|
||||
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
|
||||
CONNECTION_REQUEST_TIMEOUT,
|
||||
debug,
|
||||
deviceId
|
||||
);
|
||||
|
||||
if (await udpProm) return "";
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
export async function watchTask(deviceId, taskId, timeout): Promise<string> {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const task = await db.tasksCollection.findOne(
|
||||
{ _id: taskId },
|
||||
{ projection: { _id: 1 } }
|
||||
export async function awaitSessionStart(
|
||||
deviceId: string,
|
||||
lastInform: number,
|
||||
timeout: number
|
||||
): Promise<boolean> {
|
||||
const now = Date.now();
|
||||
const device = await db.devicesCollection.findOne(
|
||||
{ _id: deviceId },
|
||||
{ projection: { _lastInform: 1 } }
|
||||
);
|
||||
if (!task) return "completed";
|
||||
const li = (device["_lastInform"] as Date).getTime();
|
||||
if (li > lastInform) return true;
|
||||
const token = await cache.get(`cwmp_session_${deviceId}`);
|
||||
if (token) return true;
|
||||
if (timeout < 500) return false;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
timeout -= Date.now() - now;
|
||||
return awaitSessionStart(deviceId, lastInform, timeout);
|
||||
}
|
||||
|
||||
const q = { _id: `${deviceId}:task_${taskId}` };
|
||||
const fault = await db.faultsCollection.findOne(q, {
|
||||
projection: { _id: 1 }
|
||||
});
|
||||
if (fault) return "fault";
|
||||
|
||||
if ((timeout -= 500) <= 0) return "timeout";
|
||||
|
||||
return watchTask(deviceId, taskId, timeout);
|
||||
export async function awaitSessionEnd(
|
||||
deviceId: string,
|
||||
timeout: number
|
||||
): Promise<boolean> {
|
||||
const now = Date.now();
|
||||
const token = await cache.get(`cwmp_session_${deviceId}`);
|
||||
if (!token) return true;
|
||||
if (timeout < 500) return false;
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
timeout -= Date.now() - now;
|
||||
return awaitSessionEnd(deviceId, timeout);
|
||||
}
|
||||
|
||||
function sanitizeTask(task): void {
|
||||
task.timestamp = new Date(task.timestamp || Date.now());
|
||||
if (task.expiry) {
|
||||
if (common.typeOf(task.expiry) === common.DATE_TYPE || isNaN(task.expiry))
|
||||
if (task.expiry instanceof Date || isNaN(task.expiry))
|
||||
task.expiry = new Date(task.expiry);
|
||||
else task.expiry = new Date(task.timestamp.getTime() + +task.expiry * 1000);
|
||||
}
|
||||
@@ -209,6 +252,10 @@ function sanitizeTask(task): void {
|
||||
break;
|
||||
|
||||
case "refreshObject":
|
||||
if (typeof task.objectName !== "string")
|
||||
throw new Error("Missing 'objectName' property");
|
||||
break;
|
||||
|
||||
case "deleteObject":
|
||||
if (typeof task.objectName !== "string" || !task.objectName.length)
|
||||
throw new Error("Missing 'objectName' property");
|
||||
@@ -226,11 +273,14 @@ function sanitizeTask(task): void {
|
||||
break;
|
||||
|
||||
case "download":
|
||||
if (typeof task.fileType !== "string" || !task.fileType.length)
|
||||
throw new Error("Missing 'fileType' property");
|
||||
// genieacs-gui sends file ID instead of fileName and fileType
|
||||
if (!task.file) {
|
||||
if (typeof task.fileType !== "string" || !task.fileType.length)
|
||||
throw new Error("Missing 'fileType' property");
|
||||
|
||||
if (typeof task.fileName !== "string" || !task.fileName.length)
|
||||
throw new Error("Missing 'fileName' property");
|
||||
if (typeof task.fileName !== "string" || !task.fileName.length)
|
||||
throw new Error("Missing 'fileName' property");
|
||||
}
|
||||
|
||||
if (
|
||||
task.targetFileName != null &&
|
||||
@@ -238,21 +288,43 @@ function sanitizeTask(task): void {
|
||||
)
|
||||
throw new Error("Invalid 'targetFileName' property");
|
||||
break;
|
||||
|
||||
case "provisions":
|
||||
if (
|
||||
!Array.isArray(task.provisions) ||
|
||||
!task.provisions.every((arr) =>
|
||||
arr.every(
|
||||
(s) =>
|
||||
s == null || ["boolean", "number", "string"].includes(typeof s)
|
||||
)
|
||||
)
|
||||
)
|
||||
throw new Error("Invalid 'provisions' property");
|
||||
break;
|
||||
|
||||
case "reboot":
|
||||
break;
|
||||
|
||||
case "factoryReset":
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Invalid task name");
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
export async function insertTasks(tasks): Promise<Task[]> {
|
||||
if (tasks && common.typeOf(tasks) !== common.ARRAY_TYPE) tasks = [tasks];
|
||||
else if (!tasks || tasks.length === 0) return tasks || [];
|
||||
export async function insertTasks(tasks: any[]): Promise<Task[]> {
|
||||
if (tasks && !Array.isArray(tasks)) tasks = [tasks];
|
||||
else if (!tasks?.length) return tasks || [];
|
||||
|
||||
for (const task of tasks) {
|
||||
sanitizeTask(task);
|
||||
if (task.uniqueKey) {
|
||||
await db.tasksCollection.deleteOne({
|
||||
device: task.device,
|
||||
uniqueKey: task.uniqueKey
|
||||
uniqueKey: task.uniqueKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -260,20 +332,20 @@ export async function insertTasks(tasks): Promise<Task[]> {
|
||||
return tasks;
|
||||
}
|
||||
|
||||
export async function deleteDevice(deviceId): Promise<void> {
|
||||
export async function deleteDevice(deviceId: string): Promise<void> {
|
||||
await Promise.all([
|
||||
db.tasksCollection.deleteMany({ device: deviceId }),
|
||||
db.devicesCollection.deleteOne({ _id: deviceId }),
|
||||
db.faultsCollection.deleteMany({
|
||||
_id: {
|
||||
$regex: `^${common.escapeRegExp(deviceId)}\\:`
|
||||
}
|
||||
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
|
||||
},
|
||||
}),
|
||||
db.operationsCollection.deleteMany({
|
||||
_id: {
|
||||
$regex: `^${common.escapeRegExp(deviceId)}\\:`
|
||||
}
|
||||
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
|
||||
},
|
||||
}),
|
||||
cache.del(`${deviceId}_tasks_faults_operations`)
|
||||
cache.del(`${deviceId}_tasks_faults_operations`),
|
||||
]);
|
||||
}
|
||||
+18
-23
@@ -19,15 +19,17 @@
|
||||
|
||||
import { createHash, randomBytes, pbkdf2 } from "crypto";
|
||||
|
||||
function parseHeaderFeilds(str: string): {} {
|
||||
function parseHeaderFeilds(str: string): Record<string, string> {
|
||||
const res = {};
|
||||
const parts = str.split(",");
|
||||
|
||||
let part;
|
||||
let part: string;
|
||||
while ((part = parts.shift()) != null) {
|
||||
const name = part.split("=", 1)[0];
|
||||
if (name.length === part.length)
|
||||
if (name.length === part.length) {
|
||||
if (!part.trim()) continue;
|
||||
throw new Error("Unable to parse auth header");
|
||||
}
|
||||
|
||||
let value = part.slice(name.length + 1);
|
||||
if (!/^\s*"/.test(value)) {
|
||||
@@ -50,7 +52,9 @@ function parseHeaderFeilds(str: string): {} {
|
||||
return res;
|
||||
}
|
||||
|
||||
export function parseAuthorizationHeader(authHeader): { method: string } {
|
||||
export function parseAuthorizationHeader(authHeader: string): {
|
||||
method: string;
|
||||
} {
|
||||
authHeader = authHeader.trim();
|
||||
const method = authHeader.split(" ", 1)[0];
|
||||
const res = { method: method };
|
||||
@@ -72,7 +76,9 @@ export function parseAuthorizationHeader(authHeader): { method: string } {
|
||||
return res;
|
||||
}
|
||||
|
||||
export function parseWwwAuthenticateHeader(authHeader): {} {
|
||||
export function parseWwwAuthenticateHeader(
|
||||
authHeader: string
|
||||
): Record<string, string> {
|
||||
authHeader = authHeader.trim();
|
||||
const method = authHeader.split(" ", 1)[0];
|
||||
const res = { method: method };
|
||||
@@ -80,7 +86,7 @@ export function parseWwwAuthenticateHeader(authHeader): {} {
|
||||
return res;
|
||||
}
|
||||
|
||||
export function basic(username, password): string {
|
||||
export function basic(username: string, password: string): string {
|
||||
return "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
|
||||
}
|
||||
|
||||
@@ -97,20 +103,12 @@ export function digest(
|
||||
nc?: string | Buffer
|
||||
): string {
|
||||
const ha1 = createHash("md5");
|
||||
ha1
|
||||
.update(username)
|
||||
.update(":")
|
||||
.update(realm)
|
||||
.update(":")
|
||||
.update(password);
|
||||
ha1.update(username).update(":").update(realm).update(":").update(password);
|
||||
// TODO support "MD5-sess" algorithm directive
|
||||
const ha1d = ha1.digest("hex");
|
||||
|
||||
const ha2 = createHash("md5");
|
||||
ha2
|
||||
.update(httpMethod)
|
||||
.update(":")
|
||||
.update(uri);
|
||||
ha2.update(httpMethod).update(":").update(uri);
|
||||
|
||||
if (qop === "auth-int") {
|
||||
const bodyHash = createHash("md5")
|
||||
@@ -122,10 +120,7 @@ export function digest(
|
||||
const ha2d = ha2.digest("hex");
|
||||
|
||||
const hash = createHash("md5");
|
||||
hash
|
||||
.update(ha1d)
|
||||
.update(":")
|
||||
.update(nonce);
|
||||
hash.update(ha1d).update(":").update(nonce);
|
||||
if (qop) {
|
||||
hash
|
||||
.update(":")
|
||||
@@ -146,7 +141,7 @@ export function solveDigest(
|
||||
uri: string | Buffer,
|
||||
httpMethod: string | Buffer,
|
||||
body: string | Buffer,
|
||||
authHeader
|
||||
authHeader: Record<string, string>
|
||||
): string {
|
||||
const cnonce = randomBytes(8).toString("hex");
|
||||
const nc = "00000001";
|
||||
@@ -183,7 +178,7 @@ export function solveDigest(
|
||||
return authString;
|
||||
}
|
||||
|
||||
export function generateSalt(length): Promise<string> {
|
||||
export function generateSalt(length: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
randomBytes(length, (err, rand) => {
|
||||
if (err) return void reject(err);
|
||||
@@ -192,7 +187,7 @@ export function generateSalt(length): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
export function hashPassword(pass, salt): Promise<string> {
|
||||
export function hashPassword(pass: string, salt: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
pbkdf2(pass, salt, 10000, 128, "sha512", (err, hash) => {
|
||||
if (err) return void reject(err);
|
||||
|
||||
+71
-93
@@ -17,124 +17,102 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { MongoClient, Collection } from "mongodb";
|
||||
import { Collection } from "mongodb";
|
||||
import { onConnect } from "./db";
|
||||
import * as config from "./config";
|
||||
|
||||
const CLOCK_SKEW_TOLERANCE = 30000;
|
||||
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
|
||||
|
||||
let mongoClient: MongoClient;
|
||||
let mongoCollection: Collection;
|
||||
let mongoTimeOffset = 0;
|
||||
let cacheCollection: Collection;
|
||||
|
||||
export async function connect(): Promise<void> {
|
||||
const MONGODB_CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
|
||||
mongoClient = await MongoClient.connect(MONGODB_CONNECTION_URL, {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
const db = mongoClient.db();
|
||||
mongoCollection = db.collection("cache");
|
||||
await mongoCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
|
||||
const now = Date.now();
|
||||
const res = await db.command({ hostInfo: 1 });
|
||||
mongoTimeOffset = res.system.currentTime.getTime() - now;
|
||||
onConnect(async (db) => {
|
||||
cacheCollection = db.collection("cache");
|
||||
await cacheCollection.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
|
||||
});
|
||||
|
||||
export async function get(key: string): Promise<string> {
|
||||
const res = await cacheCollection.findOne({ _id: key });
|
||||
if (res) return res["value"];
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function disconnect(): Promise<void> {
|
||||
if (mongoClient) await mongoClient.close();
|
||||
}
|
||||
|
||||
export async function get(key): Promise<any> {
|
||||
const expire = new Date(Date.now() - mongoTimeOffset);
|
||||
if (Array.isArray(key)) {
|
||||
const res = await mongoCollection.find({ _id: { $in: key } }).toArray();
|
||||
|
||||
const indices = {};
|
||||
key.forEach((v, i) => {
|
||||
indices[v] = i;
|
||||
});
|
||||
|
||||
const values = [];
|
||||
res.forEach(r => {
|
||||
if (r["expire"] > expire) values[indices[r["_id"]]] = r["value"];
|
||||
});
|
||||
|
||||
return values;
|
||||
} else {
|
||||
const res = await mongoCollection.findOne({ _id: { $in: [key] } });
|
||||
if (res && res["expire"] > expire) return res["value"];
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export async function del(key): Promise<void> {
|
||||
if (Array.isArray(key))
|
||||
await mongoCollection.deleteMany({ _id: { $in: key } });
|
||||
else await mongoCollection.deleteOne({ _id: key });
|
||||
export async function del(key: string): Promise<void> {
|
||||
await cacheCollection.deleteOne({ _id: key });
|
||||
}
|
||||
|
||||
export async function set(
|
||||
key: string,
|
||||
value: string | number,
|
||||
value: string,
|
||||
ttl: number = MAX_CACHE_TTL
|
||||
): Promise<void> {
|
||||
const expire = new Date(Date.now() - mongoTimeOffset + ttl * 1000);
|
||||
await mongoCollection.replaceOne(
|
||||
const timestamp = new Date();
|
||||
const expire = new Date(
|
||||
timestamp.getTime() + CLOCK_SKEW_TOLERANCE + ttl * 1000
|
||||
);
|
||||
await cacheCollection.replaceOne(
|
||||
{ _id: key },
|
||||
{ _id: key, value: value, expire: expire },
|
||||
{ _id: key, value, expire, timestamp },
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
|
||||
export async function pop(key): Promise<any> {
|
||||
const res = await mongoCollection.findOneAndDelete({ _id: key });
|
||||
|
||||
if (
|
||||
res &&
|
||||
res["value"] &&
|
||||
+res["value"]["expire"] - (Date.now() - mongoTimeOffset)
|
||||
)
|
||||
return res["value"]["value"];
|
||||
|
||||
export async function pop(key: string): Promise<any> {
|
||||
const res = await cacheCollection.findOneAndDelete({ _id: key });
|
||||
if (res?.["value"]) return res["value"]["value"];
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function lock(lockName, ttl): Promise<Function> {
|
||||
const token = Math.random()
|
||||
.toString(36)
|
||||
.slice(2);
|
||||
|
||||
async function unlockOrExtend(extendTtl): Promise<void> {
|
||||
if (!extendTtl) {
|
||||
const res = await mongoCollection.deleteOne({
|
||||
_id: lockName,
|
||||
value: token
|
||||
});
|
||||
if (res["result"]["n"] !== 1) throw new Error("Lock expired");
|
||||
} else {
|
||||
const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000);
|
||||
const res = await mongoCollection.updateOne(
|
||||
{ _id: lockName, value: token },
|
||||
{ expire: expire }
|
||||
);
|
||||
if (res["result"]["n"] !== 1) throw new Error("Lock expired");
|
||||
}
|
||||
}
|
||||
|
||||
const expireTest = new Date(Date.now() - mongoTimeOffset);
|
||||
const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000);
|
||||
|
||||
export async function acquireLock(
|
||||
lockName: string,
|
||||
ttl: number,
|
||||
timeout = 0,
|
||||
token = Math.random().toString(36).slice(2)
|
||||
): Promise<string> {
|
||||
try {
|
||||
await mongoCollection.updateOne(
|
||||
{ _id: lockName, expire: { $lte: expireTest } },
|
||||
{ $set: { value: token, expire: expireSet } },
|
||||
{ upsert: true }
|
||||
const now = Date.now();
|
||||
const r = await cacheCollection.findOneAndUpdate(
|
||||
{ _id: lockName, token },
|
||||
{
|
||||
$set: {
|
||||
value: token,
|
||||
expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE),
|
||||
},
|
||||
$currentDate: { timestamp: true as unknown as Date },
|
||||
},
|
||||
{ upsert: true, returnDocument: "after" }
|
||||
);
|
||||
const v = r.value;
|
||||
if (Math.abs(v["timestamp"].getTime() - now) > CLOCK_SKEW_TOLERANCE)
|
||||
throw new Error("Database clock skew too great");
|
||||
} catch (err) {
|
||||
if (err && err.code === 11000) {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
return lock(lockName, ttl);
|
||||
if (err.code === 11000) {
|
||||
if (timeout > 0) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const w = 50 + Math.random() * 50;
|
||||
setTimeout(() => {
|
||||
acquireLock(lockName, ttl, timeout - w, token).then(
|
||||
resolve,
|
||||
reject
|
||||
);
|
||||
}, w);
|
||||
});
|
||||
}
|
||||
throw new Error("Failed to acquire lock");
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return unlockOrExtend;
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function releaseLock(
|
||||
lockName: string,
|
||||
token: string
|
||||
): Promise<void> {
|
||||
const res = await cacheCollection.deleteOne({
|
||||
_id: lockName,
|
||||
value: token,
|
||||
});
|
||||
if (res.deletedCount !== 1) throw new Error("Lock expired");
|
||||
}
|
||||
+44
-15
@@ -17,19 +17,31 @@
|
||||
* 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 * as logger from "./logger";
|
||||
|
||||
let respawnTimestamp = 0;
|
||||
let tooManyCrashesTimestamp = 0;
|
||||
let crashes: number[] = [];
|
||||
|
||||
function fork(): Worker {
|
||||
const w = cluster.fork();
|
||||
w.on("error", (err: NodeJS.ErrnoException) => {
|
||||
// Avoid exception when attempting to kill the process just as it's exiting
|
||||
if (err.code !== "EPIPE") throw err;
|
||||
setTimeout(() => {
|
||||
if (!w.isDead()) throw err;
|
||||
}, 50);
|
||||
});
|
||||
return w;
|
||||
}
|
||||
|
||||
function restartWorker(worker, code, signal): void {
|
||||
const msg = {
|
||||
message: "Worker died",
|
||||
pid: worker.process.pid,
|
||||
exitCode: null,
|
||||
signal: null
|
||||
signal: null,
|
||||
};
|
||||
|
||||
if (code != null) msg.exitCode = code;
|
||||
@@ -39,32 +51,49 @@ function restartWorker(worker, code, signal): void {
|
||||
logger.error(msg);
|
||||
|
||||
const now = Date.now();
|
||||
respawnTimestamp = Math.max(now, respawnTimestamp + 2000);
|
||||
if (respawnTimestamp === now) {
|
||||
tooManyCrashesTimestamp = now;
|
||||
cluster.fork();
|
||||
return;
|
||||
}
|
||||
crashes.push(now);
|
||||
|
||||
if (now - tooManyCrashesTimestamp > 60000) {
|
||||
let min1 = 0,
|
||||
min2 = 0,
|
||||
min3 = 0;
|
||||
|
||||
crashes = crashes.filter((n) => {
|
||||
if (n > now - 60000) ++min1;
|
||||
else if (n > now - 120000) ++min2;
|
||||
else if (n > now - 180000) ++min3;
|
||||
else return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (min1 > 5 && min2 > 5 && min3 > 5) {
|
||||
process.exitCode = 1;
|
||||
cluster.removeListener("exit", restartWorker);
|
||||
for (const pid in cluster.workers) cluster.workers[pid].kill();
|
||||
|
||||
logger.error({
|
||||
message: "Too many crashes, exiting",
|
||||
pid: process.pid
|
||||
pid: process.pid,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
respawnTimestamp = Math.max(now, respawnTimestamp + 2000);
|
||||
if (respawnTimestamp === now) {
|
||||
fork();
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (process.exitCode) return;
|
||||
cluster.fork();
|
||||
fork();
|
||||
}, respawnTimestamp - now);
|
||||
}
|
||||
|
||||
export function start(workerCount, servicePort, serviceAddress): void {
|
||||
export function start(
|
||||
workerCount: number,
|
||||
servicePort: number,
|
||||
serviceAddress: string
|
||||
): void {
|
||||
cluster.on("listening", (worker, address) => {
|
||||
if (
|
||||
(address.addressType === 4 || address.addressType === 6) &&
|
||||
@@ -75,7 +104,7 @@ export function start(workerCount, servicePort, serviceAddress): void {
|
||||
message: "Worker listening",
|
||||
pid: worker.process.pid,
|
||||
address: address.address,
|
||||
port: address.port
|
||||
port: address.port,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -84,7 +113,7 @@ export function start(workerCount, servicePort, serviceAddress): void {
|
||||
|
||||
if (!workerCount) workerCount = Math.max(2, cpus().length);
|
||||
|
||||
for (let i = 0; i < workerCount; ++i) cluster.fork();
|
||||
for (let i = 0; i < workerCount; ++i) fork();
|
||||
}
|
||||
|
||||
export function stop(): void {
|
||||
|
||||
+19
-15
@@ -17,22 +17,12 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const UNDEFINED_TYPE = "[object Undefined]";
|
||||
export const NULL_TYPE = "[object Null]";
|
||||
export const BOOLEAN_TYPE = "[object Boolean]";
|
||||
export const NUMBER_TYPE = "[object Number]";
|
||||
export const STRING_TYPE = "[object String]";
|
||||
export const ARRAY_TYPE = "[object Array]";
|
||||
export const OBJECT_TYPE = "[object Object]";
|
||||
export const REGEXP_TYPE = "[object RegExp]";
|
||||
export const DATE_TYPE = "[object Date]";
|
||||
|
||||
export const typeOf = (obj): string => Object.prototype.toString.call(obj);
|
||||
|
||||
export function generateDeviceId(deviceIdStruct): string {
|
||||
export function generateDeviceId(
|
||||
deviceIdStruct: Record<string, string>
|
||||
): string {
|
||||
// Escapes everything except alphanumerics and underscore
|
||||
function esc(str): string {
|
||||
return str.replace(/[^A-Za-z0-9_]/g, chr => {
|
||||
return str.replace(/[^A-Za-z0-9_]/g, (chr) => {
|
||||
const buf = Buffer.from(chr, "utf8");
|
||||
let rep = "";
|
||||
for (const b of buf) rep += "%" + b.toString(16).toUpperCase();
|
||||
@@ -54,6 +44,20 @@ export function generateDeviceId(deviceIdStruct): string {
|
||||
}
|
||||
|
||||
// Source: http://stackoverflow.com/a/6969486
|
||||
export function escapeRegExp(str): string {
|
||||
export function escapeRegExp(str: string): string {
|
||||
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function encodeTag(tag: string): string {
|
||||
return encodeURIComponent(tag)
|
||||
.replace(
|
||||
/[!~*'().]/g,
|
||||
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
|
||||
)
|
||||
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
|
||||
.replace(/%/g, "0x");
|
||||
}
|
||||
|
||||
export function decodeTag(tag: string): string {
|
||||
return decodeURIComponent(tag.replace(/0x(?=[0-9A-Z]{2})/g, "%"));
|
||||
}
|
||||
+38
-12
@@ -18,23 +18,25 @@
|
||||
*/
|
||||
|
||||
import { PermissionSet, Expression } from "../types";
|
||||
import { evaluate } from "./expression";
|
||||
import { evaluate, or } from "./expression";
|
||||
|
||||
export default class Authorizer {
|
||||
private permissionSets: PermissionSet[];
|
||||
private validatorCache: WeakMap<
|
||||
object,
|
||||
private declare permissionSets: PermissionSet[];
|
||||
private declare validatorCache: WeakMap<
|
||||
any,
|
||||
(mutationType, mutation, any) => boolean
|
||||
>;
|
||||
private hasAccessCache: Map<string, boolean>;
|
||||
private declare hasAccessCache: Map<string, boolean>;
|
||||
private declare getFilterCache: Map<string, Expression>;
|
||||
|
||||
public constructor(permissionSets) {
|
||||
public constructor(permissionSets: PermissionSet[]) {
|
||||
this.permissionSets = permissionSets;
|
||||
this.validatorCache = new WeakMap();
|
||||
this.hasAccessCache = new Map();
|
||||
this.getFilterCache = new Map();
|
||||
}
|
||||
|
||||
public hasAccess(resourceType, access): boolean {
|
||||
public hasAccess(resourceType: string, access: number): boolean {
|
||||
const cacheKey = `${resourceType}-${access}`;
|
||||
if (this.hasAccessCache.has(cacheKey))
|
||||
return this.hasAccessCache.get(cacheKey);
|
||||
@@ -55,10 +57,29 @@ export default class Authorizer {
|
||||
return has;
|
||||
}
|
||||
|
||||
public getFilter(resourceType: string, access: number): Expression {
|
||||
const cacheKey = `${resourceType}-${access}`;
|
||||
if (this.getFilterCache.has(cacheKey))
|
||||
return this.getFilterCache.get(cacheKey);
|
||||
|
||||
let filter: Expression = null;
|
||||
for (const permissionSet of this.permissionSets) {
|
||||
for (const perm of permissionSet) {
|
||||
if (perm[resourceType]) {
|
||||
if (perm[resourceType].access >= access)
|
||||
filter = or(filter, perm[resourceType].filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.getFilterCache.set(cacheKey, filter);
|
||||
return filter;
|
||||
}
|
||||
|
||||
public getValidator(
|
||||
resourceType,
|
||||
resource
|
||||
): (mutationType: string, mutation: any, args: any) => boolean {
|
||||
resourceType: string,
|
||||
resource: unknown
|
||||
): (mutationType: string, mutation?: any, args?: any) => boolean {
|
||||
if (this.validatorCache.has(resource))
|
||||
return this.validatorCache.get(resource);
|
||||
|
||||
@@ -87,7 +108,7 @@ export default class Authorizer {
|
||||
mutation,
|
||||
resourceType,
|
||||
object: resource,
|
||||
options: any
|
||||
options: any,
|
||||
};
|
||||
|
||||
const valueFunction = (paramName): any => {
|
||||
@@ -103,7 +124,8 @@ export default class Authorizer {
|
||||
if (value == null) break;
|
||||
}
|
||||
} else if (object[entry]) {
|
||||
value = object[entry][paramName];
|
||||
if (paramName) value = object[entry][paramName];
|
||||
else value = object[entry];
|
||||
}
|
||||
|
||||
return value;
|
||||
@@ -120,4 +142,8 @@ export default class Authorizer {
|
||||
this.validatorCache.set(resource, validator);
|
||||
return validator;
|
||||
}
|
||||
|
||||
public getPermissionSets(): PermissionSet[] {
|
||||
return this.permissionSets;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright 2013-2020 GenieACS Inc.
|
||||
*
|
||||
* This file is part of GenieACS.
|
||||
*
|
||||
* GenieACS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* GenieACS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function and(a: bigint, b: bigint): bigint {
|
||||
return a & b;
|
||||
}
|
||||
|
||||
export function or(a: bigint, b: bigint): bigint {
|
||||
return a | b;
|
||||
}
|
||||
|
||||
export function xor(a: bigint, b: bigint): bigint {
|
||||
return a ^ b;
|
||||
}
|
||||
|
||||
export function not(a: bigint): bigint {
|
||||
return ~a;
|
||||
}
|
||||
|
||||
export function lshift(a: bigint, b: bigint): bigint {
|
||||
return a << b;
|
||||
}
|
||||
|
||||
export function rshift(a: bigint, b: bigint): bigint {
|
||||
return a >> b;
|
||||
}
|
||||
|
||||
export function add(a: bigint, b: bigint): bigint {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export function sub(a: bigint, b: bigint): bigint {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
export function mul(a: bigint, b: bigint): bigint {
|
||||
return a * b;
|
||||
}
|
||||
|
||||
export function div(a: bigint, b: bigint): bigint {
|
||||
return a / b;
|
||||
}
|
||||
|
||||
export function exp(a: bigint, b: bigint): bigint {
|
||||
return a ** b;
|
||||
}
|
||||
|
||||
export function rem(a: bigint, b: bigint): bigint {
|
||||
return a % b;
|
||||
}
|
||||
|
||||
export function toNumber(a: bigint): number {
|
||||
return Number(a);
|
||||
}
|
||||
|
||||
export function eq(a: bigint, b: bigint): boolean {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
export function ne(a: bigint, b: bigint): boolean {
|
||||
return a !== b;
|
||||
}
|
||||
|
||||
export function lt(a: bigint, b: bigint): boolean {
|
||||
return a < b;
|
||||
}
|
||||
|
||||
export function lte(a: bigint, b: bigint): boolean {
|
||||
return a <= b;
|
||||
}
|
||||
|
||||
export function gt(a: bigint, b: bigint): boolean {
|
||||
return a > b;
|
||||
}
|
||||
|
||||
export function gte(a: bigint, b: bigint): boolean {
|
||||
return a >= b;
|
||||
}
|
||||
|
||||
export function asUintN(a: number, b: bigint): bigint {
|
||||
return BigInt.asUintN(a, b);
|
||||
}
|
||||
|
||||
export function asIntN(a: number, b: bigint): bigint {
|
||||
return BigInt.asIntN(a, b);
|
||||
}
|
||||
|
||||
const _BigInt = BigInt;
|
||||
type _bigint = bigint;
|
||||
export { _BigInt as BigInt, _bigint as bigint };
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,285 +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 { map, parseLikePattern } from "./expression-parser";
|
||||
import { Expression } from "../types";
|
||||
|
||||
type CNF = number[][];
|
||||
|
||||
function or(cnf1: CNF, cnf2: CNF): CNF {
|
||||
if (cnf1.length === 0) return cnf2;
|
||||
else if (cnf2.length === 0) return cnf1;
|
||||
|
||||
if (cnf1.length > cnf2.length) {
|
||||
const c = cnf1;
|
||||
cnf1 = cnf2;
|
||||
cnf2 = c;
|
||||
}
|
||||
|
||||
const res = [];
|
||||
for (let i = 0; i < cnf1.length; ++i)
|
||||
for (let j = i; j < cnf2.length; ++j) res.push(cnf1[i].concat(cnf2[j]));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function likePatternIncludes(pat1, pat2, idx1 = 0, idx2 = 0): boolean {
|
||||
while (idx1 < pat1.length && idx2 < pat2.length) {
|
||||
if (pat1[idx1] === "\\%") {
|
||||
for (let i = idx2; i <= pat2.length; ++i)
|
||||
if (likePatternIncludes(pat1, pat2, idx1 + 1, i)) return true;
|
||||
return false;
|
||||
} else if (pat2[idx2] === "\\%") {
|
||||
if (pat1[idx1] !== "\\%") return false;
|
||||
} else if (pat1[idx1] === "\\_") {
|
||||
// Ignore
|
||||
} else if (pat2[idx2] === "\\_") {
|
||||
return false;
|
||||
} else if (pat2[idx2] !== pat1[idx1]) {
|
||||
return false;
|
||||
}
|
||||
++idx1;
|
||||
++idx2;
|
||||
}
|
||||
|
||||
if (idx1 === pat1.length && idx2 === pat2.length) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function booleanCnf(exp: Expression): { vars: number; clauses: CNF } {
|
||||
const ranges = new Map();
|
||||
const likePatterns = new Map();
|
||||
map(exp, e => {
|
||||
if (!Array.isArray(e)) return e;
|
||||
if (e[0] === "LIKE" || e[0] === "NOT LIKE") {
|
||||
if (
|
||||
typeof e[2] === "string" &&
|
||||
(e.length >= 4 || (e[3] != null && !Array.isArray(e[3])))
|
||||
) {
|
||||
const key = JSON.stringify(e[1]);
|
||||
let s = likePatterns.get(key);
|
||||
if (!s) likePatterns.set(key, (s = new Map()));
|
||||
s.set(`${e[3] || ""}:${e[2]}`, parseLikePattern(e[2], e[3]));
|
||||
}
|
||||
} else if ([">", ">=", "<", "<=", "=", "<>"].includes(e[0])) {
|
||||
if (!Array.isArray(e[2])) {
|
||||
const key = `${JSON.stringify(e[1])}:${typeof e[2]}`;
|
||||
let s = ranges.get(key);
|
||||
if (!s) ranges.set(key, (s = new Set()));
|
||||
s.add(e[2]);
|
||||
} else if (!Array.isArray(e[1])) {
|
||||
const key = `${JSON.stringify(e[2])}:${typeof e[1]}`;
|
||||
let s = ranges.get(key);
|
||||
if (!s) ranges.set(key, (s = new Set()));
|
||||
s.add(e[2]);
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
|
||||
for (const [k, v] of ranges) {
|
||||
if (k.endsWith(":number")) {
|
||||
ranges.set(
|
||||
k,
|
||||
Array.from(v as number[]).sort((a: number, b: number) => a - b)
|
||||
);
|
||||
} else {
|
||||
ranges.set(k, Array.from(v).sort());
|
||||
}
|
||||
}
|
||||
|
||||
const expressions: Map<string, number> = new Map();
|
||||
const mutuallyExclusive: Map<string, Set<number>> = new Map();
|
||||
|
||||
function getVariable(lhs, op, rhs = ""): number {
|
||||
const key = `${lhs}:${op}:${rhs}`;
|
||||
let f = expressions.get(key);
|
||||
if (!f) {
|
||||
expressions.set(key, (f = expressions.size + 1));
|
||||
let m = mutuallyExclusive.get(lhs);
|
||||
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
|
||||
m.add(f);
|
||||
if (rhs || op !== "null") m.add(getVariable(lhs, "null"));
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
function getComparisonVariables(
|
||||
lhs,
|
||||
from,
|
||||
fromInclusive,
|
||||
to,
|
||||
toInclusive
|
||||
): number[] {
|
||||
const type = from != null ? typeof from : typeof to;
|
||||
const range = ranges.get(`${lhs}:${type}`);
|
||||
const vars = [];
|
||||
|
||||
if (from == null) vars.push(`${lhs}:--${range[0]}`);
|
||||
|
||||
for (let i = 0; i < range.length; ++i) {
|
||||
if (
|
||||
i > 0 &&
|
||||
(from == null || range[i] > from) &&
|
||||
(to == null || range[i] <= to)
|
||||
)
|
||||
vars.push(`${lhs}:${range[i - 1]}--${range[i]}`);
|
||||
|
||||
if (
|
||||
(range[i] > from && range[i] < to) ||
|
||||
(range[i] === from && fromInclusive) ||
|
||||
(range[i] === to && toInclusive)
|
||||
)
|
||||
vars.push(`${lhs}:${range[i]}`);
|
||||
}
|
||||
|
||||
if (to == null) vars.push(`${lhs}:${range[range.length - 1]}--`);
|
||||
|
||||
return vars.map(key => {
|
||||
let f = expressions.get(key);
|
||||
if (!f) {
|
||||
expressions.set(key, (f = expressions.size + 1));
|
||||
let m = mutuallyExclusive.get(lhs);
|
||||
if (!m) mutuallyExclusive.set(lhs, (m = new Set()));
|
||||
m.add(f);
|
||||
}
|
||||
return f;
|
||||
});
|
||||
}
|
||||
|
||||
function getLikeVariables(lhs, pat, esc): number[] {
|
||||
const vars = [];
|
||||
const pats = likePatterns.get(lhs);
|
||||
pat = pats.get(`${esc || ""}:${pat}`);
|
||||
for (const p of pats.values()) {
|
||||
if (pat === p || likePatternIncludes(pat, p))
|
||||
vars.push(`${lhs}:like:${JSON.stringify(p)}`);
|
||||
}
|
||||
|
||||
return vars.map(key => {
|
||||
let f = expressions.get(key);
|
||||
if (!f) expressions.set(key, (f = expressions.size + 1));
|
||||
return f;
|
||||
});
|
||||
}
|
||||
|
||||
interface ComparisonOperators {
|
||||
[name: string]: (v: string) => [string, boolean, string, boolean];
|
||||
}
|
||||
|
||||
const comparisonOperators: ComparisonOperators = {
|
||||
"=": v => [v, true, v, true],
|
||||
">": v => [v, false, null, null],
|
||||
">=": v => [v, true, null, null],
|
||||
"<": v => [null, null, v, false],
|
||||
"<=": v => [null, null, v, true]
|
||||
};
|
||||
|
||||
function recursive(clause, negate): CNF {
|
||||
let op = clause[0];
|
||||
|
||||
if ((op === "AND" && !negate) || (op === "OR" && negate)) {
|
||||
let res = [];
|
||||
for (let i = 1; i < clause.length; ++i)
|
||||
res = res.concat(recursive(clause[i], negate));
|
||||
return res;
|
||||
} else if ((op === "OR" && !negate) || (op === "AND" && negate)) {
|
||||
let res = [];
|
||||
for (let i = 1; i < clause.length; ++i)
|
||||
res = or(res, recursive(clause[i], negate));
|
||||
return res;
|
||||
} else if (op === "NOT") {
|
||||
return recursive(clause[1], !negate);
|
||||
} else if (op === "<>") {
|
||||
const vars = getComparisonVariables(
|
||||
JSON.stringify(clause[1]),
|
||||
...comparisonOperators["="](clause[2])
|
||||
);
|
||||
if (!negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else if (op === "IS NULL") {
|
||||
const v = getVariable(JSON.stringify(clause[1]), "null");
|
||||
if (negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
} else if (op === "IS NOT NULL") {
|
||||
const v = getVariable(JSON.stringify(clause[1]), "null");
|
||||
if (!negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
} else if (op in comparisonOperators) {
|
||||
if (!Array.isArray(clause[2])) {
|
||||
const vars = getComparisonVariables(
|
||||
JSON.stringify(clause[1]),
|
||||
...comparisonOperators[op](clause[2])
|
||||
);
|
||||
if (negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else if (!Array.isArray(clause[1])) {
|
||||
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
|
||||
const vars = getComparisonVariables(
|
||||
JSON.stringify(clause[2]),
|
||||
...comparisonOperators[op](clause[1])
|
||||
);
|
||||
if (negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else {
|
||||
let lhs = JSON.stringify(clause[1]);
|
||||
let rhs = JSON.stringify(clause[2]);
|
||||
// For consistency
|
||||
if (rhs > lhs) {
|
||||
const t = rhs;
|
||||
rhs = lhs;
|
||||
lhs = t;
|
||||
op = { ">": "<", ">=": "<=", "<": ">", "<=": ">=" }[op] || op;
|
||||
}
|
||||
|
||||
const v = getVariable(lhs, op, rhs);
|
||||
if (negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
}
|
||||
} else if (
|
||||
(op === "LIKE" || op === "NOT LIKE") &&
|
||||
!Array.isArray(clause[2]) &&
|
||||
clause[2] != null &&
|
||||
(clause.length >= 4 || clause[3] != null)
|
||||
) {
|
||||
if (op === "NOT LIKE") negate = !negate;
|
||||
const vars = getLikeVariables(
|
||||
JSON.stringify(clause[1]),
|
||||
clause[2],
|
||||
clause[3]
|
||||
);
|
||||
if (negate) return vars.map(x => [0 - x]);
|
||||
else return [vars];
|
||||
} else {
|
||||
const v = getVariable(JSON.stringify(clause), "");
|
||||
if (negate) return [[0 - v]];
|
||||
else return [[v]];
|
||||
}
|
||||
}
|
||||
|
||||
const cnf = recursive(exp, false);
|
||||
|
||||
for (const m of mutuallyExclusive.values()) {
|
||||
const ar: number[] = Array.from(m);
|
||||
for (let i = 0; i < ar.length; ++i)
|
||||
for (let j = i + 1; j < ar.length; ++j) cnf.push([0 - ar[i], 0 - ar[j]]);
|
||||
}
|
||||
|
||||
return { vars: expressions.size, clauses: cnf };
|
||||
}
|
||||
+115
-49
@@ -17,7 +17,7 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as parsimmon from "parsimmon";
|
||||
import parsimmon from "parsimmon";
|
||||
import { Expression } from "../types";
|
||||
|
||||
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
|
||||
@@ -27,9 +27,9 @@ function interpretEscapes(str): string {
|
||||
f: "\f",
|
||||
n: "\n",
|
||||
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 hex = escape.slice(1);
|
||||
if (type === "u") return String.fromCharCode(parseInt(hex, 16));
|
||||
@@ -40,7 +40,10 @@ function interpretEscapes(str): string {
|
||||
});
|
||||
}
|
||||
|
||||
export function map(exp, callback): Expression {
|
||||
export function map(
|
||||
exp: Expression,
|
||||
callback: (e: Expression) => Expression
|
||||
): Expression {
|
||||
if (!Array.isArray(exp)) return callback(exp);
|
||||
|
||||
let clone;
|
||||
@@ -55,7 +58,10 @@ export function map(exp, callback): Expression {
|
||||
return callback(clone || exp);
|
||||
}
|
||||
|
||||
export async function mapAsync(exp, callback): Promise<Expression> {
|
||||
export async function mapAsync(
|
||||
exp: Expression,
|
||||
callback: (e: Expression) => Promise<Expression>
|
||||
): Promise<Expression> {
|
||||
if (!Array.isArray(exp)) return callback(exp);
|
||||
|
||||
let clone;
|
||||
@@ -70,7 +76,10 @@ export async function mapAsync(exp, callback): Promise<Expression> {
|
||||
return callback(clone || exp);
|
||||
}
|
||||
|
||||
function binaryLeft(operatorsParser, nextParser): parsimmon.Parser<{}> {
|
||||
function binaryLeft(
|
||||
operatorsParser: parsimmon.Parser<string>,
|
||||
nextParser: parsimmon.Parser<Expression>
|
||||
): parsimmon.Parser<Expression> {
|
||||
return parsimmon.seqMap(
|
||||
nextParser,
|
||||
parsimmon.seq(operatorsParser, nextParser).many(),
|
||||
@@ -86,7 +95,7 @@ function binaryLeft(operatorsParser, nextParser): parsimmon.Parser<{}> {
|
||||
}
|
||||
|
||||
const lang = parsimmon.createLanguage({
|
||||
ComparisonOperator: function() {
|
||||
ComparisonOperator: function () {
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon.string(">="),
|
||||
@@ -98,13 +107,10 @@ const lang = parsimmon.createLanguage({
|
||||
)
|
||||
.skip(parsimmon.optWhitespace);
|
||||
},
|
||||
LikeOperator: function() {
|
||||
LikeOperator: function () {
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon
|
||||
.regexp(/like/i)
|
||||
.result("LIKE")
|
||||
.desc("LIKE"),
|
||||
parsimmon.regexp(/like/i).result("LIKE").desc("LIKE"),
|
||||
parsimmon
|
||||
.regexp(/not\s+like/i)
|
||||
.result("NOT LIKE")
|
||||
@@ -113,7 +119,7 @@ const lang = parsimmon.createLanguage({
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace);
|
||||
},
|
||||
IsNullOperator: function() {
|
||||
IsNullOperator: function () {
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon
|
||||
@@ -128,7 +134,7 @@ const lang = parsimmon.createLanguage({
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace);
|
||||
},
|
||||
NotOperator: function() {
|
||||
NotOperator: function () {
|
||||
return parsimmon
|
||||
.regexp(/not/i)
|
||||
.result("NOT")
|
||||
@@ -136,7 +142,7 @@ const lang = parsimmon.createLanguage({
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("NOT");
|
||||
},
|
||||
AndOperator: function() {
|
||||
AndOperator: function () {
|
||||
return parsimmon
|
||||
.regexp(/and/i)
|
||||
.result("AND")
|
||||
@@ -144,7 +150,7 @@ const lang = parsimmon.createLanguage({
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("AND");
|
||||
},
|
||||
OrOperator: function() {
|
||||
OrOperator: function () {
|
||||
return parsimmon
|
||||
.regexp(/or/i)
|
||||
.result("OR")
|
||||
@@ -152,7 +158,7 @@ const lang = parsimmon.createLanguage({
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("OR");
|
||||
},
|
||||
Parameter: function(r) {
|
||||
Parameter: function (r) {
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon.regexp(/[a-zA-Z0-9_.*-]+/),
|
||||
@@ -162,48 +168,43 @@ const lang = parsimmon.createLanguage({
|
||||
)
|
||||
)
|
||||
.atLeast(1)
|
||||
.map(x => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]])
|
||||
.map((x) => ["PARAM", x.length > 1 ? ["||"].concat(x) : x[0]])
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("parameter");
|
||||
},
|
||||
StringValueSql: function() {
|
||||
StringValueSql: function () {
|
||||
return parsimmon
|
||||
.regexp(/'([^']*)'/, 1)
|
||||
.atLeast(1)
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.map(s => s.join("'"))
|
||||
.map((s) => s.join("'"))
|
||||
.desc("string");
|
||||
},
|
||||
StringValueJs: function() {
|
||||
StringValueJs: function () {
|
||||
return parsimmon
|
||||
.regexp(/"((?:\\.|.)*?)"/, 1)
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.map(interpretEscapes)
|
||||
.desc("string");
|
||||
},
|
||||
NumberValue: function() {
|
||||
NumberValue: function () {
|
||||
return parsimmon
|
||||
.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.map(Number)
|
||||
.desc("number");
|
||||
},
|
||||
BooleanValue: function() {
|
||||
BooleanValue: function () {
|
||||
return parsimmon
|
||||
.alt(
|
||||
parsimmon
|
||||
.regexp(/true/i)
|
||||
.result(true)
|
||||
.desc("TRUE"),
|
||||
parsimmon
|
||||
.regexp(/false/i)
|
||||
.result(false)
|
||||
.desc("FALSE")
|
||||
parsimmon.regexp(/true/i).result(true).desc("TRUE"),
|
||||
parsimmon.regexp(/false/i).result(false).desc("FALSE")
|
||||
)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace);
|
||||
},
|
||||
NullValue: function() {
|
||||
NullValue: function () {
|
||||
return parsimmon
|
||||
.regexp(/null/i)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
@@ -211,32 +212,73 @@ const lang = parsimmon.createLanguage({
|
||||
.result(null)
|
||||
.desc("NULL");
|
||||
},
|
||||
FuncValue: function(r) {
|
||||
FuncValue: function (r) {
|
||||
return parsimmon.seqMap(
|
||||
parsimmon
|
||||
.regexp(/([a-zA-Z0-9_]+)/, 1)
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("function"),
|
||||
r.ValueExpression.sepBy(
|
||||
parsimmon.string(",").skip(parsimmon.optWhitespace)
|
||||
).wrap(
|
||||
r.ExpressionList.wrap(
|
||||
parsimmon.string("(").skip(parsimmon.optWhitespace),
|
||||
parsimmon.string(")").skip(parsimmon.optWhitespace)
|
||||
),
|
||||
(f, args) => ["FUNC", f.toUpperCase()].concat(args)
|
||||
);
|
||||
},
|
||||
Value: function(r) {
|
||||
WhenPair: function (r) {
|
||||
return parsimmon.seq(
|
||||
parsimmon
|
||||
.regexp(/when/i)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("WHEN")
|
||||
.then(r.Expression),
|
||||
parsimmon
|
||||
.regexp(/then/i)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("THEN")
|
||||
.then(r.Expression)
|
||||
);
|
||||
},
|
||||
CaseStatement: function (r) {
|
||||
return parsimmon.seqMap(
|
||||
parsimmon
|
||||
.regexp(/case/i)
|
||||
.result("CASE")
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("CASE"),
|
||||
r.WhenPair.many(),
|
||||
parsimmon
|
||||
.regexp(/else/i)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
.skip(parsimmon.optWhitespace)
|
||||
.desc("ELSE")
|
||||
.then(r.Expression)
|
||||
.map((e) => [[true, e]])
|
||||
.fallback(null)
|
||||
.skip(
|
||||
parsimmon
|
||||
.regex(/end/i)
|
||||
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
|
||||
)
|
||||
.skip(parsimmon.optWhitespace),
|
||||
(...arr) => arr.flat(2)
|
||||
);
|
||||
},
|
||||
Value: function (r) {
|
||||
return parsimmon.alt(
|
||||
r.NullValue,
|
||||
r.BooleanValue,
|
||||
r.NumberValue,
|
||||
r.StringValueSql,
|
||||
r.StringValueJs,
|
||||
r.FuncValue
|
||||
r.FuncValue,
|
||||
r.CaseStatement
|
||||
);
|
||||
},
|
||||
ValueExpression: function(r) {
|
||||
ValueExpression: function (r) {
|
||||
return binaryLeft(
|
||||
parsimmon.string("||").skip(parsimmon.optWhitespace),
|
||||
binaryLeft(
|
||||
@@ -259,7 +301,7 @@ const lang = parsimmon.createLanguage({
|
||||
)
|
||||
);
|
||||
},
|
||||
Comparison: function(r) {
|
||||
Comparison: function (r) {
|
||||
return parsimmon.alt(
|
||||
parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]),
|
||||
parsimmon.seqMap(
|
||||
@@ -289,8 +331,16 @@ const lang = parsimmon.createLanguage({
|
||||
)
|
||||
);
|
||||
},
|
||||
Expression: function(r) {
|
||||
function unary(operatorsParser, nextParser): parsimmon.Parser<{}> {
|
||||
ExpressionList: function (r) {
|
||||
return r.Expression.sepBy(
|
||||
parsimmon.string(",").skip(parsimmon.optWhitespace)
|
||||
);
|
||||
},
|
||||
Expression: function (r) {
|
||||
function unary(
|
||||
operatorsParser: parsimmon.Parser<string>,
|
||||
nextParser: parsimmon.Parser<Expression>
|
||||
): parsimmon.Parser<Expression> {
|
||||
return parsimmon.seq(operatorsParser, nextParser).or(nextParser);
|
||||
}
|
||||
|
||||
@@ -301,7 +351,7 @@ const lang = parsimmon.createLanguage({
|
||||
unary(r.NotOperator, r.Comparison.or(r.ValueExpression))
|
||||
)
|
||||
).trim(parsimmon.optWhitespace);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export function parse(str: string): Expression {
|
||||
@@ -309,7 +359,12 @@ export function parse(str: string): Expression {
|
||||
return lang.Expression.tryParse(str);
|
||||
}
|
||||
|
||||
export function stringify(exp, level = 0): string {
|
||||
export function parseList(str: string): Expression[] {
|
||||
if (!str) return [];
|
||||
return lang.ExpressionList.tryParse(str);
|
||||
}
|
||||
|
||||
export function stringify(exp: Expression, level = 0): string {
|
||||
if (!Array.isArray(exp)) return JSON.stringify(exp);
|
||||
|
||||
const opLevels = {
|
||||
@@ -330,7 +385,7 @@ export function stringify(exp, level = 0): string {
|
||||
"+": 31,
|
||||
"-": 31,
|
||||
"*": 32,
|
||||
"/": 32
|
||||
"/": 32,
|
||||
};
|
||||
|
||||
const op = exp[0].toUpperCase();
|
||||
@@ -344,7 +399,7 @@ export function stringify(exp, level = 0): string {
|
||||
return wrap(
|
||||
`${exp[1]}(${exp
|
||||
.slice(2)
|
||||
.map(e => stringify(e))
|
||||
.map((e) => stringify(e))
|
||||
.join(", ")})`
|
||||
);
|
||||
} else if (op === "PARAM") {
|
||||
@@ -354,7 +409,7 @@ export function stringify(exp, level = 0): string {
|
||||
return wrap(
|
||||
exp[1]
|
||||
.slice(1)
|
||||
.map(p => {
|
||||
.map((p) => {
|
||||
if (typeof p === "string") return p;
|
||||
else return `{${stringify(p)}}`;
|
||||
})
|
||||
@@ -381,6 +436,17 @@ export function stringify(exp, level = 0): string {
|
||||
)}`
|
||||
);
|
||||
}
|
||||
} else if (op === "CASE") {
|
||||
const parts: string[] = ["CASE"];
|
||||
for (let i = 1; i < exp.length - 1; i += 2) {
|
||||
if (!Array.isArray(exp[i]) && exp[i]) {
|
||||
if (exp[i + 1] != null) parts.push("ELSE", stringify(exp[i + 1]));
|
||||
break;
|
||||
}
|
||||
parts.push("WHEN", stringify(exp[i]), "THEN", stringify(exp[i + 1]));
|
||||
}
|
||||
parts.push("END");
|
||||
return parts.join(" ");
|
||||
} else if (op in opLevels) {
|
||||
const parts = exp.slice(1).map((e, i) => {
|
||||
return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0));
|
||||
@@ -393,7 +459,7 @@ export function stringify(exp, level = 0): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseLikePattern(pat, esc): string[] {
|
||||
export function parseLikePattern(pat: string, esc: string): string[] {
|
||||
const chars = pat.split("");
|
||||
|
||||
for (let i = 0; i < chars.length; ++i) {
|
||||
@@ -408,5 +474,5 @@ export function parseLikePattern(pat, esc): string[] {
|
||||
while (chars[i + 1] === "%") chars[++i] = "";
|
||||
}
|
||||
}
|
||||
return chars.filter(c => c);
|
||||
return chars.filter((c) => c);
|
||||
}
|
||||
+239
-168
@@ -18,13 +18,11 @@
|
||||
*/
|
||||
|
||||
import { map, mapAsync, parseLikePattern } from "./expression-parser";
|
||||
import { booleanCnf } from "./expression-cnf";
|
||||
import { naiveDpll } from "./sat-solver";
|
||||
import { Expression } from "../types";
|
||||
|
||||
const isArray = Array.isArray;
|
||||
|
||||
const regExpCache = new WeakMap<object, RegExp>();
|
||||
const regExpCache = new WeakMap<any, RegExp>();
|
||||
|
||||
const REDUCE_SKIP = {};
|
||||
function reduce(exp, callback): Expression {
|
||||
@@ -44,7 +42,7 @@ function reduce(exp, callback): Expression {
|
||||
return exp;
|
||||
}
|
||||
|
||||
export function likePatternToRegExp(pat, esc = "", flags = ""): RegExp {
|
||||
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
|
||||
const convChars = {
|
||||
"-": "\\-",
|
||||
"/": "\\/",
|
||||
@@ -63,159 +61,240 @@ export function likePatternToRegExp(pat, esc = "", flags = ""): RegExp {
|
||||
"{": "\\{",
|
||||
"}": "\\}",
|
||||
"\\%": ".*",
|
||||
"\\_": "."
|
||||
"\\_": ".",
|
||||
};
|
||||
let chars = parseLikePattern(pat, esc);
|
||||
if (!chars.length) return new RegExp("^$", flags);
|
||||
chars = chars.map(c => convChars[c] || c);
|
||||
chars = chars.map((c) => convChars[c] || c);
|
||||
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
|
||||
const l = chars.length - 1;
|
||||
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
|
||||
return new RegExp(chars.join(""), flags);
|
||||
}
|
||||
|
||||
function evalExp(e: Expression): Expression {
|
||||
if (e[0] === "AND") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a)) return a ? b : a;
|
||||
if (!isArray(b)) return b ? a : b;
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
} else if (e[0] === "OR") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a)) return a ? a : b;
|
||||
if (!isArray(b)) return b ? b : a;
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
} else if (e[0] === "NOT") {
|
||||
if (!isArray(e[1])) return !e[1];
|
||||
else if (e[1][0] === "NOT") return e[1][1];
|
||||
} else if (e[0] === "IS NULL") {
|
||||
if (isArray(e[1])) return e;
|
||||
else if (e[1] == null) return true;
|
||||
else return null;
|
||||
} else if (e[0] === "IS NOT NULL") {
|
||||
if (isArray(e[1])) return e;
|
||||
else if (e[1] != null) return true;
|
||||
else return null;
|
||||
} else if (e[0] === "LIKE") {
|
||||
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
|
||||
function compare(
|
||||
a: boolean | number | string,
|
||||
b: boolean | number | string
|
||||
): number {
|
||||
if (typeof a === "boolean") a = +a;
|
||||
if (typeof b === "boolean") b = +b;
|
||||
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
|
||||
return a > b ? 1 : a < b ? -1 : 0;
|
||||
}
|
||||
|
||||
function toNumber(a: boolean | number | string): number {
|
||||
switch (typeof a) {
|
||||
case "number":
|
||||
return a;
|
||||
case "boolean":
|
||||
return +a;
|
||||
case "string":
|
||||
return parseFloat(a) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
function toString(a: boolean | number | string): string {
|
||||
switch (typeof a) {
|
||||
case "string":
|
||||
return a;
|
||||
case "number":
|
||||
return a.toString();
|
||||
case "boolean":
|
||||
return (+a).toString();
|
||||
}
|
||||
}
|
||||
|
||||
export function evaluateCallback(exp: Expression): Expression {
|
||||
if (!Array.isArray(exp)) return exp;
|
||||
if (exp[0] === "CASE") {
|
||||
for (let i = 1; i < exp.length; i += 2) {
|
||||
if (Array.isArray(exp[i])) return exp;
|
||||
if (exp[i]) return exp[i + 1];
|
||||
}
|
||||
return null;
|
||||
} else if (exp[0] === "FUNC") {
|
||||
if (exp[1] === "COALESCE") {
|
||||
const args: Expression[] = [];
|
||||
for (let i = 2; i < exp.length; ++i) {
|
||||
const e = exp[i];
|
||||
if (e == null) continue;
|
||||
args.push(e);
|
||||
if (!Array.isArray(e)) break;
|
||||
}
|
||||
if (!args.length) return null;
|
||||
if (args.length === 1) return args[0];
|
||||
return ["FUNC", "COALESCE", ...args];
|
||||
} else if (exp[1] === "UPPER") {
|
||||
if (exp[2] == null) return null;
|
||||
if (!isArray(exp[2])) return toString(exp[2]).toUpperCase();
|
||||
} else if (exp[1] === "LOWER") {
|
||||
if (exp[2] == null) return null;
|
||||
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
|
||||
} else if (exp[1] === "ROUND") {
|
||||
const n = exp[2];
|
||||
const p = exp.length > 3 ? exp[3] : 0;
|
||||
if (n == null || p == null) return null;
|
||||
if (!isArray(n) && !isArray(p)) {
|
||||
const d = 10 ** p;
|
||||
const m = n * d * (1 + Number.EPSILON);
|
||||
return Math.round(m) / d;
|
||||
}
|
||||
}
|
||||
} else if (exp[0] === "PARAM") {
|
||||
if (exp[1] == null) return null;
|
||||
} else if (exp[0] === "AND") {
|
||||
for (let i = 1; i < exp.length; ++i)
|
||||
if (!Array.isArray(exp[i]) && exp[i] != null && !exp[i]) return false;
|
||||
const args: Expression[] = [];
|
||||
for (let i = 1; i < exp.length; ++i) {
|
||||
const ee = exp[i];
|
||||
if (ee == null) return null;
|
||||
if (Array.isArray(ee)) {
|
||||
if (ee[0] === "AND") args.push(...ee.slice(1));
|
||||
else args.push(ee);
|
||||
}
|
||||
}
|
||||
if (!args.length) return true;
|
||||
if (args.length === 1) args.push(true);
|
||||
return ["AND", ...args];
|
||||
} else if (exp[0] === "OR") {
|
||||
const args: Expression[] = [];
|
||||
for (let i = 1; i < exp.length; ++i) {
|
||||
const ee = exp[i];
|
||||
if (Array.isArray(ee)) {
|
||||
if (ee[0] === "OR") args.push(...ee.slice(1));
|
||||
else args.push(ee);
|
||||
} else if (ee) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!args.length) return exp.some((ee) => ee == null) ? null : false;
|
||||
if (args.length === 1) args.push(false);
|
||||
return ["OR", ...args];
|
||||
} else if (exp[0] === "NOT") {
|
||||
if (exp[1] == null) return null;
|
||||
if (!isArray(exp[1])) return !exp[1];
|
||||
else if (exp[1][0] === "NOT") return exp[1][1];
|
||||
} else if (exp[0] === "IS NULL") {
|
||||
if (isArray(exp[1])) return exp;
|
||||
else if (exp[1] == null) return true;
|
||||
else return false;
|
||||
} else if (exp[0] === "IS NOT NULL") {
|
||||
if (isArray(exp[1])) return exp;
|
||||
else if (exp[1] != null) return true;
|
||||
else return false;
|
||||
} else if (exp[0] === "LIKE") {
|
||||
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
|
||||
else if (
|
||||
e[1] == null ||
|
||||
e[2] == null ||
|
||||
((e as any[]).length >= 4 && e[3] == null)
|
||||
exp[1] == null ||
|
||||
exp[2] == null ||
|
||||
((exp as any[]).length >= 4 && exp[3] == null)
|
||||
)
|
||||
return null;
|
||||
let r = regExpCache.get(e as any[]);
|
||||
let r = regExpCache.get(exp as any[]);
|
||||
if (!r) {
|
||||
r = likePatternToRegExp(e[2], e[3]);
|
||||
regExpCache.set(e as any[], r);
|
||||
r = likePatternToRegExp(exp[2], exp[3]);
|
||||
regExpCache.set(exp as any[], r);
|
||||
}
|
||||
return r.test(e[1]);
|
||||
} else if (e[0] === "NOT LIKE") {
|
||||
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
|
||||
return r.test(exp[1]);
|
||||
} else if (exp[0] === "NOT LIKE") {
|
||||
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
|
||||
else if (
|
||||
e[1] == null ||
|
||||
e[2] == null ||
|
||||
((e as any[]).length >= 4 && e[3] == null)
|
||||
exp[1] == null ||
|
||||
exp[2] == null ||
|
||||
((exp as any[]).length >= 4 && exp[3] == null)
|
||||
)
|
||||
return null;
|
||||
let r = regExpCache.get(e as any[]);
|
||||
let r = regExpCache.get(exp as any[]);
|
||||
if (!r) {
|
||||
r = likePatternToRegExp(e[2], e[3]);
|
||||
regExpCache.set(e as any[], r);
|
||||
r = likePatternToRegExp(exp[2], exp[3]);
|
||||
regExpCache.set(exp as any[], r);
|
||||
}
|
||||
return !r.test(e[1]);
|
||||
} else if (e[0] === "=") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] === e[2];
|
||||
} else if (e[0] === "<>") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] !== e[2];
|
||||
} else if (e[0] === ">") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] > e[2];
|
||||
} else if (e[0] === ">=") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] >= e[2];
|
||||
} else if (e[0] === "<") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] < e[2];
|
||||
} else if (e[0] === "<=") {
|
||||
if (isArray(e[1]) || isArray(e[2])) return e;
|
||||
if (e[1] == null || e[2] == null) return null;
|
||||
return e[1] <= e[2];
|
||||
} else if (e[0] === "*") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return a * b;
|
||||
}
|
||||
return !r.test(exp[1]);
|
||||
} else if (exp[0] === "=") {
|
||||
if (exp[1] == null || exp[2] == null) return null;
|
||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
||||
return compare(exp[1], exp[2]) === 0;
|
||||
} else if (exp[0] === "<>") {
|
||||
if (exp[1] == null || exp[2] == null) return null;
|
||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
||||
return compare(exp[1], exp[2]) !== 0;
|
||||
} else if (exp[0] === ">") {
|
||||
if (exp[1] == null || exp[2] == null) return null;
|
||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
||||
return compare(exp[1], exp[2]) > 0;
|
||||
} else if (exp[0] === ">=") {
|
||||
if (exp[1] == null || exp[2] == null) return null;
|
||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
||||
return compare(exp[1], exp[2]) >= 0;
|
||||
} else if (exp[0] === "<") {
|
||||
if (exp[1] == null || exp[2] == null) return null;
|
||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
||||
return compare(exp[1], exp[2]) < 0;
|
||||
} else if (exp[0] === "<=") {
|
||||
if (exp[1] == null || exp[2] == null) return null;
|
||||
if (isArray(exp[1]) || isArray(exp[2])) return exp;
|
||||
return compare(exp[1], exp[2]) <= 0;
|
||||
} else if (exp[0] === "*") {
|
||||
return reduce(exp, (a, b) => {
|
||||
if (a == null || b == null) return null;
|
||||
if (!isArray(a) && !isArray(b)) return toNumber(a) * toNumber(b);
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
} else if (e[0] === "/") {
|
||||
return reduce(e, (a, b, i) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return i === 0 ? a / b : a * b;
|
||||
}
|
||||
} else if (exp[0] === "/") {
|
||||
return reduce(exp, (a, b, i) => {
|
||||
if (a == null || b == null) return null;
|
||||
if (!isArray(a) && !isArray(b))
|
||||
return i === 0 ? toNumber(a) / toNumber(b) : toNumber(a) * toNumber(b);
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
} else if (e[0] === "+") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return a + b;
|
||||
}
|
||||
} else if (exp[0] === "+") {
|
||||
return reduce(exp, (a, b) => {
|
||||
if (a == null || b == null) return null;
|
||||
if (!isArray(a) && !isArray(b)) return toNumber(a) + toNumber(b);
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
} else if (e[0] === "-") {
|
||||
return reduce(e, (a, b, i) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return i === 0 ? a - b : a + b;
|
||||
}
|
||||
} else if (exp[0] === "-") {
|
||||
return reduce(exp, (a, b, i) => {
|
||||
if (a == null || b == null) return null;
|
||||
if (!isArray(a) && !isArray(b))
|
||||
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
} else if (e[0] === "||") {
|
||||
return reduce(e, (a, b) => {
|
||||
if (!isArray(a) && !isArray(b)) {
|
||||
if (a == null || b == null) return null;
|
||||
return `${a}${b}`;
|
||||
}
|
||||
} else if (exp[0] === "||") {
|
||||
return reduce(exp, (a, b) => {
|
||||
if (a == null || b == null) return null;
|
||||
if (!isArray(a) && !isArray(b)) return toString(a) + toString(b);
|
||||
return REDUCE_SKIP;
|
||||
});
|
||||
}
|
||||
return e;
|
||||
return exp;
|
||||
}
|
||||
|
||||
export function evaluate(
|
||||
exp,
|
||||
obj,
|
||||
exp: Expression,
|
||||
obj: Record<string, unknown> | ((e: string) => Expression),
|
||||
now: number,
|
||||
cb?: Function
|
||||
cb?: (e: Expression) => Expression
|
||||
): string | number | boolean | null;
|
||||
export function evaluate(exp, obj?, now?: number, cb?: Function): Expression;
|
||||
export function evaluate(exp, obj?, now?: number, cb?: Function): Expression {
|
||||
return map(exp, e => {
|
||||
export function evaluate(
|
||||
exp: Expression,
|
||||
obj?: Record<string, unknown> | ((e: string) => Expression),
|
||||
now?: number,
|
||||
cb?: (e: Expression) => Expression
|
||||
): Expression;
|
||||
export function evaluate(
|
||||
exp: Expression,
|
||||
obj?: Record<string, unknown> | ((e: string) => Expression),
|
||||
now?: number,
|
||||
cb?: (e: Expression) => Expression
|
||||
): Expression {
|
||||
return map(exp, (e) => {
|
||||
if (cb) e = cb(e);
|
||||
if (!isArray(e)) return e;
|
||||
|
||||
if (e[0] === "FUNC") {
|
||||
if (e[1] === "NOW") {
|
||||
if (now) return now;
|
||||
} else if (e[1] === "UPPER") {
|
||||
if (e[2] == null) return null;
|
||||
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
|
||||
} else if (e[1] === "LOWER") {
|
||||
if (e[2] == null) return null;
|
||||
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
|
||||
}
|
||||
if (e[0] === "FUNC" && e[1] === "NOW") {
|
||||
if (now) return now;
|
||||
} else if (e[0] === "PARAM") {
|
||||
if (e[1] == null) return null;
|
||||
if (obj && !isArray(e[1])) {
|
||||
@@ -228,100 +307,92 @@ export function evaluate(exp, obj?, now?: number, cb?: Function): Expression {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return evalExp(e);
|
||||
return evaluateCallback(e);
|
||||
});
|
||||
}
|
||||
|
||||
export async function evaluateAsync(
|
||||
exp,
|
||||
obj,
|
||||
exp: Expression,
|
||||
obj: Record<string, unknown>,
|
||||
now: number,
|
||||
cb?: Function
|
||||
cb?: (e: Expression) => Promise<Expression>
|
||||
): Promise<string | number | boolean | null>;
|
||||
export async function evaluateAsync(
|
||||
exp,
|
||||
obj?,
|
||||
exp: Expression,
|
||||
obj?: Record<string, unknown>,
|
||||
now?: number,
|
||||
cb?: Function
|
||||
cb?: (e: Expression) => Promise<Expression>
|
||||
): Promise<Expression>;
|
||||
export async function evaluateAsync(
|
||||
exp,
|
||||
obj?,
|
||||
exp: Expression,
|
||||
obj?: Record<string, unknown>,
|
||||
now?: number,
|
||||
cb?: Function
|
||||
cb?: (e: Expression) => Promise<Expression>
|
||||
): Promise<Expression> {
|
||||
return mapAsync(exp, async e => {
|
||||
return mapAsync(exp, async (e) => {
|
||||
if (cb) e = await cb(e);
|
||||
if (!isArray(e)) return e;
|
||||
|
||||
if (e[0] === "FUNC") {
|
||||
if (e[1] === "NOW") {
|
||||
if (now) return now;
|
||||
} else if (e[1] === "UPPER") {
|
||||
if (e[2] == null) return null;
|
||||
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
|
||||
} else if (e[1] === "LOWER") {
|
||||
if (e[2] == null) return null;
|
||||
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
|
||||
}
|
||||
if (e[1] === "NOW") if (now) return now;
|
||||
} else if (e[0] === "PARAM") {
|
||||
if (e[1] == null) return null;
|
||||
if (obj && !isArray(e[1])) {
|
||||
let v = obj[e[1]];
|
||||
if (v == null) return null;
|
||||
if (typeof v === "object") v = v.value ? v.value[0] : null;
|
||||
return v;
|
||||
if (typeof v === "object") v = v["value"] ? v["value"][0] : null;
|
||||
return v as Expression;
|
||||
}
|
||||
}
|
||||
return evalExp(e);
|
||||
return evaluateCallback(e);
|
||||
});
|
||||
}
|
||||
|
||||
export function and(exp1, exp2): Expression {
|
||||
if (!isArray(exp1)) return exp1 ? exp2 : exp1;
|
||||
if (!isArray(exp2)) return exp2 ? exp1 : exp2;
|
||||
export function and(exp1: Expression, exp2: Expression): Expression {
|
||||
if (exp1 != null && !exp1) return false;
|
||||
if (exp2 != null && !exp2) return false;
|
||||
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
|
||||
if (exp1 == null || exp2 == null) return null;
|
||||
return true;
|
||||
}
|
||||
if (!Array.isArray(exp2) && exp1[0] === "AND") return exp1;
|
||||
if (!Array.isArray(exp1) && exp2[0] === "AND") return exp2;
|
||||
|
||||
let res: Expression = ["AND"];
|
||||
const res: Expression = ["AND"];
|
||||
|
||||
if (exp1[0] === "AND") res = res.concat(exp1.slice(1));
|
||||
if (Array.isArray(exp1) && exp1[0] === "AND") res.push(...exp1.slice(1));
|
||||
else res.push(exp1);
|
||||
|
||||
if (exp2[0] === "AND") res = res.concat(exp2.slice(1));
|
||||
if (Array.isArray(exp2) && exp2[0] === "AND") res.push(...exp2.slice(1));
|
||||
else res.push(exp2);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function or(exp1, exp2): Expression {
|
||||
if (!isArray(exp1)) return exp1 ? exp1 : exp2;
|
||||
if (!isArray(exp2)) return exp2 ? exp2 : exp1;
|
||||
export function or(exp1: Expression, exp2: Expression): Expression {
|
||||
if (!Array.isArray(exp1) && exp1) return true;
|
||||
if (!Array.isArray(exp2) && exp2) return true;
|
||||
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
|
||||
if (exp1 == null || exp2 == null) return null;
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(exp2) && exp1[0] === "OR") return exp1;
|
||||
if (!Array.isArray(exp1) && exp2[0] === "OR") return exp2;
|
||||
|
||||
let res: Expression = ["OR"];
|
||||
const res: Expression = ["OR"];
|
||||
|
||||
if (exp1[0] === "OR") res = res.concat(exp1.slice(1));
|
||||
if (Array.isArray(exp1) && exp1[0] === "OR") res.push(...exp1.slice(1));
|
||||
else res.push(exp1);
|
||||
|
||||
if (exp2[0] === "OR") res = res.concat(exp2.slice(1));
|
||||
if (Array.isArray(exp2) && exp2[0] === "OR") res.push(...exp2.slice(1));
|
||||
else res.push(exp2);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function not(exp): Expression {
|
||||
if (isArray(exp) && exp[0] === "NOT") return exp[1];
|
||||
return ["NOT", exp];
|
||||
}
|
||||
|
||||
export function subset(exp1, exp2): boolean {
|
||||
const e = evaluate(["NOT", ["OR", ["NOT", exp1], exp2]]);
|
||||
if (!isArray(e)) return !e;
|
||||
const { vars, clauses } = booleanCnf(e);
|
||||
return !naiveDpll(clauses, vars);
|
||||
}
|
||||
|
||||
export function extractParams(exp): Expression[] {
|
||||
export function extractParams(exp: Expression): Expression[] {
|
||||
const params = new Set<Expression>();
|
||||
map(exp, e => {
|
||||
map(exp, (e) => {
|
||||
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
|
||||
return e;
|
||||
});
|
||||
|
||||
+13
-4
@@ -39,7 +39,7 @@ function getKey(obj): string {
|
||||
return k;
|
||||
}
|
||||
|
||||
export default function memoize<T extends Function>(func: T): T {
|
||||
export default function memoize<T extends (...args: any[]) => any>(func: T): T {
|
||||
const funcKey = getKey(func);
|
||||
return ((...args) => {
|
||||
const key = JSON.stringify(args.map(getKey)) + funcKey;
|
||||
@@ -47,9 +47,18 @@ export default function memoize<T extends Function>(func: T): T {
|
||||
if (cache1.has(key)) return cache1.get(key);
|
||||
|
||||
let r;
|
||||
if (cache2.has(key)) r = cache2.get(key);
|
||||
else r = func(...args);
|
||||
cache1.set(key, r);
|
||||
if (cache2.has(key)) {
|
||||
cache1.set(key, (r = cache2.get(key)));
|
||||
} else {
|
||||
cache1.set(key, (r = func(...args)));
|
||||
// Evict rejected promises
|
||||
if (r instanceof Promise) {
|
||||
r.catch(() => {
|
||||
cache1.delete(key);
|
||||
cache2.delete(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}) as any;
|
||||
}
|
||||
|
||||
+10
-10
@@ -20,9 +20,9 @@
|
||||
import Path from "./path";
|
||||
|
||||
export default class PathSet {
|
||||
private lengthIndex: Set<Path>[];
|
||||
private fragmentIndex: Map<string, Set<Path>>[];
|
||||
private stringIndex: Map<string, Path>;
|
||||
private declare lengthIndex: Set<Path>[];
|
||||
private declare fragmentIndex: Map<string, Set<Path>>[];
|
||||
private declare stringIndex: Map<string, Path>;
|
||||
|
||||
public constructor() {
|
||||
this.lengthIndex = [];
|
||||
@@ -73,8 +73,8 @@ export default class PathSet {
|
||||
|
||||
public find(
|
||||
path: Path,
|
||||
superset: boolean = false,
|
||||
subset: boolean = false,
|
||||
superset = false,
|
||||
subset = false,
|
||||
depth: number = path.length
|
||||
): Path[] {
|
||||
if (path.alias) throw new Error("PathSet does not support aliased paths");
|
||||
@@ -106,24 +106,24 @@ export default class PathSet {
|
||||
if (!fragmentIndexSet1) {
|
||||
if (!fragmentIndexSet2) return [];
|
||||
if (!res) res = [...fragmentIndexSet2];
|
||||
else res = res.filter(r => fragmentIndexSet2.has(r));
|
||||
else res = res.filter((r) => fragmentIndexSet2.has(r));
|
||||
} else if (!fragmentIndexSet2) {
|
||||
if (!res) res = [...fragmentIndexSet1];
|
||||
else res = res.filter(r => fragmentIndexSet1.has(r));
|
||||
else res = res.filter((r) => fragmentIndexSet1.has(r));
|
||||
} else {
|
||||
if (!res) {
|
||||
res = [...fragmentIndexSet1, ...fragmentIndexSet2];
|
||||
} else {
|
||||
res = res.filter(
|
||||
r => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r)
|
||||
(r) => fragmentIndexSet1.has(r) || fragmentIndexSet2.has(r)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!res.length) return res;
|
||||
}
|
||||
|
||||
if (!res) res = [].concat(...lengthIndex.map(a => [...a]));
|
||||
else res = res.filter(r => lengthIndex.some(a => a.has(r)));
|
||||
if (!res) res = [].concat(...lengthIndex.map((a) => [...a]));
|
||||
else res = res.filter((r) => lengthIndex.some((a) => a.has(r)));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
+112
-56
@@ -23,44 +23,102 @@ type Alias = [Path, string][];
|
||||
let cache1 = 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 {
|
||||
public readonly segments: Segments;
|
||||
public readonly wildcard: number;
|
||||
public readonly alias: number;
|
||||
protected _string: string;
|
||||
protected _stringIndex: number[];
|
||||
public declare readonly segments: Segments;
|
||||
public declare readonly wildcard: number;
|
||||
public declare readonly alias: number;
|
||||
protected declare _string: string;
|
||||
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(
|
||||
pattern: string,
|
||||
index: number
|
||||
): { index: number; alias: Alias } {
|
||||
const alias: Alias = [];
|
||||
while (index < pattern.length && pattern[index] !== "]") {
|
||||
const { index: idx, segments } = Path.parsePath(pattern, index);
|
||||
let j = (index = idx + 1);
|
||||
while (pattern[j] !== "]" && pattern[j] !== ",") {
|
||||
if (pattern[j] === '"' && index === j) {
|
||||
++j;
|
||||
while (pattern[j] !== '"' || pattern[j - 1] === "\\") {
|
||||
if (++j >= pattern.length)
|
||||
throw new Error("Invalid alias expression");
|
||||
}
|
||||
}
|
||||
if (++j >= pattern.length) throw new Error("Invalid alias expression");
|
||||
}
|
||||
let i = index;
|
||||
for (;;) {
|
||||
let path: Path;
|
||||
let value: string;
|
||||
({ index: i, path } = Path.parseAliasPath(pattern, i));
|
||||
if (!path) break;
|
||||
({ index: i, value } = Path.parseAliasValue(pattern, i));
|
||||
if (value == null) break;
|
||||
alias.push([path, value]);
|
||||
index = i;
|
||||
|
||||
let value = pattern.slice(index, j).trim();
|
||||
index = j;
|
||||
if (value[0] === '"') {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (error) {
|
||||
throw new Error("Invalid alias expression");
|
||||
}
|
||||
}
|
||||
|
||||
alias.push([new Path(segments), value]);
|
||||
if (pattern[index] === ",") ++index;
|
||||
if (charCodeAt(pattern, index) !== CHAR_COMMA) break;
|
||||
++i;
|
||||
}
|
||||
|
||||
// Ensure identical expressions have idential string representation
|
||||
@@ -81,29 +139,26 @@ export default class Path {
|
||||
index: number
|
||||
): { index: number; segments: Segments } {
|
||||
const segments = [];
|
||||
// Colon separator is needed for parseAlias
|
||||
if (index < pattern.length && pattern[index] !== ":") {
|
||||
for (;;) {
|
||||
if (pattern[index] === "[") {
|
||||
const { index: idx, alias } = Path.parseAlias(pattern, index + 1);
|
||||
index = idx + 1;
|
||||
segments.push(alias);
|
||||
} else {
|
||||
const j = index;
|
||||
while (
|
||||
index < pattern.length &&
|
||||
pattern[index] !== ":" &&
|
||||
pattern[index] !== "."
|
||||
)
|
||||
++index;
|
||||
const s = pattern.slice(j, index).trim();
|
||||
segments.push(s);
|
||||
}
|
||||
|
||||
if (index >= pattern.length || pattern[index] === ":") break;
|
||||
else if (pattern[index] !== ".")
|
||||
throw new Error("Invalid alias expression");
|
||||
++index;
|
||||
let segStart = index;
|
||||
let wildcard = -1;
|
||||
for (let i = index; i <= pattern.length; ++i) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +173,7 @@ export default class Path {
|
||||
if (Array.isArray(s)) {
|
||||
alias |= 1 << i;
|
||||
const parts = s.map(
|
||||
al => `${al[0].toString()}:${JSON.stringify(al[1])}`
|
||||
(al) => `${al[0].toString()}:${JSON.stringify(al[1])}`
|
||||
);
|
||||
return `[${parts.join(",")}]`;
|
||||
} else if (s === "*") {
|
||||
@@ -142,7 +197,8 @@ export default class Path {
|
||||
if (!path) {
|
||||
path = cache2.get(str);
|
||||
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);
|
||||
if (path.toString() !== str) cache1.set(path.toString(), path);
|
||||
}
|
||||
@@ -159,7 +215,7 @@ export default class Path {
|
||||
return this._string;
|
||||
}
|
||||
|
||||
public slice(start: number = 0, end: number = this.segments.length): Path {
|
||||
public slice(start = 0, end: number = this.segments.length): Path {
|
||||
if (start < 0) start = Math.max(0, this.segments.length + start);
|
||||
if (end < 0) end = Math.max(0, this.segments.length + end);
|
||||
|
||||
@@ -211,7 +267,7 @@ export default class Path {
|
||||
|
||||
public stripAlias(): Path {
|
||||
if (!this.alias) return this;
|
||||
const segments = this.segments.map(s => (Array.isArray(s) ? "*" : s));
|
||||
const segments = this.segments.map((s) => (Array.isArray(s) ? "*" : s));
|
||||
const str = segments.join(".");
|
||||
|
||||
let path = cache1.get(str);
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
/**
|
||||
* Naive DPLL solver for propositional logic adapted from
|
||||
* https://github.com/tammet/logictools
|
||||
*
|
||||
* Origianl code by Tanel Tammet released under the MIT license.
|
||||
*
|
||||
* Check whether a clause set is satisfiable or contradictory, using a naive
|
||||
* implementation of the dpll (Davis-Putnam-Logemann-Loveland) procedure
|
||||
* combining truth table search with the resolution-like unit propagation.
|
||||
*
|
||||
* The standard pure-literal rule is not implemented in this naive version,
|
||||
* since it is not really crucial for the algorithm.
|
||||
*
|
||||
* The code is very similar to the pure table solver, except we use a
|
||||
* unit-propagating, derived-varlist-returing satisfiableAt.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
|
||||
function satisfiableAt(clauses, varVals, varnr, val): boolean {
|
||||
if (varnr !== 0) varVals[varnr] = val;
|
||||
|
||||
const propRes = unitPropagate(clauses, varVals);
|
||||
if (propRes === 1) {
|
||||
varVals[varnr] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// if result false (-1) unitPropagate restores the varVals state itself
|
||||
if (propRes === -1) {
|
||||
varVals[varnr] = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// find next unassigned var to split
|
||||
let nextVar = 0;
|
||||
for (let i = 1; i < varVals.length; i++) {
|
||||
if (varVals[i] === 0) {
|
||||
nextVar = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextVar === 0) throw new Error("Error in satisfiableAt");
|
||||
|
||||
if (
|
||||
satisfiableAt(clauses, varVals, nextVar, 1) ||
|
||||
satisfiableAt(clauses, varVals, nextVar, -1)
|
||||
)
|
||||
// not necessary to restore split var: search is halted anyway
|
||||
return true;
|
||||
|
||||
// restore split var
|
||||
varVals[nextVar] = 0;
|
||||
|
||||
// restore derived vars
|
||||
for (let i = 0; i < propRes.length; i++) varVals[propRes[i]] = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether clauses are satisfiable under the varVals assignment, without
|
||||
* search.
|
||||
*
|
||||
* Iterate through all clauses and check whether
|
||||
*
|
||||
* - all literals in the clause are assigned false: undo derived units (set
|
||||
* values in varVals to 0) and return false immediately
|
||||
*
|
||||
* - a clause contains a literal assigned true: skip further actions on this
|
||||
* clause
|
||||
*
|
||||
* - a single literal in the clause is unassigned, all the rest are assigned
|
||||
* false: derive this literal as a unit clause.
|
||||
*
|
||||
* In case at least one literal is derived during the process above, iterate
|
||||
* the whole process until no more new literals are derived.
|
||||
*
|
||||
* Returns:
|
||||
*
|
||||
* 1: all clauses were assigned true, hence the clause set is satisfiable.
|
||||
* The whole search will terminate after that.
|
||||
*
|
||||
* -1: clause set is contradictory. Derived units have been restored.
|
||||
*
|
||||
* a list of derived variables: the status of the clause set is
|
||||
* undetermined. The values of variables on the list will be restored (set to
|
||||
* 0) during backtracking.
|
||||
*/
|
||||
function unitPropagate(clauses, varVals): number[] | 1 | -1 {
|
||||
// all vars with a value derived in the forthcoming main loop
|
||||
const derivedVars = [];
|
||||
|
||||
// the main loop is run, deriving new unit clauses, until either:
|
||||
// - the clause set is detected to be unsatisfiable
|
||||
// - the clause set is detected to be satisfiable
|
||||
// - there are no more derived unit clauses
|
||||
|
||||
for (;;) {
|
||||
let allClausesFound = true; // set to false if we find an undetermined clause
|
||||
const derivedQueue = []; // derived units (literals) during one iteration
|
||||
for (let i = 0; i < clauses.length; i++) {
|
||||
const clause = clauses[i];
|
||||
let clauseVal = 0;
|
||||
let unassignedCount = 0; // count unassigned vars in clause
|
||||
let unassignedLit = 0; // 0 means none found for this clause
|
||||
|
||||
for (let j = 0; j < clause.length; j++) {
|
||||
const lit = clause[j];
|
||||
let nr, polarity;
|
||||
if (lit < 0) {
|
||||
nr = 0 - lit;
|
||||
polarity = -1;
|
||||
} else {
|
||||
nr = lit;
|
||||
polarity = 1;
|
||||
}
|
||||
|
||||
if (varVals[nr] === polarity) {
|
||||
clauseVal = 1;
|
||||
break;
|
||||
} else if (varVals[nr] === 0) {
|
||||
unassignedCount++;
|
||||
unassignedLit = lit;
|
||||
}
|
||||
}
|
||||
|
||||
if (clauseVal === 1) continue; // clause is subsumed
|
||||
|
||||
// clause is not subsumed by varVals
|
||||
if (unassignedCount === 0) {
|
||||
for (let j = 0; j < derivedVars.length; j++)
|
||||
varVals[derivedVars[j]] = 0; // restore derived
|
||||
return -1;
|
||||
} else if (unassignedCount === 1) {
|
||||
// unassignedLit is a derived literal
|
||||
derivedQueue.push(unassignedLit);
|
||||
}
|
||||
|
||||
if (unassignedCount !== 0) allClausesFound = false;
|
||||
}
|
||||
|
||||
// if all clauses were subsumed or all the variables in a clause had a value
|
||||
// no need to restore varVals: whole search finished
|
||||
if (allClausesFound) return 1;
|
||||
|
||||
// in case there are any derived units present, store to varVals and
|
||||
// iterate the main loop
|
||||
if (derivedQueue.length === 0) break;
|
||||
|
||||
// some units were derived: iterate the main loop
|
||||
for (let j = 0; j < derivedQueue.length; j++) {
|
||||
const lit = derivedQueue[j];
|
||||
let nr, polarity;
|
||||
if (lit < 0) {
|
||||
nr = 0 - lit;
|
||||
polarity = -1;
|
||||
} else {
|
||||
nr = lit;
|
||||
polarity = 1;
|
||||
}
|
||||
varVals[nr] = polarity;
|
||||
derivedVars.push(nr);
|
||||
}
|
||||
}
|
||||
|
||||
return derivedVars;
|
||||
}
|
||||
|
||||
export function naiveDpll(clauses, maxvarnr): boolean {
|
||||
// variable values are 0 if not set, 1 if positive, -1 if negative
|
||||
const varVals = new Int32Array(maxvarnr + 1);
|
||||
return satisfiableAt(clauses, varVals, 0, 0);
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
+31
-57
@@ -32,27 +32,26 @@ while (!existsSync(`${ROOT_DIR}/package.json`)) {
|
||||
}
|
||||
|
||||
// For compatibility with v1.1
|
||||
let configDir, cwmpSsl, nbiSsl, fsSsl, uiSsl;
|
||||
let configDir, cwmpSsl, nbiSsl, fsSsl, uiSsl, fsHostname;
|
||||
|
||||
const options = {
|
||||
EXT_DIR: { type: "path", default: resolve(ROOT_DIR, "config/ext") },
|
||||
MONGODB_CONNECTION_URL: {
|
||||
type: "string",
|
||||
default: "mongodb://127.0.0.1/genieacs"
|
||||
default: "mongodb://127.0.0.1/genieacs",
|
||||
},
|
||||
|
||||
CWMP_WORKER_PROCESSES: { type: "int", default: 0 },
|
||||
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_KEY: { type: "string", default: "" },
|
||||
CWMP_LOG_FILE: { type: "path", default: "" },
|
||||
CWMP_ACCESS_LOG_FILE: { type: "path", default: "" },
|
||||
CWMP_KEEP_ALIVE_TIMEOUT: { type: "int", default: 0 },
|
||||
|
||||
NBI_WORKER_PROCESSES: { type: "int", default: 0 },
|
||||
NBI_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_KEY: { type: "string", default: "" },
|
||||
NBI_LOG_FILE: { type: "path", default: "" },
|
||||
@@ -60,16 +59,16 @@ const options = {
|
||||
|
||||
FS_WORKER_PROCESSES: { type: "int", default: 0 },
|
||||
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_KEY: { type: "string", default: "" },
|
||||
FS_HOSTNAME: { type: "string", default: "" },
|
||||
FS_URL_PREFIX: { type: "string", default: "" },
|
||||
FS_LOG_FILE: { type: "path", default: "" },
|
||||
FS_ACCESS_LOG_FILE: { type: "path", default: "" },
|
||||
|
||||
UI_WORKER_PROCESSES: { type: "int", default: 0 },
|
||||
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_KEY: { type: "string", default: "" },
|
||||
UI_LOG_FILE: { type: "path", default: "" },
|
||||
@@ -77,6 +76,7 @@ const options = {
|
||||
UI_JWT_SECRET: { type: "string", default: "" },
|
||||
|
||||
UDP_CONNECTION_REQUEST_PORT: { type: "int", default: 0 },
|
||||
FORWARDED_HEADER: { type: "string", default: "" },
|
||||
|
||||
DOWNLOAD_TIMEOUT: { type: "int", default: 3600 },
|
||||
EXT_TIMEOUT: { type: "int", default: 3000 },
|
||||
@@ -99,16 +99,8 @@ const options = {
|
||||
CONNECTION_REQUEST_ALLOW_BASIC_AUTH: { type: "bool", default: false },
|
||||
MAX_COMMIT_ITERATIONS: { type: "int", default: 32 },
|
||||
|
||||
// XML configuration
|
||||
XML_RECOVER: { type: "bool", default: false },
|
||||
XML_IGNORE_ENC: { type: "bool", default: false },
|
||||
XML_FORMAT: { type: "bool", default: false },
|
||||
XML_NO_DECL: { type: "bool", default: false },
|
||||
XML_NO_EMPTY: { type: "bool", default: false },
|
||||
XML_LIBXMLJS: { type: "bool", default: false },
|
||||
|
||||
// Should probably never be changed
|
||||
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 }
|
||||
DEVICE_ONLINE_THRESHOLD: { type: "int", default: 4000 },
|
||||
};
|
||||
|
||||
const allConfig: { [name: string]: string | number } = {};
|
||||
@@ -120,37 +112,20 @@ function setConfig(name, value, commandLineArgument = false): boolean {
|
||||
if (name === "CONFIG_DIR" || name === "config-dir")
|
||||
configDir = configDir || resolve(ROOT_DIR, value);
|
||||
|
||||
if (name === "CWMP_SSL" || name === "cwmp-ssl") {
|
||||
cwmpSsl =
|
||||
cwmpSsl ||
|
||||
String(value)
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
if (name === "CWMP_SSL" || name === "cwmp-ssl")
|
||||
cwmpSsl = cwmpSsl || String(value).toLowerCase().trim();
|
||||
|
||||
if (name === "NBI_SSL" || name === "nbi-ssl") {
|
||||
nbiSsl =
|
||||
nbiSsl ||
|
||||
String(value)
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
if (name === "NBI_SSL" || name === "nbi-ssl")
|
||||
nbiSsl = nbiSsl || String(value).toLowerCase().trim();
|
||||
|
||||
if (name === "FS_SSL" || name === "fs-ssl") {
|
||||
fsSsl =
|
||||
fsSsl ||
|
||||
String(value)
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
if (name === "FS_SSL" || name === "fs-ssl")
|
||||
fsSsl = fsSsl || String(value).toLowerCase().trim();
|
||||
|
||||
if (name === "UI_SSL" || name === "ui-ssl") {
|
||||
uiSsl =
|
||||
uiSsl ||
|
||||
String(value)
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
}
|
||||
if (name === "UI_SSL" || name === "ui-ssl")
|
||||
uiSsl = uiSsl || String(value).toLowerCase().trim();
|
||||
|
||||
if (name === "FS_HOSTNAME" || name === "fs-hostname")
|
||||
fsHostname = fsHostname || String(value).trim();
|
||||
|
||||
// For compatibility with v1.0
|
||||
if (name === "PRESETS_CACHE_DURATION" || name === "presets-cache-duration")
|
||||
@@ -168,12 +143,6 @@ function setConfig(name, value, commandLineArgument = false): boolean {
|
||||
)
|
||||
setConfig("GPV_BATCH_SIZE", value);
|
||||
|
||||
if (name === "XML_PARSE_IGNORE_ENC" || name === "xml-parse-ignore-enc")
|
||||
setConfig("XML_IGNORE_ENC", value);
|
||||
|
||||
if (name === "XML_PARSE_RECOVER" || name === "xml-parse-recover")
|
||||
setConfig("XML_RECOVER", value);
|
||||
|
||||
if (name === "FS_IP" || name === "fs-ip") setConfig("FS_HOSTNAME", value);
|
||||
|
||||
function cast(val, type): string | number | boolean {
|
||||
@@ -181,11 +150,7 @@ function setConfig(name, value, commandLineArgument = false): boolean {
|
||||
case "int":
|
||||
return Number(val);
|
||||
case "bool":
|
||||
return ["true", "1"].includes(
|
||||
String(val)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
return ["true", "1"].includes(String(val).trim().toLowerCase());
|
||||
case "string":
|
||||
return String(val);
|
||||
case "path":
|
||||
@@ -275,6 +240,15 @@ if (["true", "1"].includes(uiSsl)) {
|
||||
setConfig("UI_SSL_KEY", `${d}/cwmp.key`);
|
||||
}
|
||||
|
||||
if (fsHostname) {
|
||||
const FS_PORT = allConfig["FS_PORT"] || 7567;
|
||||
const FS_SSL = !!allConfig["FS_SSL_CERT"];
|
||||
setConfig(
|
||||
"FS_URL_PREFIX",
|
||||
(FS_SSL ? "https" : "http") + `://${fsHostname}:${FS_PORT}/`
|
||||
);
|
||||
}
|
||||
|
||||
// Defaults
|
||||
for (const [k, v] of Object.entries(options))
|
||||
if (v["default"] != null) setConfig(k, v["default"]);
|
||||
@@ -310,7 +284,7 @@ export function get(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getDefault(optionName): string | number | boolean {
|
||||
export function getDefault(optionName: string): string | number | boolean {
|
||||
const option = options[optionName];
|
||||
if (!option) return null;
|
||||
|
||||
|
||||
+73
-78
@@ -19,7 +19,7 @@
|
||||
|
||||
import * as crypto from "crypto";
|
||||
import * as dgram from "dgram";
|
||||
import { parse } from "url";
|
||||
import { URL } from "url";
|
||||
import * as http from "http";
|
||||
import { evaluateAsync } from "./common/expression";
|
||||
import { Expression } from "./types";
|
||||
@@ -29,15 +29,13 @@ import * as debug from "./debug";
|
||||
|
||||
async function extractAuth(
|
||||
exp: Expression,
|
||||
context: {},
|
||||
now: number,
|
||||
dflt: any
|
||||
): Promise<[string, string, Expression]> {
|
||||
let username, password;
|
||||
const _exp = await evaluateAsync(
|
||||
exp,
|
||||
context,
|
||||
now,
|
||||
{},
|
||||
0,
|
||||
async (e: Expression): Promise<Expression> => {
|
||||
if (!username && Array.isArray(e) && e[0] === "FUNC") {
|
||||
if (e[1] === "EXT") {
|
||||
@@ -50,8 +48,8 @@ async function extractAuth(
|
||||
return fault ? null : value;
|
||||
} else if (e[1] === "AUTH") {
|
||||
if (!Array.isArray(e[2]) && !Array.isArray(e[3])) {
|
||||
username = e[2];
|
||||
password = e[3];
|
||||
username = e[2] || "";
|
||||
password = e[3] || "";
|
||||
}
|
||||
return dflt;
|
||||
}
|
||||
@@ -63,31 +61,29 @@ async function extractAuth(
|
||||
}
|
||||
|
||||
function httpGet(
|
||||
url: URL,
|
||||
options: http.RequestOptions,
|
||||
timeout,
|
||||
_debug: boolean,
|
||||
deviceId: string
|
||||
): Promise<{ statusCode: number; headers: {} }> {
|
||||
): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http
|
||||
.get(options, res => {
|
||||
.get(url, options, (res) => {
|
||||
res.resume();
|
||||
resolve({ statusCode: res.statusCode, headers: res.headers });
|
||||
if (_debug) {
|
||||
debug.outgoingHttpRequest(req, deviceId, options, null);
|
||||
debug.outgoingHttpRequest(req, deviceId, "GET", url, null);
|
||||
debug.incomingHttpResponse(res, deviceId, null);
|
||||
}
|
||||
})
|
||||
.on("error", err => {
|
||||
req.abort();
|
||||
reject(new Error("Device is offline"));
|
||||
if (_debug) debug.outgoingHttpRequestError(req, deviceId, options, err);
|
||||
.on("error", (err) => {
|
||||
req.destroy();
|
||||
reject(err);
|
||||
if (_debug)
|
||||
debug.outgoingHttpRequestError(req, deviceId, "GET", url, err);
|
||||
})
|
||||
.on("socket", socket => {
|
||||
socket.setTimeout(timeout);
|
||||
socket.on("timeout", () => {
|
||||
req.abort();
|
||||
});
|
||||
.on("timeout", () => {
|
||||
req.destroy();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -95,23 +91,20 @@ function httpGet(
|
||||
export async function httpConnectionRequest(
|
||||
address: string,
|
||||
authExp: Expression,
|
||||
context: {},
|
||||
allowBasicAuth: boolean,
|
||||
timeout: number,
|
||||
_debug: boolean,
|
||||
deviceId: string
|
||||
): Promise<void> {
|
||||
const now = Date.now();
|
||||
const options: http.RequestOptions = parse(address);
|
||||
if (options.protocol !== "http:")
|
||||
throw new Error("Invalid connection request URL or protocol");
|
||||
): Promise<string> {
|
||||
const url = new URL(address);
|
||||
if (url.protocol !== "http:")
|
||||
return "Invalid connection request URL or protocol";
|
||||
|
||||
options.agent = new http.Agent({
|
||||
maxSockets: 1,
|
||||
keepAlive: true
|
||||
});
|
||||
const options: http.RequestOptions = {
|
||||
agent: new http.Agent({ maxSockets: 1, keepAlive: true, timeout: timeout }),
|
||||
};
|
||||
|
||||
let authHeader: {};
|
||||
let authHeader: Record<string, string>;
|
||||
let username: string;
|
||||
let password: string;
|
||||
|
||||
@@ -119,14 +112,13 @@ export async function httpConnectionRequest(
|
||||
let opts = options;
|
||||
if (authHeader) {
|
||||
if (authHeader["method"] === "Basic") {
|
||||
if (!allowBasicAuth)
|
||||
throw new Error("Basic HTTP authentication not allowed");
|
||||
if (!allowBasicAuth) return "Basic HTTP authentication not allowed";
|
||||
|
||||
opts = Object.assign(
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.basic(username || "", password || "")
|
||||
}
|
||||
Authorization: auth.basic(username || "", password || ""),
|
||||
},
|
||||
},
|
||||
options
|
||||
);
|
||||
@@ -137,57 +129,70 @@ export async function httpConnectionRequest(
|
||||
Authorization: auth.solveDigest(
|
||||
username,
|
||||
password,
|
||||
options.path,
|
||||
url.pathname + url.search,
|
||||
"GET",
|
||||
null,
|
||||
authHeader
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
options
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unrecognized auth method");
|
||||
return "Unrecognized auth method";
|
||||
}
|
||||
}
|
||||
|
||||
let res = await httpGet(opts, timeout, _debug, deviceId);
|
||||
let res: { statusCode: number; headers: http.IncomingHttpHeaders };
|
||||
try {
|
||||
res = await httpGet(url, opts, _debug, deviceId);
|
||||
} catch (err) {
|
||||
// Workaround for some devices unexpectedly closing the connection
|
||||
if (authHeader) {
|
||||
try {
|
||||
res = await httpGet(url, opts, _debug, deviceId);
|
||||
} catch (err) {
|
||||
return `Connection request error: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for some devices unexpectedly closing the connection
|
||||
if (res.statusCode === 0 && authHeader)
|
||||
res = await httpGet(opts, timeout, _debug, deviceId);
|
||||
if (res.statusCode === 0) throw new Error("Device is offline");
|
||||
if (res.statusCode === 200 || res.statusCode === 204) return;
|
||||
if (err["code"] === "ECONNRESET" || err["code"] === "ECONNREFUSED")
|
||||
return "Device is offline";
|
||||
|
||||
return `Connection request error: ${err.message}`;
|
||||
}
|
||||
|
||||
if (res.statusCode === 200 || res.statusCode === 204) return "";
|
||||
|
||||
// When a Connection Request is received for the Virtual CWMP Device and the Proxied
|
||||
// Device is offline the CPE Proxier MUST respond with an HTTP 503 failure
|
||||
if (res.statusCode === 503) return "Device is offline";
|
||||
|
||||
if (res.statusCode === 401 && res.headers["www-authenticate"]) {
|
||||
authHeader = auth.parseWwwAuthenticateHeader(
|
||||
res.headers["www-authenticate"]
|
||||
);
|
||||
[username, password, authExp] = await extractAuth(
|
||||
authExp,
|
||||
context,
|
||||
now,
|
||||
false
|
||||
);
|
||||
try {
|
||||
authHeader = auth.parseWwwAuthenticateHeader(
|
||||
res.headers["www-authenticate"]
|
||||
);
|
||||
} catch (err) {
|
||||
return "Connection request error: Error parsing www-authenticate header";
|
||||
}
|
||||
[username, password, authExp] = await extractAuth(authExp, false);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unexpected response code from device: ${res.statusCode}`
|
||||
);
|
||||
return `Connection request error: Unexpected status code ${res.statusCode}`;
|
||||
}
|
||||
}
|
||||
throw new Error("Incorrect connection request credentials");
|
||||
|
||||
return "Connection request error: Incorrect connection request credentials";
|
||||
}
|
||||
|
||||
export async function udpConnectionRequest(
|
||||
address: string,
|
||||
host: string,
|
||||
port: number,
|
||||
authExp: Expression,
|
||||
context: {},
|
||||
sourcePort: number = 0,
|
||||
sourcePort = 0,
|
||||
_debug: boolean,
|
||||
deviceId: string
|
||||
): Promise<void> {
|
||||
const [host, portStr] = address.split(":", 2);
|
||||
const port = portStr ? parseInt(portStr) : 80;
|
||||
const now = Date.now();
|
||||
|
||||
const client = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
||||
@@ -202,12 +207,7 @@ export async function udpConnectionRequest(
|
||||
let username: string;
|
||||
let password: string;
|
||||
|
||||
[username, password, authExp] = await extractAuth(
|
||||
authExp,
|
||||
context,
|
||||
now,
|
||||
null
|
||||
);
|
||||
[username, password, authExp] = await extractAuth(authExp, null);
|
||||
|
||||
if (username == null) username = "";
|
||||
if (password == null) password = "";
|
||||
@@ -219,12 +219,12 @@ export async function udpConnectionRequest(
|
||||
.createHmac("sha1", password)
|
||||
.update(`${ts}${id}${username}${cn}`)
|
||||
.digest("hex");
|
||||
const uri = `http://${address}?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 uri = `http://${host}:${port}?ts=${ts}&id=${id}&un=${username}&cn=${cn}&sig=${sig}`;
|
||||
const msg = `GET ${uri} HTTP/1.1\r\nHost: ${host}:${port}\r\n\r\n`;
|
||||
const message = Buffer.from(msg);
|
||||
|
||||
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) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
@@ -233,12 +233,7 @@ export async function udpConnectionRequest(
|
||||
});
|
||||
}
|
||||
|
||||
[username, password, authExp] = await extractAuth(
|
||||
authExp,
|
||||
context,
|
||||
now,
|
||||
null
|
||||
);
|
||||
[username, password, authExp] = await extractAuth(authExp, null);
|
||||
}
|
||||
client.close();
|
||||
}
|
||||
+535
-305
File diff suppressed because it is too large.
Load diff
@@ -17,9 +17,9 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { MongoClient, ObjectID, Collection } from "mongodb";
|
||||
import { MongoClient, ObjectId, Collection, Db } from "mongodb";
|
||||
import { get } from "./config";
|
||||
import { escapeRegExp } from "./common";
|
||||
import { decodeTag, encodeTag, escapeRegExp } from "./common";
|
||||
import { parse } from "./common/expression-parser";
|
||||
import {
|
||||
DeviceData,
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
SessionFault,
|
||||
Task,
|
||||
Operation,
|
||||
Expression
|
||||
Expression,
|
||||
} from "./types";
|
||||
import Path from "./common/path";
|
||||
|
||||
@@ -44,15 +44,31 @@ export let tasksCollection: Collection,
|
||||
usersCollection: Collection,
|
||||
configCollection: Collection;
|
||||
|
||||
export let client: MongoClient;
|
||||
let clientPromise: Promise<MongoClient>;
|
||||
|
||||
const INVALID_PATH_SUFFIX = "__invalid";
|
||||
|
||||
function compareAccessLists(list1: string[], list2: string[]): boolean {
|
||||
if (list1.length !== list2.length) return false;
|
||||
for (const [i, v] of list1.entries()) if (v !== list2[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const onConnectCallbacks: ((db: Db) => Promise<void>)[] = [];
|
||||
|
||||
export function onConnect(callback: (db: Db) => Promise<void>): void {
|
||||
onConnectCallbacks.push(callback);
|
||||
}
|
||||
|
||||
export async function connect(): Promise<void> {
|
||||
client = await MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), {
|
||||
useNewUrlParser: true
|
||||
});
|
||||
clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"));
|
||||
|
||||
const client = await clientPromise;
|
||||
const db = client.db();
|
||||
await Promise.all(onConnectCallbacks.map((c) => c(db)));
|
||||
}
|
||||
|
||||
onConnect(async (db) => {
|
||||
tasksCollection = db.collection("tasks");
|
||||
await tasksCollection.createIndex({ device: 1, timestamp: 1 });
|
||||
|
||||
@@ -67,15 +83,15 @@ export async function connect(): Promise<void> {
|
||||
permissionsCollection = db.collection("permissions");
|
||||
usersCollection = db.collection("users");
|
||||
configCollection = db.collection("config");
|
||||
}
|
||||
});
|
||||
|
||||
export async function disconnect(): Promise<void> {
|
||||
if (client) await client.close();
|
||||
if (clientPromise) await (await clientPromise).close();
|
||||
}
|
||||
|
||||
// Optimize projection by removing overlaps
|
||||
// This can modify the object
|
||||
function optimizeProjection(obj: {}): {} {
|
||||
function optimizeProjection(obj: { [path: string]: 1 }): { [path: string]: 1 } {
|
||||
if (obj[""]) return { "": obj[""] };
|
||||
|
||||
const keys = Object.keys(obj).sort();
|
||||
@@ -102,20 +118,27 @@ export async function fetchDevice(
|
||||
[
|
||||
Path.parse("Events"),
|
||||
timestamp,
|
||||
{ object: [timestamp, 1], writable: [timestamp, 0] }
|
||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
||||
],
|
||||
[
|
||||
Path.parse("DeviceID"),
|
||||
timestamp,
|
||||
{ object: [timestamp, 1], writable: [timestamp, 0] }
|
||||
]
|
||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
||||
],
|
||||
];
|
||||
|
||||
const device = await devicesCollection.findOne({ _id: id });
|
||||
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["_attributesTimestamp"])
|
||||
obj["_attributesTimestamp"] = +obj["_attributesTimestamp"];
|
||||
|
||||
const attrs: Attributes = {};
|
||||
let t = obj["_timestamp"] || 1;
|
||||
@@ -133,19 +156,44 @@ export async function fetchDevice(
|
||||
|
||||
if (obj["_object"] != null) attrs.object = [t, obj["_object"] ? 1 : 0];
|
||||
|
||||
res.push([Path.parse(path.slice(0, -1)), t, attrs]);
|
||||
if (obj["_notification"] != null) {
|
||||
attrs.notification = [
|
||||
obj["_attributesTimestamp"] || 1,
|
||||
obj["_notification"],
|
||||
];
|
||||
}
|
||||
|
||||
if (obj["_accessList"] != null)
|
||||
attrs.accessList = [obj["_attributesTimestamp"] || 1, obj["_accessList"]];
|
||||
|
||||
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)) {
|
||||
if (!k.startsWith("_")) {
|
||||
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"])
|
||||
res.push([Path.parse(path + "*"), obj["_timestamp"]]);
|
||||
if (obj["_object"] && 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)) {
|
||||
switch (k) {
|
||||
case "_lastInform":
|
||||
@@ -155,8 +203,8 @@ export async function fetchDevice(
|
||||
{
|
||||
object: [+v, 0],
|
||||
writable: [+v, 0],
|
||||
value: [+v, [+v, "xsd:dateTime"]]
|
||||
}
|
||||
value: [+v, [+v, "xsd:dateTime"]],
|
||||
},
|
||||
]);
|
||||
break;
|
||||
case "_lastBoot":
|
||||
@@ -166,8 +214,8 @@ export async function fetchDevice(
|
||||
{
|
||||
object: [+v, 0],
|
||||
writable: [+v, 0],
|
||||
value: [+v, [+v, "xsd:dateTime"]]
|
||||
}
|
||||
value: [+v, [+v, "xsd:dateTime"]],
|
||||
},
|
||||
]);
|
||||
break;
|
||||
case "_lastBootstrap":
|
||||
@@ -177,8 +225,8 @@ export async function fetchDevice(
|
||||
{
|
||||
object: [+v, 0],
|
||||
writable: [+v, 0],
|
||||
value: [+v, [+v, "xsd:dateTime"]]
|
||||
}
|
||||
value: [+v, [+v, "xsd:dateTime"]],
|
||||
},
|
||||
]);
|
||||
break;
|
||||
case "_registered":
|
||||
@@ -189,8 +237,8 @@ export async function fetchDevice(
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
writable: [timestamp, 0],
|
||||
value: [timestamp, [+v, "xsd:dateTime"]]
|
||||
}
|
||||
value: [timestamp, [+v, "xsd:dateTime"]],
|
||||
},
|
||||
]);
|
||||
break;
|
||||
case "_id":
|
||||
@@ -200,8 +248,8 @@ export async function fetchDevice(
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
writable: [timestamp, 0],
|
||||
value: [timestamp, [v as string, "xsd:string"]]
|
||||
}
|
||||
value: [timestamp, [v as string, "xsd:string"]],
|
||||
},
|
||||
]);
|
||||
break;
|
||||
case "_tags":
|
||||
@@ -209,20 +257,19 @@ export async function fetchDevice(
|
||||
res.push([
|
||||
Path.parse("Tags"),
|
||||
timestamp,
|
||||
{ object: [timestamp, 1], writable: [timestamp, 0] }
|
||||
{ object: [timestamp, 1], writable: [timestamp, 0] },
|
||||
]);
|
||||
}
|
||||
|
||||
for (let t of v as string[]) {
|
||||
t = t.replace(/[^a-zA-Z0-9-]+/g, "_");
|
||||
for (const t of v as string[]) {
|
||||
res.push([
|
||||
Path.parse("Tags." + t),
|
||||
Path.parse("Tags." + encodeTag(t)),
|
||||
timestamp,
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
writable: [timestamp, 1],
|
||||
value: [timestamp, [true, "xsd:boolean"]]
|
||||
}
|
||||
value: [timestamp, [true, "xsd:boolean"]],
|
||||
},
|
||||
]);
|
||||
}
|
||||
break;
|
||||
@@ -234,8 +281,8 @@ export async function fetchDevice(
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
writable: [timestamp, 0],
|
||||
value: [timestamp, [v["_Manufacturer"], "xsd:string"]]
|
||||
}
|
||||
value: [timestamp, [v["_Manufacturer"], "xsd:string"]],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -246,8 +293,8 @@ export async function fetchDevice(
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
writable: [timestamp, 0],
|
||||
value: [timestamp, [v["_OUI"], "xsd:string"]]
|
||||
}
|
||||
value: [timestamp, [v["_OUI"], "xsd:string"]],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -258,8 +305,8 @@ export async function fetchDevice(
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
writable: [timestamp, 0],
|
||||
value: [timestamp, [v["_ProductClass"], "xsd:string"]]
|
||||
}
|
||||
value: [timestamp, [v["_ProductClass"], "xsd:string"]],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -270,14 +317,15 @@ export async function fetchDevice(
|
||||
{
|
||||
object: [timestamp, 0],
|
||||
writable: [timestamp, 0],
|
||||
value: [timestamp, [v["_SerialNumber"], "xsd:string"]]
|
||||
}
|
||||
value: [timestamp, [v["_SerialNumber"], "xsd:string"]],
|
||||
},
|
||||
]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!k.startsWith("_")) storeParams(v, k, 1, ts);
|
||||
}
|
||||
}
|
||||
|
||||
storeParams(device, "", 0, 0);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -329,6 +377,12 @@ export async function saveDevice(
|
||||
const object2 = ((diff[2] || {}).object || [])[1];
|
||||
const writable2 = ((diff[2] || {}).writable || [])[1];
|
||||
const writable1 = ((diff[1] || {}).writable || [])[1];
|
||||
const attributesTimestamp1 = ((diff[1] || {}).notification || [])[0];
|
||||
const attributesTimestamp2 = ((diff[2] || {}).notification || [])[0];
|
||||
const notification1 = ((diff[1] || {}).notification || [])[1];
|
||||
const notification2 = ((diff[2] || {}).notification || [])[1];
|
||||
const accessList1 = ((diff[1] || {}).accessList || [])[1];
|
||||
const accessList2 = ((diff[2] || {}).accessList || [])[1];
|
||||
|
||||
switch (path.segments[0]) {
|
||||
case "Events":
|
||||
@@ -396,7 +450,7 @@ export async function saveDevice(
|
||||
} else {
|
||||
if (!update["$pull"]["_tags"]) {
|
||||
update["$pull"]["_tags"] = {
|
||||
$in: []
|
||||
$in: [],
|
||||
};
|
||||
}
|
||||
update["$pull"]["_tags"]["$in"].push(path.segments[1]);
|
||||
@@ -406,7 +460,16 @@ export async function saveDevice(
|
||||
break;
|
||||
default:
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -442,7 +505,7 @@ export async function saveDevice(
|
||||
|
||||
break;
|
||||
case "object":
|
||||
if (!diff[1] || !diff[1].object || object2 !== object1) {
|
||||
if (!diff[1]?.object || object2 !== object1) {
|
||||
update["$set"][
|
||||
path.length ? path.toString() + "._object" : "_object"
|
||||
] = !!object2;
|
||||
@@ -450,11 +513,49 @@ export async function saveDevice(
|
||||
|
||||
break;
|
||||
case "writable":
|
||||
if (!diff[1] || !diff[1].writable || writable2 !== writable1) {
|
||||
if (!diff[1]?.writable || writable2 !== writable1) {
|
||||
update["$set"][
|
||||
path.length ? path.toString() + "._writable" : "_writable"
|
||||
] = !!writable2;
|
||||
}
|
||||
|
||||
break;
|
||||
case "notification":
|
||||
if (
|
||||
!diff[1] ||
|
||||
!diff[1].notification ||
|
||||
notification2 !== notification1
|
||||
) {
|
||||
update["$set"][
|
||||
path.length
|
||||
? path.toString() + "._notification"
|
||||
: "_notification"
|
||||
] = notification2;
|
||||
}
|
||||
|
||||
if (attributesTimestamp2 !== attributesTimestamp1) {
|
||||
update["$set"][path.toString() + "._attributesTimestamp"] =
|
||||
new Date(attributesTimestamp2);
|
||||
}
|
||||
|
||||
break;
|
||||
case "accessList":
|
||||
if (
|
||||
!diff[1] ||
|
||||
!diff[1].accessList ||
|
||||
!compareAccessLists(accessList2, accessList1)
|
||||
) {
|
||||
update["$set"][
|
||||
path.length
|
||||
? path.toString() + "._accessList"
|
||||
: "_accessList"
|
||||
] = accessList2;
|
||||
}
|
||||
|
||||
if (attributesTimestamp2 !== attributesTimestamp1) {
|
||||
update["$set"][path.toString() + "._attributesTimestamp"] =
|
||||
new Date(attributesTimestamp2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,13 +564,19 @@ export async function saveDevice(
|
||||
for (const attrName of Object.keys(diff[1])) {
|
||||
if (
|
||||
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() + "." : "";
|
||||
update["$unset"][`${p}_${attrName}`] = 1;
|
||||
if (attrName === "value") {
|
||||
update["$unset"][p + "_type"] = 1;
|
||||
update["$unset"][p + "_timestamp"] = 1;
|
||||
} else if (attrName === "notification") {
|
||||
if (accessList2 == null)
|
||||
update["$unset"][`${p}_attributesTimestamp`] = 1;
|
||||
} else if (attrName === "accessList") {
|
||||
if (notification2 == null)
|
||||
update["$unset"][`${p}_attributesTimestamp`] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -508,10 +615,10 @@ export async function saveDevice(
|
||||
}
|
||||
|
||||
const result = await devicesCollection.updateOne({ _id: deviceId }, update, {
|
||||
upsert: isNew
|
||||
upsert: isNew,
|
||||
});
|
||||
|
||||
if (result.result.n !== 1)
|
||||
if (!result.matchedCount && !result.upsertedCount)
|
||||
throw new Error(`Device ${deviceId} not found in database`);
|
||||
|
||||
if (update2) {
|
||||
@@ -521,7 +628,7 @@ export async function saveDevice(
|
||||
}
|
||||
|
||||
export async function getFaults(
|
||||
deviceId
|
||||
deviceId: string
|
||||
): Promise<{ [channel: string]: SessionFault }> {
|
||||
const res = await faultsCollection
|
||||
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
|
||||
@@ -535,32 +642,39 @@ export async function getFaults(
|
||||
delete r.device;
|
||||
r.timestamp = +r.timestamp;
|
||||
r.provisions = JSON.parse(r.provisions);
|
||||
faults[channel] = r;
|
||||
faults[channel] = r as SessionFault;
|
||||
}
|
||||
|
||||
return faults;
|
||||
}
|
||||
|
||||
export async function saveFault(deviceId, channel, fault): Promise<void> {
|
||||
export async function saveFault(
|
||||
deviceId: string,
|
||||
channel: string,
|
||||
fault: SessionFault
|
||||
): Promise<void> {
|
||||
const id = `${deviceId}:${channel}`;
|
||||
fault = Object.assign({}, fault);
|
||||
fault._id = id;
|
||||
fault.device = deviceId;
|
||||
fault.channel = channel;
|
||||
fault.timestamp = new Date(fault.timestamp);
|
||||
fault.provisions = JSON.stringify(fault.provisions);
|
||||
await faultsCollection.replaceOne({ _id: id }, fault, { upsert: true });
|
||||
const f = Object.assign({}, fault) as Record<string, any>;
|
||||
f["_id"] = id;
|
||||
f["device"] = deviceId;
|
||||
f["channel"] = channel;
|
||||
f["timestamp"] = new Date(fault.timestamp);
|
||||
f["provisions"] = JSON.stringify(fault.provisions);
|
||||
await faultsCollection.replaceOne({ _id: id }, f, { upsert: true });
|
||||
}
|
||||
|
||||
export async function deleteFault(deviceId, channel): Promise<void> {
|
||||
export async function deleteFault(
|
||||
deviceId: string,
|
||||
channel: string
|
||||
): Promise<void> {
|
||||
await faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` });
|
||||
}
|
||||
|
||||
export async function getDueTasks(
|
||||
deviceId,
|
||||
timestamp
|
||||
deviceId: string,
|
||||
timestamp: number
|
||||
): Promise<[Task[], number]> {
|
||||
const cur = tasksCollection.find({ device: deviceId }).sort(["timestamp"]);
|
||||
const cur = tasksCollection.find({ device: deviceId }).sort({ timestamp: 1 });
|
||||
const tasks = [] as Task[];
|
||||
|
||||
for await (const task of cur) {
|
||||
@@ -569,13 +683,13 @@ export async function getDueTasks(
|
||||
if (task.timestamp >= timestamp) return [tasks, +task.timestamp];
|
||||
task._id = String(task._id);
|
||||
|
||||
tasks.push(task);
|
||||
tasks.push(task as Task);
|
||||
|
||||
// For API compatibility
|
||||
if (task.name === "download" && task.file) {
|
||||
let q;
|
||||
if (ObjectID.isValid(task.file))
|
||||
q = { _id: { $in: [task.file, new ObjectID(task.file)] } };
|
||||
if (ObjectId.isValid(task.file))
|
||||
q = { _id: { $in: [task.file, new ObjectId(task.file)] } };
|
||||
else q = { _id: task.file };
|
||||
|
||||
const res = await filesCollection.find(q).toArray();
|
||||
@@ -591,14 +705,17 @@ export async function getDueTasks(
|
||||
return [tasks, null];
|
||||
}
|
||||
|
||||
export async function clearTasks(deviceId, taskIds): Promise<void> {
|
||||
export async function clearTasks(
|
||||
deviceId: string,
|
||||
taskIds: string[]
|
||||
): Promise<void> {
|
||||
await tasksCollection.deleteMany({
|
||||
_id: { $in: taskIds.map(id => new ObjectID(id)) }
|
||||
_id: { $in: taskIds.map((id) => new ObjectId(id)) },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getOperations(
|
||||
deviceId
|
||||
deviceId: string
|
||||
): Promise<{ [commandKey: string]: Operation }> {
|
||||
const res = await operationsCollection
|
||||
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
|
||||
@@ -608,53 +725,61 @@ export async function getOperations(
|
||||
for (const r of res) {
|
||||
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
|
||||
if (typeof r.provisions !== "string") {
|
||||
operations[commandKey] = r as Operation;
|
||||
continue;
|
||||
}
|
||||
r.timestamp = +r.timestamp;
|
||||
if (r.args) r.args = JSON.parse(r.args);
|
||||
r.provisions = JSON.parse(r.provisions);
|
||||
r.retries = JSON.parse(r.retries);
|
||||
operations[commandKey] = r;
|
||||
operations[commandKey] = r as Operation;
|
||||
}
|
||||
return operations;
|
||||
}
|
||||
|
||||
export async function saveOperation(
|
||||
deviceId,
|
||||
commandKey,
|
||||
operation
|
||||
deviceId: string,
|
||||
commandKey: string,
|
||||
operation: Operation
|
||||
): Promise<void> {
|
||||
const id = `${deviceId}:${commandKey}`;
|
||||
operation = Object.assign({}, operation);
|
||||
operation._id = id;
|
||||
operation.timestamp = new Date(operation.timestamp);
|
||||
operation.provisions = JSON.stringify(operation.provisions);
|
||||
operation.retries = JSON.stringify(operation.retries);
|
||||
operation.args = JSON.stringify(operation.args);
|
||||
await operationsCollection.replaceOne({ _id: id }, operation, {
|
||||
upsert: true
|
||||
const o = Object.assign({}, operation) as Record<string, any>;
|
||||
o["_id"] = id;
|
||||
o["timestamp"] = new Date(operation.timestamp);
|
||||
o["provisions"] = JSON.stringify(operation.provisions);
|
||||
o["retries"] = JSON.stringify(operation.retries);
|
||||
o["args"] = JSON.stringify(operation.args);
|
||||
await operationsCollection.replaceOne({ _id: id }, o, {
|
||||
upsert: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteOperation(deviceId, commandKey): Promise<void> {
|
||||
export async function deleteOperation(
|
||||
deviceId: string,
|
||||
commandKey: string
|
||||
): Promise<void> {
|
||||
await operationsCollection.deleteOne({ _id: `${deviceId}:${commandKey}` });
|
||||
}
|
||||
|
||||
export async function getPresets(): Promise<{}[]> {
|
||||
export async function getPresets(): Promise<Record<string, any>[]> {
|
||||
return presetsCollection.find().toArray();
|
||||
}
|
||||
|
||||
export async function getObjects(): Promise<{}[]> {
|
||||
export async function getObjects(): Promise<Record<string, any>[]> {
|
||||
return objectsCollection.find().toArray();
|
||||
}
|
||||
|
||||
export async function getProvisions(): Promise<{}[]> {
|
||||
export async function getProvisions(): Promise<Record<string, any>[]> {
|
||||
return provisionsCollection.find().toArray();
|
||||
}
|
||||
|
||||
export async function getVirtualParameters(): Promise<{}[]> {
|
||||
export async function getVirtualParameters(): Promise<Record<string, any>[]> {
|
||||
return virtualParametersCollection.find().toArray();
|
||||
}
|
||||
|
||||
export function getFiles(): Promise<{}[]> {
|
||||
export function getFiles(): Promise<Record<string, any>[]> {
|
||||
return filesCollection.find().toArray();
|
||||
}
|
||||
|
||||
@@ -662,9 +787,9 @@ export async function getConfig(): Promise<
|
||||
{ id: string; value: Expression }[]
|
||||
> {
|
||||
const res = await configCollection.find().toArray();
|
||||
return res.map(c => ({
|
||||
return res.map((c) => ({
|
||||
id: c["_id"],
|
||||
value: parse(c["value"])
|
||||
value: parse(c["value"]),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -677,7 +802,7 @@ interface Permission {
|
||||
}
|
||||
|
||||
export async function getPermissions(): Promise<Permission[]> {
|
||||
return permissionsCollection.find().toArray();
|
||||
return permissionsCollection.find().toArray() as unknown as Permission[];
|
||||
}
|
||||
|
||||
interface User {
|
||||
@@ -688,5 +813,5 @@ interface User {
|
||||
}
|
||||
|
||||
export async function getUsers(): Promise<User[]> {
|
||||
return usersCollection.find().toArray();
|
||||
return usersCollection.find().toArray() as unknown as User[];
|
||||
}
|
||||
+30
-33
@@ -17,15 +17,10 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
IncomingMessage,
|
||||
ServerResponse,
|
||||
ClientRequest,
|
||||
RequestOptions
|
||||
} from "http";
|
||||
import { IncomingMessage, ServerResponse, ClientRequest } from "http";
|
||||
import { Socket } from "net";
|
||||
import { appendFileSync } from "fs";
|
||||
import * as yaml from "yaml";
|
||||
import { stringify } from "./common/yaml";
|
||||
import * as config from "./config";
|
||||
|
||||
const DEBUG_FILE = "" + config.get("DEBUG_FILE");
|
||||
@@ -49,7 +44,7 @@ export function incomingHttpRequest(
|
||||
): void {
|
||||
if (!DEBUG_FILE) return;
|
||||
const now = new Date();
|
||||
const con = httpRequest.connection;
|
||||
const con = httpRequest.socket;
|
||||
const msg = {
|
||||
event: "incoming HTTP request",
|
||||
timestamp: now,
|
||||
@@ -60,11 +55,11 @@ export function incomingHttpRequest(
|
||||
method: httpRequest.method,
|
||||
url: httpRequest.url,
|
||||
headers: httpRequest.headers,
|
||||
body: body
|
||||
body: body,
|
||||
};
|
||||
|
||||
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`);
|
||||
@@ -77,7 +72,7 @@ export function outgoingHttpResponse(
|
||||
): void {
|
||||
if (!DEBUG_FILE) return;
|
||||
const now = new Date();
|
||||
const con = httpResponse.connection;
|
||||
const con = httpResponse.socket;
|
||||
const msg = {
|
||||
event: "outgoing HTTP response",
|
||||
timestamp: now,
|
||||
@@ -86,11 +81,11 @@ export function outgoingHttpResponse(
|
||||
connection: getConnectionTimestamp(con),
|
||||
statusCode: httpResponse.statusCode,
|
||||
headers: httpResponse.getHeaders(),
|
||||
body: body
|
||||
body: body,
|
||||
};
|
||||
|
||||
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`);
|
||||
@@ -99,27 +94,28 @@ export function outgoingHttpResponse(
|
||||
export function outgoingHttpRequest(
|
||||
httpRequest: ClientRequest,
|
||||
deviceId: string,
|
||||
options: RequestOptions,
|
||||
method: "GET" | "PUT" | "POST" | "DELETE",
|
||||
url: URL,
|
||||
body: string
|
||||
): void {
|
||||
if (!DEBUG_FILE) return;
|
||||
const now = new Date();
|
||||
const con = httpRequest.connection;
|
||||
const con = httpRequest.socket;
|
||||
const msg = {
|
||||
event: "outgoing HTTP request",
|
||||
timestamp: now,
|
||||
remoteAddress: con.remoteAddress,
|
||||
deviceId: deviceId,
|
||||
connection: getConnectionTimestamp(con),
|
||||
remotePort: options.port,
|
||||
method: options.method || "GET",
|
||||
url: options.path,
|
||||
remotePort: url.port,
|
||||
method: method,
|
||||
url: url.pathname + url.search,
|
||||
headers: httpRequest.getHeaders(),
|
||||
body: body
|
||||
body: body,
|
||||
};
|
||||
|
||||
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`);
|
||||
@@ -128,7 +124,8 @@ export function outgoingHttpRequest(
|
||||
export function outgoingHttpRequestError(
|
||||
httpRequest: ClientRequest,
|
||||
deviceId: string,
|
||||
options: RequestOptions,
|
||||
method: "GET" | "PUT" | "POST" | "DELETE",
|
||||
url: URL,
|
||||
err: Error
|
||||
): void {
|
||||
if (!DEBUG_FILE) return;
|
||||
@@ -136,18 +133,18 @@ export function outgoingHttpRequestError(
|
||||
const msg = {
|
||||
event: "outgoing HTTP request",
|
||||
timestamp: now,
|
||||
remoteAddress: options.hostname,
|
||||
remoteAddress: url.hostname,
|
||||
deviceId: deviceId,
|
||||
connection: null,
|
||||
remotePort: options.port,
|
||||
method: options.method,
|
||||
url: options.path,
|
||||
remotePort: url.port,
|
||||
method: method,
|
||||
url: url.pathname + url.search,
|
||||
headers: httpRequest.getHeaders(),
|
||||
error: err.message
|
||||
error: err.message,
|
||||
};
|
||||
|
||||
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`);
|
||||
@@ -160,20 +157,20 @@ export function incomingHttpResponse(
|
||||
): void {
|
||||
if (!DEBUG_FILE) return;
|
||||
const now = new Date();
|
||||
const con = httpResponse.connection;
|
||||
const con = httpResponse.socket;
|
||||
const msg = {
|
||||
event: "incoming HTTP response",
|
||||
timestamp: now,
|
||||
remoteAddress: con.remoteAddress,
|
||||
deviceId: deviceId,
|
||||
connection: getConnectionTimestamp(httpResponse.connection),
|
||||
connection: getConnectionTimestamp(httpResponse.socket),
|
||||
statusCode: httpResponse.statusCode,
|
||||
headers: httpResponse.headers,
|
||||
body: body
|
||||
body: body,
|
||||
};
|
||||
|
||||
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`);
|
||||
@@ -193,11 +190,11 @@ export function outgoingUdpMessage(
|
||||
remoteAddress: remoteAddress,
|
||||
deviceId: deviceId,
|
||||
remotePort: remotePort,
|
||||
body: body
|
||||
body: body,
|
||||
};
|
||||
|
||||
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`);
|
||||
|
||||
+136
-32
@@ -21,90 +21,191 @@ import Path from "./common/path";
|
||||
import * as config from "./config";
|
||||
import * as device from "./device";
|
||||
import * as scheduling from "./scheduling";
|
||||
import { SessionContext, Declaration } from "./types";
|
||||
|
||||
const MAX_DEPTH = +config.get("MAX_DEPTH");
|
||||
|
||||
export function refresh(sessionContext, provision, declarations): boolean {
|
||||
const segments = Path.parse(provision[1]).segments.slice();
|
||||
const l = segments.length;
|
||||
segments.length = MAX_DEPTH;
|
||||
segments.fill("*", l);
|
||||
const path = Path.parse(segments.join("."));
|
||||
const every = 1000 * (provision[2] || 1);
|
||||
export function refresh(
|
||||
sessionContext: SessionContext,
|
||||
provision: (string | number | boolean)[],
|
||||
declarations: Declaration[]
|
||||
): boolean {
|
||||
if (
|
||||
(provision.length !== 2 || typeof provision[1] !== "string") &&
|
||||
(provision.length !== 3 ||
|
||||
typeof provision[1] !== "string" ||
|
||||
typeof provision[2] !== "number") &&
|
||||
(provision.length < 4 ||
|
||||
typeof provision[1] !== "string" ||
|
||||
typeof provision[2] !== "number" ||
|
||||
typeof provision[3] !== "boolean")
|
||||
)
|
||||
throw new Error("Invalid arguments");
|
||||
|
||||
const every = 1000 * ((provision[2] as number) || 1);
|
||||
const offset = scheduling.variance(sessionContext.deviceId, every);
|
||||
const t = scheduling.interval(sessionContext.timestamp, every, offset);
|
||||
|
||||
for (let i = l; i < path.length; ++i) {
|
||||
let attrGet;
|
||||
let refreshChildren;
|
||||
if (provision[3] == null) {
|
||||
refreshChildren = true;
|
||||
attrGet = { object: 1, writable: 1, value: t };
|
||||
} else {
|
||||
attrGet = {};
|
||||
refreshChildren = !!provision[3];
|
||||
for (const a of provision.slice(4)) attrGet[a as string] = t;
|
||||
}
|
||||
|
||||
let path = Path.parse(provision[1]);
|
||||
let l = path.length;
|
||||
if (refreshChildren) {
|
||||
const segments = path.segments.slice();
|
||||
l = segments.length;
|
||||
segments.length = MAX_DEPTH;
|
||||
segments.fill("*", l);
|
||||
path = Path.parse(segments.join("."));
|
||||
}
|
||||
|
||||
for (let i = l; i <= path.length; ++i) {
|
||||
declarations.push({
|
||||
path: path.slice(0, i),
|
||||
pathGet: t,
|
||||
pathSet: null,
|
||||
attrGet: { object: 1, writable: 1, value: t },
|
||||
attrGet: attrGet,
|
||||
attrSet: null,
|
||||
defer: true
|
||||
defer: true,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function value(sessionContext, provision, declarations): boolean {
|
||||
export function value(
|
||||
sessionContext: SessionContext,
|
||||
provision: (string | number | boolean)[],
|
||||
declarations: Declaration[]
|
||||
): boolean {
|
||||
if (
|
||||
provision.length < 3 ||
|
||||
provision.length > 4 ||
|
||||
typeof provision[1] !== "string"
|
||||
)
|
||||
throw new Error("Invalid arguments");
|
||||
|
||||
let attr: string, val: any;
|
||||
|
||||
if (provision.length === 3) {
|
||||
attr = "value";
|
||||
val = provision[2];
|
||||
} else {
|
||||
attr = (provision[2] as string) || "";
|
||||
val = provision[3];
|
||||
}
|
||||
|
||||
if (attr === "accessList") {
|
||||
val = (val || "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => !!s);
|
||||
} else if (attr === "value") {
|
||||
val = [val];
|
||||
}
|
||||
|
||||
declarations.push({
|
||||
path: Path.parse(provision[1]),
|
||||
pathGet: 1,
|
||||
pathSet: null,
|
||||
attrGet: { value: 1 },
|
||||
attrSet: { value: [provision[2]] },
|
||||
defer: true
|
||||
attrGet: { [attr]: 1 },
|
||||
attrSet: { [attr]: val },
|
||||
defer: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function tag(sessionContext, provision, declarations): boolean {
|
||||
export function tag(
|
||||
sessionContext: SessionContext,
|
||||
provision: (string | number | boolean)[],
|
||||
declarations: Declaration[]
|
||||
): boolean {
|
||||
if (
|
||||
provision.length !== 3 ||
|
||||
typeof provision[1] !== "string" ||
|
||||
typeof provision[2] !== "boolean"
|
||||
)
|
||||
throw new Error("Invalid arguments");
|
||||
|
||||
declarations.push({
|
||||
path: Path.parse(`Tags.${provision[1]}`),
|
||||
pathGet: 1,
|
||||
pathSet: null,
|
||||
attrGet: { value: 1 },
|
||||
attrSet: { value: [provision[2]] },
|
||||
defer: true
|
||||
defer: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function reboot(sessionContext, provision, declarations): boolean {
|
||||
export function reboot(
|
||||
sessionContext: SessionContext,
|
||||
provision: (string | number | boolean)[],
|
||||
declarations: Declaration[]
|
||||
): boolean {
|
||||
if (provision.length !== 1) throw new Error("Invalid arguments");
|
||||
|
||||
declarations.push({
|
||||
path: Path.parse("Reboot"),
|
||||
pathGet: 1,
|
||||
pathSet: null,
|
||||
attrGet: { value: 1 },
|
||||
attrSet: { value: [sessionContext.timestamp] },
|
||||
defer: true
|
||||
defer: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function reset(sessionContext, provision, declarations): boolean {
|
||||
export function reset(
|
||||
sessionContext: SessionContext,
|
||||
provision: (string | number | boolean)[],
|
||||
declarations: Declaration[]
|
||||
): boolean {
|
||||
if (provision.length !== 1) throw new Error("Invalid arguments");
|
||||
|
||||
declarations.push({
|
||||
path: Path.parse("FactoryReset"),
|
||||
pathGet: 1,
|
||||
pathSet: null,
|
||||
attrGet: { value: 1 },
|
||||
attrSet: { value: [sessionContext.timestamp] },
|
||||
defer: true
|
||||
defer: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function download(sessionContext, provision, declarations): boolean {
|
||||
export function download(
|
||||
sessionContext: SessionContext,
|
||||
provision: (string | number | boolean)[],
|
||||
declarations: Declaration[]
|
||||
): boolean {
|
||||
if (
|
||||
(provision.length !== 3 ||
|
||||
typeof provision[1] !== "string" ||
|
||||
typeof provision[2] !== "string") &&
|
||||
(provision.length !== 4 ||
|
||||
typeof provision[1] !== "string" ||
|
||||
typeof provision[2] !== "string" ||
|
||||
typeof provision[3] !== "string")
|
||||
)
|
||||
throw new Error("Invalid arguments");
|
||||
|
||||
const alias = [
|
||||
`FileType:${JSON.stringify(provision[1] || "")}`,
|
||||
`FileName:${JSON.stringify(provision[2] || "")}`,
|
||||
`TargetFileName:${JSON.stringify(provision[3] || "")}`
|
||||
`TargetFileName:${JSON.stringify(provision[3] || "")}`,
|
||||
].join(",");
|
||||
|
||||
declarations.push({
|
||||
@@ -113,7 +214,7 @@ export function download(sessionContext, provision, declarations): boolean {
|
||||
pathSet: 1,
|
||||
attrGet: null,
|
||||
attrSet: null,
|
||||
defer: true
|
||||
defer: true,
|
||||
});
|
||||
|
||||
declarations.push({
|
||||
@@ -122,22 +223,25 @@ export function download(sessionContext, provision, declarations): boolean {
|
||||
pathSet: null,
|
||||
attrGet: { value: 1 },
|
||||
attrSet: { value: [sessionContext.timestamp] },
|
||||
defer: true
|
||||
defer: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function instances(
|
||||
sessionContext,
|
||||
provision,
|
||||
declarations,
|
||||
startRevision,
|
||||
endRevision
|
||||
sessionContext: SessionContext,
|
||||
provision: (string | number | boolean)[],
|
||||
declarations: Declaration[],
|
||||
startRevision: number,
|
||||
endRevision: number
|
||||
): boolean {
|
||||
if (provision.length !== 3 || typeof provision[1] !== "string")
|
||||
throw new Error("Invalid arguments");
|
||||
|
||||
let count = Number(provision[2]);
|
||||
|
||||
if (Number.isNaN(count)) return true;
|
||||
if (Number.isNaN(count)) throw new Error("Invalid arguments");
|
||||
|
||||
const path = Path.parse(provision[1]);
|
||||
|
||||
@@ -148,7 +252,7 @@ export function instances(
|
||||
pathSet: null,
|
||||
attrGet: null,
|
||||
attrSet: null,
|
||||
defer: true
|
||||
defer: true,
|
||||
});
|
||||
|
||||
if (endRevision === startRevision) return false;
|
||||
@@ -167,7 +271,7 @@ export function instances(
|
||||
pathSet: count,
|
||||
attrGet: null,
|
||||
attrSet: null,
|
||||
defer: true
|
||||
defer: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
+28
-24
@@ -18,12 +18,20 @@
|
||||
*/
|
||||
|
||||
import Path from "./common/path";
|
||||
import { DeviceData, Declaration, Clear } from "./types";
|
||||
import {
|
||||
DeviceData,
|
||||
Declaration,
|
||||
Clear,
|
||||
AttributeTimestamps,
|
||||
Attributes,
|
||||
} from "./types";
|
||||
|
||||
const CHANGE_FLAGS = {
|
||||
object: 2,
|
||||
writable: 4,
|
||||
value: 8
|
||||
value: 8,
|
||||
notification: 16,
|
||||
accessList: 32,
|
||||
};
|
||||
|
||||
function parseBool(v): boolean {
|
||||
@@ -36,7 +44,7 @@ function parseBool(v): boolean {
|
||||
|
||||
export function sanitizeParameterValue(
|
||||
parameterValue: [string | number | boolean, string]
|
||||
): [string | number | boolean, string?] {
|
||||
): [string | number | boolean, string] {
|
||||
if (parameterValue[0] != null) {
|
||||
switch (parameterValue[1]) {
|
||||
case "xsd:boolean":
|
||||
@@ -46,13 +54,6 @@ export function sanitizeParameterValue(
|
||||
else parameterValue[0] = b;
|
||||
}
|
||||
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:unsignedInt":
|
||||
if (typeof parameterValue[0] !== "number") {
|
||||
@@ -75,7 +76,7 @@ export function sanitizeParameterValue(
|
||||
}
|
||||
break;
|
||||
default:
|
||||
parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0]));
|
||||
parameterValue[0] = "" + parameterValue[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -96,8 +97,8 @@ export function getAliasDeclarations(
|
||||
pathSet: null,
|
||||
attrGet: attrGet,
|
||||
attrSet: null,
|
||||
defer: true
|
||||
}
|
||||
defer: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (path.alias) {
|
||||
@@ -107,7 +108,7 @@ export function getAliasDeclarations(
|
||||
for (const [p] of alias as [Path, string][]) {
|
||||
decs = decs.concat(
|
||||
getAliasDeclarations(parent.concat(p), timestamp, {
|
||||
value: timestamp
|
||||
value: timestamp,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -189,10 +190,10 @@ export function unpack(
|
||||
}
|
||||
|
||||
export function clear(
|
||||
deviceData,
|
||||
path,
|
||||
timestamp,
|
||||
attributes,
|
||||
deviceData: DeviceData,
|
||||
path: Path,
|
||||
timestamp: number,
|
||||
attributes: AttributeTimestamps,
|
||||
changeFlags = 0
|
||||
): void {
|
||||
const changeTrackers = {};
|
||||
@@ -200,7 +201,7 @@ export function clear(
|
||||
timestamp = timestamp || 0;
|
||||
|
||||
let descendantsTimestamp = timestamp;
|
||||
if (attributes && attributes.object) {
|
||||
if (attributes?.object) {
|
||||
if (attributes.object > descendantsTimestamp)
|
||||
descendantsTimestamp = attributes.object;
|
||||
if (!(attributes.object <= attributes.value))
|
||||
@@ -271,8 +272,8 @@ function compareEquality(a, b): boolean {
|
||||
export function set(
|
||||
deviceData: DeviceData,
|
||||
path: Path,
|
||||
timestamp,
|
||||
attributes,
|
||||
timestamp: number,
|
||||
attributes: Attributes,
|
||||
toClear?: Clear[]
|
||||
): Clear[] {
|
||||
path = deviceData.paths.add(path);
|
||||
@@ -300,7 +301,7 @@ export function set(
|
||||
attributes.object[1] &&
|
||||
attributes.object[0] >= (attributes.value ? attributes.value[0] : 0)
|
||||
)
|
||||
attributes.value = [attributes.object[0]];
|
||||
attributes.value = [attributes.object[0], null];
|
||||
|
||||
const newAttributes = Object.assign({}, currentAttributes, attributes);
|
||||
|
||||
@@ -376,7 +377,7 @@ export function track(
|
||||
deviceData: DeviceData,
|
||||
path: Path,
|
||||
marker: string,
|
||||
attributes?
|
||||
attributes?: string[]
|
||||
): void {
|
||||
path = deviceData.paths.add(path);
|
||||
let f = 1;
|
||||
@@ -393,7 +394,10 @@ export function track(
|
||||
cur[marker] |= f;
|
||||
}
|
||||
|
||||
export function clearTrackers(deviceData: DeviceData, tracker): void {
|
||||
export function clearTrackers(
|
||||
deviceData: DeviceData,
|
||||
tracker: string | string[]
|
||||
): void {
|
||||
if (Array.isArray(tracker)) {
|
||||
for (const v of deviceData.trackers.values())
|
||||
for (const t of tracker) delete v[t];
|
||||
|
||||
+32
-17
@@ -21,42 +21,38 @@ import { spawn, ChildProcess } from "child_process";
|
||||
import * as crypto from "crypto";
|
||||
import * as config from "./config";
|
||||
import { Fault } from "./types";
|
||||
import { ROOT_DIR } from "./config";
|
||||
import * as logger from "./logger";
|
||||
import readline from "readline";
|
||||
|
||||
const TIMEOUT = +config.get("EXT_TIMEOUT");
|
||||
|
||||
const processes: { [script: string]: ChildProcess } = {};
|
||||
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): Promise<{ fault: Fault; value: any }> {
|
||||
return new Promise(resolve => {
|
||||
export function run(args: string[]): Promise<{ fault: Fault; value: any }> {
|
||||
return new Promise((resolve) => {
|
||||
const scriptName = args[0];
|
||||
|
||||
const id = crypto.randomBytes(8).toString("hex");
|
||||
jobs.set(id, resolve);
|
||||
|
||||
if (!processes[scriptName]) {
|
||||
const p = spawn("bin/genieacs-ext", [scriptName], {
|
||||
stdio: ["inherit", "inherit", "inherit", "ipc"]
|
||||
const p = spawn(ROOT_DIR + "/bin/genieacs-ext", [scriptName], {
|
||||
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
||||
});
|
||||
processes[scriptName] = p;
|
||||
|
||||
p.on("error", err => {
|
||||
p.on("error", (err) => {
|
||||
if (processes[scriptName] === p) {
|
||||
if (jobs.delete(id)) {
|
||||
resolve({
|
||||
fault: { code: err.name, message: err.message },
|
||||
value: null
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
kill(processes[scriptName]);
|
||||
delete processes[scriptName];
|
||||
}
|
||||
@@ -66,14 +62,33 @@ export function run(args): Promise<{ fault: Fault; value: any }> {
|
||||
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(() => {
|
||||
if (jobs.delete(id)) {
|
||||
resolve({
|
||||
fault: { code: "timeout", message: "Extension timed out" },
|
||||
value: null
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
}, TIMEOUT);
|
||||
@@ -85,7 +100,7 @@ export function run(args): Promise<{ fault: Fault; value: any }> {
|
||||
}
|
||||
|
||||
function kill(process: ChildProcess): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
const timeToKill = Date.now() + 5000;
|
||||
|
||||
process.kill();
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Copyright 2013-2019 GenieACS Inc.
|
||||
*
|
||||
* This file is part of GenieACS.
|
||||
*
|
||||
* GenieACS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* GenieACS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { IncomingMessage } from "http";
|
||||
import { TLSSocket } from "tls";
|
||||
import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js";
|
||||
import * as config from "./config";
|
||||
|
||||
interface RequestOrigin {
|
||||
localAddress: string;
|
||||
localPort: number;
|
||||
remoteAddress: string;
|
||||
remotePort: number;
|
||||
host: string;
|
||||
encrypted: boolean;
|
||||
}
|
||||
|
||||
const FORWARDED_HEADER = "" + config.get("FORWARDED_HEADER");
|
||||
const cache = new WeakMap<IncomingMessage, RequestOrigin>();
|
||||
const cidrs: [IPv4 | IPv6, number][] = [];
|
||||
|
||||
for (const str of FORWARDED_HEADER.split(",").map((s) => s.trim())) {
|
||||
try {
|
||||
cidrs.push(parseCIDR(str));
|
||||
} catch (err) {
|
||||
// Not a valid CIDR format, try parsing as IP
|
||||
try {
|
||||
const ip = parse(str);
|
||||
cidrs.push([ip, ip.toByteArray().length * 8]);
|
||||
} catch (err) {
|
||||
// Not a valid IP either, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseForwardedHeader(str: string): { [name: string]: string } {
|
||||
str = str.toLowerCase();
|
||||
const res: { [name: string]: string } = {};
|
||||
let keyIdx = 0;
|
||||
let valueIdx = -1;
|
||||
let key: string;
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
const char = str.charCodeAt(i);
|
||||
if (char === 61 /* = */) {
|
||||
if (keyIdx >= 0) {
|
||||
key = str.slice(keyIdx, i).trim();
|
||||
keyIdx = -1;
|
||||
valueIdx = i + 1;
|
||||
}
|
||||
} else if (char === 59 /* ; */) {
|
||||
if (valueIdx >= 0) res[key] = str.slice(valueIdx, i).trim();
|
||||
valueIdx = -1;
|
||||
keyIdx = i + 1;
|
||||
} else if (char === 44 /* , */) {
|
||||
if (valueIdx >= 0) res[key] = str.slice(valueIdx, i).trim();
|
||||
return res;
|
||||
} else if (char === 34 /* " */) {
|
||||
if (valueIdx >= 0) {
|
||||
const quoteIdx = i;
|
||||
if (!str.slice(valueIdx, quoteIdx).trim()) {
|
||||
for (i = i + 1; i < str.length; ++i) {
|
||||
const c = str.charCodeAt(i);
|
||||
if (c === 92 /* \ */) ++i;
|
||||
if (c === 34 /* " */) {
|
||||
res[key] = JSON.parse(str.slice(quoteIdx, i + 1).trim());
|
||||
valueIdx = -1;
|
||||
keyIdx = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (valueIdx >= 0) res[key] = str.slice(valueIdx).trim();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getRequestOrigin(request: IncomingMessage): RequestOrigin {
|
||||
let origin = cache.get(request);
|
||||
if (!origin) {
|
||||
const soc = request.socket;
|
||||
origin = {
|
||||
localAddress: soc.localAddress,
|
||||
localPort: soc.localPort,
|
||||
remoteAddress: soc.remoteAddress,
|
||||
remotePort: soc.remotePort,
|
||||
host: request.headers["host"],
|
||||
encrypted: !!(request.socket as TLSSocket).encrypted,
|
||||
};
|
||||
|
||||
const header = request.headers["forwarded"];
|
||||
if (header) {
|
||||
const ip = parse(soc.remoteAddress) as IPv4;
|
||||
if (cidrs.some((cidr) => ip.match(cidr as [IPv4, number]))) {
|
||||
const parsed = parseForwardedHeader(header);
|
||||
|
||||
if (parsed["proto"] === "https") {
|
||||
origin.encrypted = true;
|
||||
origin.localPort = 443;
|
||||
} else if (parsed["proto"] === "http") {
|
||||
origin.encrypted = false;
|
||||
origin.localPort = 80;
|
||||
}
|
||||
|
||||
if (parsed["host"]) {
|
||||
origin.host = parsed["host"];
|
||||
const [, port] = parsed["host"].split(":", 2);
|
||||
origin.localPort = +port || origin.localPort;
|
||||
}
|
||||
|
||||
if (parsed["for"]) {
|
||||
if (parsed["for"].startsWith("[")) {
|
||||
const i = parsed["for"].lastIndexOf("]");
|
||||
if (i >= 0) {
|
||||
origin.remoteAddress = parsed["for"].slice(1, i);
|
||||
origin.remotePort =
|
||||
parseInt(parsed["for"].slice(i + 2)) || origin.remotePort;
|
||||
}
|
||||
} else {
|
||||
const i = parsed["for"].lastIndexOf(":");
|
||||
if (i >= 0) {
|
||||
origin.remoteAddress = parsed["for"].slice(0, i);
|
||||
origin.remotePort =
|
||||
parseInt(parsed["for"].slice(i + 1)) || origin.remotePort;
|
||||
} else {
|
||||
origin.remoteAddress = parsed["for"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed["by"]) {
|
||||
if (parsed["by"].startsWith("[")) {
|
||||
const i = parsed["by"].lastIndexOf("]");
|
||||
if (i >= 0) {
|
||||
origin.localAddress = parsed["by"].slice(1, i);
|
||||
origin.localPort =
|
||||
parseInt(parsed["by"].slice(i + 2)) || origin.localPort;
|
||||
}
|
||||
} else {
|
||||
const i = parsed["by"].lastIndexOf(":");
|
||||
if (i >= 0) {
|
||||
origin.localAddress = parsed["by"].slice(0, i);
|
||||
origin.localPort =
|
||||
parseInt(parsed["by"].slice(i + 1)) || origin.localPort;
|
||||
} else {
|
||||
origin.localAddress = parsed["by"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache.set(request, origin);
|
||||
}
|
||||
return origin;
|
||||
}
|
||||
@@ -18,46 +18,80 @@
|
||||
*/
|
||||
|
||||
import * as url from "url";
|
||||
import * as querystring from "querystring";
|
||||
import { GridFSBucket } from "mongodb";
|
||||
import * as db from "./db";
|
||||
import { IncomingMessage, ServerResponse } from "http";
|
||||
import { Collection, GridFSBucket } from "mongodb";
|
||||
import { onConnect } from "./db";
|
||||
import * as logger from "./logger";
|
||||
import { getRequestOrigin } from "./forwarded";
|
||||
import memoize from "./common/memoize";
|
||||
|
||||
export function listener(request, response): void {
|
||||
const urlParts = url.parse(request.url, true);
|
||||
if (request.method === "GET") {
|
||||
const filename = querystring.unescape(urlParts.pathname.substring(1));
|
||||
let filesCollection: Collection;
|
||||
let filesBucket: GridFSBucket;
|
||||
|
||||
const log = {
|
||||
message: "Fetch file",
|
||||
filename: filename,
|
||||
remoteAddress: request.connection.remoteAddress
|
||||
};
|
||||
onConnect(async (db) => {
|
||||
filesCollection = db.collection("fs.files");
|
||||
filesBucket = new GridFSBucket(db);
|
||||
});
|
||||
|
||||
db.filesCollection.findOne({ _id: filename }, (err, file) => {
|
||||
if (err) throw err;
|
||||
|
||||
if (!file) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
log.message += " not found";
|
||||
logger.accessError(log);
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(200, {
|
||||
"Content-Type": file.contentType || "application/octet-stream",
|
||||
"Content-Length": file.length
|
||||
const 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);
|
||||
});
|
||||
|
||||
const bucket = new GridFSBucket(db.client.db());
|
||||
const downloadStream = bucket.openDownloadStreamByName(filename);
|
||||
downloadStream.pipe(response);
|
||||
|
||||
logger.accessInfo(log);
|
||||
});
|
||||
} else {
|
||||
}
|
||||
);
|
||||
|
||||
export async function listener(
|
||||
request: IncomingMessage,
|
||||
response: ServerResponse
|
||||
): Promise<void> {
|
||||
if (request.method !== "GET") {
|
||||
response.writeHead(405, { Allow: "GET" });
|
||||
response.end("405 Method Not Allowed");
|
||||
return;
|
||||
}
|
||||
|
||||
const urlParts = url.parse(request.url, true);
|
||||
const filename = decodeURIComponent(urlParts.pathname.substring(1));
|
||||
|
||||
const log = {
|
||||
message: "Fetch file",
|
||||
filename: filename,
|
||||
remoteAddress: getRequestOrigin(request).remoteAddress,
|
||||
};
|
||||
|
||||
const file = await filesCollection.findOne({ _id: filename });
|
||||
|
||||
if (!file) {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
log.message += " not found";
|
||||
logger.accessError(log);
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = await getFile(
|
||||
file["uploadDate"].getTime(),
|
||||
file.length,
|
||||
filename
|
||||
);
|
||||
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": file.length,
|
||||
});
|
||||
|
||||
response.end(buffer);
|
||||
logger.accessInfo(log);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import Path from "./common/path";
|
||||
|
||||
/**
|
||||
* Copyright 2013-2019 GenieACS Inc.
|
||||
*
|
||||
@@ -27,9 +29,12 @@ const UNDISCOVERED_DEPTH = 7;
|
||||
// flags is an int where its bits mark the segments in the pattern that
|
||||
// need refreshing. Leading 0s indicate that the pattern up to that
|
||||
// point has been discovered.
|
||||
export function estimateGpnCount(gpnPatterns, depth = 0): number {
|
||||
const children = {};
|
||||
const wildcardChildren = [];
|
||||
export function estimateGpnCount(
|
||||
gpnPatterns: [Path, number][],
|
||||
depth = 0
|
||||
): number {
|
||||
const children: { [segment: string]: [Path, number][] } = {};
|
||||
const wildcardChildren: [Path, number][] = [];
|
||||
let wildcardDiscovered = false;
|
||||
let gpnCount = 0;
|
||||
|
||||
@@ -37,7 +42,7 @@ export function estimateGpnCount(gpnPatterns, depth = 0): number {
|
||||
const path = pattern[0];
|
||||
const flags = pattern[1] >> depth;
|
||||
|
||||
const k = path[depth];
|
||||
const k = path.segments[depth] as string;
|
||||
|
||||
if (!k) {
|
||||
if (flags & 1) gpnCount = 1;
|
||||
|
||||
+54
-53
@@ -59,8 +59,8 @@ const INFORM_SCRIPT = `
|
||||
// Device ID as user name
|
||||
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.
|
||||
const password = "" + Math.random();
|
||||
// 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 informInterval = 300;
|
||||
|
||||
@@ -95,11 +95,11 @@ export async function getStatus(): Promise<Status> {
|
||||
filters: !Object.keys(ui["filters"]).length,
|
||||
device: !Object.keys(ui["device"]).length,
|
||||
index: !Object.keys(ui["index"]).length,
|
||||
overview: !Object.keys(ui["overview"]).length
|
||||
overview: !Object.keys(ui["overview"]).length,
|
||||
};
|
||||
}
|
||||
|
||||
export async function seed(options): Promise<void> {
|
||||
export async function seed(options: Record<string, boolean>): Promise<void> {
|
||||
const resources = {};
|
||||
const proms = [];
|
||||
|
||||
@@ -117,12 +117,12 @@ export async function seed(options): Promise<void> {
|
||||
role: "admin",
|
||||
resource: "virtualParameters",
|
||||
access: 3,
|
||||
validate: "true"
|
||||
}
|
||||
validate: "true",
|
||||
},
|
||||
];
|
||||
|
||||
resources["users"] = [
|
||||
{ username: "admin", password: "admin", roles: ["admin"] }
|
||||
{ username: "admin", password: "admin", roles: ["admin"] },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -135,8 +135,7 @@ export async function seed(options): Promise<void> {
|
||||
{ _id: "ui.filters.1.parameter", value: "DeviceID.ProductClass" },
|
||||
{ _id: "ui.filters.1.type", value: "'string'" },
|
||||
{ _id: "ui.filters.2.label", value: "'Tag'" },
|
||||
{ _id: "ui.filters.2.parameter", value: "tag" },
|
||||
{ _id: "ui.filters.2.type", value: "'string'" }
|
||||
{ _id: "ui.filters.2.type", value: "'tag'" },
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -150,118 +149,118 @@ export async function seed(options): Promise<void> {
|
||||
{ _id: "ui.device.2.parameters.0.label", value: "'Last inform'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.0.type",
|
||||
value: "'parameter'"
|
||||
value: "'parameter'",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.0.components.1.chart", value: "'online'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.1.type",
|
||||
value: "'overview-dot'"
|
||||
value: "'overview-dot'",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.type",
|
||||
value: "'summon-button'"
|
||||
value: "'summon-button'",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.parameters.0",
|
||||
value: "InternetGatewayDevice.DeviceInfo.HardwareVersion"
|
||||
value: "InternetGatewayDevice.DeviceInfo.HardwareVersion",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.parameters.1",
|
||||
value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion"
|
||||
value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.parameters.2",
|
||||
value:
|
||||
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress"
|
||||
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.MACAddress",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.parameters.3",
|
||||
value:
|
||||
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress"
|
||||
"InternetGatewayDevice.WANDevice.*.WANConnectionDevice.*.WANIPConnection.*.ExternalIPAddress",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.parameters.4",
|
||||
value: "InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID"
|
||||
value: "InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.SSID",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.parameters.5",
|
||||
value:
|
||||
"InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase"
|
||||
"InternetGatewayDevice.LANDevice.*.WLANConfiguration.*.KeyPassphrase",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.parameters.6",
|
||||
value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName"
|
||||
value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.HostName",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.parameters.7",
|
||||
value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress"
|
||||
value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.IPAddress",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.components.2.parameters.8",
|
||||
value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress"
|
||||
value: "InternetGatewayDevice.LANDevice.*.Hosts.Host.*.MACAddress",
|
||||
},
|
||||
{
|
||||
_id: "ui.device.2.parameters.0.parameter",
|
||||
value: "DATE_STRING(Events.Inform)"
|
||||
value: "DATE_STRING(Events.Inform)",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.1.label", value: "'Serial number'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.1.parameter",
|
||||
value: "DeviceID.SerialNumber"
|
||||
value: "DeviceID.SerialNumber",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.2.label", value: "'Product class'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.2.parameter",
|
||||
value: "DeviceID.ProductClass"
|
||||
value: "DeviceID.ProductClass",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.3.label", value: "'OUI'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.3.parameter",
|
||||
value: "DeviceID.OUI"
|
||||
value: "DeviceID.OUI",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.4.label", value: "'Manufacturer'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.4.parameter",
|
||||
value: "DeviceID.Manufacturer"
|
||||
value: "DeviceID.Manufacturer",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.5.label", value: "'Hardware version'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.5.parameter",
|
||||
value: "InternetGatewayDevice.DeviceInfo.HardwareVersion"
|
||||
value: "InternetGatewayDevice.DeviceInfo.HardwareVersion",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.6.label", value: "'Software version'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.6.parameter",
|
||||
value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion"
|
||||
value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.7.label", value: "'MAC'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.7.parameter",
|
||||
value:
|
||||
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress"
|
||||
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.8.label", value: "'IP'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.8.parameter",
|
||||
value:
|
||||
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress"
|
||||
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.9.label", value: "'WLAN SSID'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.9.parameter",
|
||||
value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID"
|
||||
value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
|
||||
},
|
||||
{ _id: "ui.device.2.parameters.10.label", value: "'WLAN passphrase'" },
|
||||
{
|
||||
_id: "ui.device.2.parameters.10.parameter",
|
||||
value:
|
||||
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase"
|
||||
"InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase",
|
||||
},
|
||||
{ _id: "ui.device.3.type", value: "'parameter-table'" },
|
||||
{ _id: "ui.device.3.label", value: "'WLAN hosts'" },
|
||||
{ _id: "ui.device.3.label", value: "'LAN hosts'" },
|
||||
{
|
||||
_id: "ui.device.3.parameter",
|
||||
value: "InternetGatewayDevice.LANDevice.1.Hosts.Host"
|
||||
value: "InternetGatewayDevice.LANDevice.1.Hosts.Host",
|
||||
},
|
||||
{ _id: "ui.device.3.childParameters.0.label", value: "'Host name'" },
|
||||
{ _id: "ui.device.3.childParameters.0.parameter", value: "HostName" },
|
||||
@@ -279,12 +278,12 @@ export async function seed(options): Promise<void> {
|
||||
{ _id: "ui.device.5.element", value: "'div.container-full-width'" },
|
||||
{
|
||||
_id: "ui.device.5.components.0.components.0",
|
||||
value: "'All parameters'"
|
||||
value: "'All parameters'",
|
||||
},
|
||||
{ _id: "ui.device.5.components.0.element", value: "'h3'" },
|
||||
{ _id: "ui.device.5.components.0.type", value: "'container'" },
|
||||
{ _id: "ui.device.5.components.1.type", value: "'all-parameters'" },
|
||||
{ _id: "ui.device.6.type", value: "'device-actions'" }
|
||||
{ _id: "ui.device.6.type", value: "'device-actions'" },
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -299,18 +298,18 @@ export async function seed(options): Promise<void> {
|
||||
{ _id: "ui.index.2.label", value: "'Software version'" },
|
||||
{
|
||||
_id: "ui.index.2.parameter",
|
||||
value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion"
|
||||
value: "InternetGatewayDevice.DeviceInfo.SoftwareVersion",
|
||||
},
|
||||
{ _id: "ui.index.3.label", value: "'IP'" },
|
||||
{
|
||||
_id: "ui.index.3.parameter",
|
||||
value:
|
||||
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress"
|
||||
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress",
|
||||
},
|
||||
{ _id: "ui.index.4.label", value: "'SSID'" },
|
||||
{
|
||||
_id: "ui.index.4.parameter",
|
||||
value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID"
|
||||
value: "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
|
||||
},
|
||||
{ _id: "ui.index.5.type", value: "'container'" },
|
||||
{ _id: "ui.index.5.label", value: "'Last inform'" },
|
||||
@@ -323,7 +322,7 @@ export async function seed(options): Promise<void> {
|
||||
{ _id: "ui.index.6.label", value: "'Tags'" },
|
||||
{ _id: "ui.index.6.parameter", value: "Tags" },
|
||||
{ _id: "ui.index.6.unsortable", value: "true" },
|
||||
{ _id: "ui.index.6.writable", value: "false" }
|
||||
{ _id: "ui.index.6.writable", value: "false" },
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -332,43 +331,44 @@ export async function seed(options): Promise<void> {
|
||||
{ _id: "ui.overview.charts.online.label", value: "'Online status'" },
|
||||
{
|
||||
_id: "ui.overview.charts.online.slices.1_onlineNow.color",
|
||||
value: "'#31a354'"
|
||||
value: "'#31a354'",
|
||||
},
|
||||
{
|
||||
_id: "ui.overview.charts.online.slices.1_onlineNow.filter",
|
||||
value: "Events.Inform > NOW() - 5 * 60 * 1000"
|
||||
value: "Events.Inform > NOW() - 5 * 60 * 1000",
|
||||
},
|
||||
{
|
||||
_id: "ui.overview.charts.online.slices.1_onlineNow.label",
|
||||
value: "'Online now'"
|
||||
value: "'Online now'",
|
||||
},
|
||||
{
|
||||
_id: "ui.overview.charts.online.slices.2_past24.color",
|
||||
value: "'#a1d99b'"
|
||||
value: "'#a1d99b'",
|
||||
},
|
||||
{
|
||||
_id: "ui.overview.charts.online.slices.2_past24.filter",
|
||||
value:
|
||||
"Events.Inform > (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000) AND Events.Inform < (NOW() - 5 * 60 * 1000)"
|
||||
"Events.Inform > (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000) AND Events.Inform < (NOW() - 5 * 60 * 1000)",
|
||||
},
|
||||
{
|
||||
_id: "ui.overview.charts.online.slices.2_past24.label",
|
||||
value: "'Past 24 hours'"
|
||||
value: "'Past 24 hours'",
|
||||
},
|
||||
{
|
||||
_id: "ui.overview.charts.online.slices.3_others.color",
|
||||
value: "'#e5f5e0'"
|
||||
value: "'#e5f5e0'",
|
||||
},
|
||||
{
|
||||
_id: "ui.overview.charts.online.slices.3_others.filter",
|
||||
value: "Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)"
|
||||
value:
|
||||
"Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)",
|
||||
},
|
||||
{
|
||||
_id: "ui.overview.charts.online.slices.3_others.label",
|
||||
value: "'Others'"
|
||||
value: "'Others'",
|
||||
},
|
||||
{ _id: "ui.overview.groups.online.label", value: "''" },
|
||||
{ _id: "ui.overview.groups.online.charts.0", value: "'online'" }
|
||||
{ _id: "ui.overview.groups.online.charts.0", value: "'online'" },
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -378,16 +378,17 @@ export async function seed(options): Promise<void> {
|
||||
_id: "bootstrap",
|
||||
weight: 0,
|
||||
channel: "bootstrap",
|
||||
provision: "bootstrap"
|
||||
events: "0 BOOTSTRAP",
|
||||
provision: "bootstrap",
|
||||
},
|
||||
{ _id: "default", weight: 0, channel: "default", provision: "default" },
|
||||
{ _id: "inform", weight: 0, channel: "inform", provision: "inform" }
|
||||
{ _id: "inform", weight: 0, channel: "inform", provision: "inform" },
|
||||
];
|
||||
|
||||
resources["provisions"] = [
|
||||
{ _id: "bootstrap", script: BOOTSTRAP_SCRIPT },
|
||||
{ _id: "default", script: DEFAULT_SCRIPT },
|
||||
{ _id: "inform", script: INFORM_SCRIPT }
|
||||
{ _id: "inform", script: INFORM_SCRIPT },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ interface Instance {
|
||||
}
|
||||
|
||||
export default class InstanceSet {
|
||||
private set: Set<Instance>;
|
||||
private declare set: Set<Instance>;
|
||||
|
||||
public constructor() {
|
||||
this.set = new Set();
|
||||
|
||||
+91
-72
@@ -22,30 +22,39 @@ import * as crypto from "crypto";
|
||||
import * as config from "./config";
|
||||
import * as db from "./db";
|
||||
import * as cache from "./cache";
|
||||
import { mongoQueryToFilter } from "./mongodb-functions";
|
||||
import { convertOldPrecondition } from "./mongodb-functions";
|
||||
import * as logger from "./logger";
|
||||
import * as scheduling from "./scheduling";
|
||||
import * as expression from "./common/expression";
|
||||
import { parse } from "./common/expression-parser";
|
||||
import { Preset, Expression } from "./types";
|
||||
import {
|
||||
Preset,
|
||||
Expression,
|
||||
Provisions,
|
||||
VirtualParameters,
|
||||
Files,
|
||||
Users,
|
||||
Permissions,
|
||||
Config,
|
||||
UiConfig,
|
||||
} from "./types";
|
||||
|
||||
interface Permissions {
|
||||
[role: string]: {
|
||||
[access: number]: {
|
||||
[resource: string]: {
|
||||
access: number;
|
||||
filter: Expression;
|
||||
validate?: Expression;
|
||||
};
|
||||
};
|
||||
};
|
||||
interface Snapshot {
|
||||
presets: Preset[];
|
||||
provisions: Provisions;
|
||||
virtualParameters: VirtualParameters;
|
||||
files: Files;
|
||||
permissions: Permissions;
|
||||
users: Users;
|
||||
config: Config;
|
||||
ui: UiConfig;
|
||||
}
|
||||
|
||||
const REFRESH = 3000;
|
||||
const EVICT_TIMEOUT = 60000;
|
||||
const REFRESH = 5000;
|
||||
const EVICT_TIMEOUT = 120000;
|
||||
|
||||
const snapshots = new Map();
|
||||
let currentSnapshot = null;
|
||||
const snapshots = new Map<string, Snapshot>();
|
||||
let currentSnapshot: string = null;
|
||||
let nextRefresh = 1;
|
||||
|
||||
function computeHash(snapshot): string {
|
||||
@@ -98,11 +107,15 @@ function computeHash(snapshot): string {
|
||||
return h.digest("hex");
|
||||
}
|
||||
|
||||
function flattenObject(src, prefix = "", dst = {}): {} {
|
||||
function flattenObject(
|
||||
src: Record<string, unknown>,
|
||||
prefix = "",
|
||||
dst = {}
|
||||
): Record<string, unknown> {
|
||||
for (const k of Object.keys(src)) {
|
||||
const v = src[k];
|
||||
if (typeof v === "object" && !Array.isArray(v))
|
||||
flattenObject(v, `${prefix}${k}.`, dst);
|
||||
flattenObject(v as Record<string, unknown>, `${prefix}${k}.`, dst);
|
||||
else dst[`${prefix}${k}`] = v;
|
||||
}
|
||||
return dst;
|
||||
@@ -112,14 +125,14 @@ async function fetchPresets(): Promise<Preset[]> {
|
||||
const res = await db.getPresets();
|
||||
let objects = await db.getObjects();
|
||||
|
||||
objects = objects.map(obj => {
|
||||
objects = objects.map((obj) => {
|
||||
// Flatten object
|
||||
obj = flattenObject(obj);
|
||||
|
||||
// If no keys are defined, consider all parameters as keys to keep the
|
||||
// same behavior from v1.0
|
||||
if (!obj["_keys"] || !obj["_keys"].length)
|
||||
obj["_keys"] = Object.keys(obj).filter(k => !k.startsWith("_"));
|
||||
if (!obj["_keys"]?.length)
|
||||
obj["_keys"] = Object.keys(obj).filter((k) => !k.startsWith("_"));
|
||||
|
||||
return obj;
|
||||
});
|
||||
@@ -136,12 +149,9 @@ async function fetchPresets(): Promise<Preset[]> {
|
||||
if (preset["schedule"]) {
|
||||
const parts = preset["schedule"].trim().split(/\s+/);
|
||||
schedule = {
|
||||
md5: crypto
|
||||
.createHash("md5")
|
||||
.update(preset["schedule"])
|
||||
.digest("hex"),
|
||||
md5: crypto.createHash("md5").update(preset["schedule"]).digest("hex"),
|
||||
duration: null,
|
||||
schedule: null
|
||||
schedule: null,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -151,7 +161,7 @@ async function fetchPresets(): Promise<Preset[]> {
|
||||
logger.warn({
|
||||
message: "Invalid preset schedule",
|
||||
preset: preset["_id"],
|
||||
schedule: preset["schedule"]
|
||||
schedule: preset["schedule"],
|
||||
});
|
||||
schedule.schedule = false;
|
||||
}
|
||||
@@ -163,7 +173,9 @@ async function fetchPresets(): Promise<Preset[]> {
|
||||
try {
|
||||
precondition = parse(preset["precondition"]);
|
||||
} catch (error) {
|
||||
precondition = mongoQueryToFilter(JSON.parse(preset["precondition"]));
|
||||
precondition = convertOldPrecondition(
|
||||
JSON.parse(preset["precondition"])
|
||||
);
|
||||
}
|
||||
|
||||
// Simplify expression
|
||||
@@ -176,7 +188,7 @@ async function fetchPresets(): Promise<Preset[]> {
|
||||
for (const c of preset["configurations"]) {
|
||||
switch (c.type) {
|
||||
case "age":
|
||||
_provisions.push(["refresh", c.name, c.age]);
|
||||
_provisions.push(["refresh", c.name, +c.age]);
|
||||
break;
|
||||
|
||||
case "value":
|
||||
@@ -199,7 +211,7 @@ async function fetchPresets(): Promise<Preset[]> {
|
||||
for (const obj of objects) {
|
||||
if (obj["_id"] === c.object) {
|
||||
const alias = obj["_keys"]
|
||||
.map(k => `${k}:${JSON.stringify(obj[k])}`)
|
||||
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
||||
.join(",");
|
||||
const p = `${c.name}.[${alias}]`;
|
||||
_provisions.push(["instances", p, 1]);
|
||||
@@ -217,7 +229,7 @@ async function fetchPresets(): Promise<Preset[]> {
|
||||
for (const obj of objects) {
|
||||
if (obj["_id"] === c.object) {
|
||||
const alias = obj["_keys"]
|
||||
.map(k => `${k}:${JSON.stringify(obj[k])}`)
|
||||
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
|
||||
.join(",");
|
||||
const p = `${c.name}.[${alias}]`;
|
||||
_provisions.push(["instances", p, 0]);
|
||||
@@ -237,14 +249,14 @@ async function fetchPresets(): Promise<Preset[]> {
|
||||
schedule: schedule,
|
||||
events: events,
|
||||
precondition: precondition,
|
||||
provisions: _provisions
|
||||
provisions: _provisions,
|
||||
});
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
async function fetchProvisions(): Promise<{}> {
|
||||
async function fetchProvisions(): Promise<Provisions> {
|
||||
const res = await db.getProvisions();
|
||||
|
||||
const provisions = {};
|
||||
@@ -263,7 +275,7 @@ async function fetchProvisions(): Promise<{}> {
|
||||
return provisions;
|
||||
}
|
||||
|
||||
async function fetchVirtualParameters(): Promise<{}> {
|
||||
async function fetchVirtualParameters(): Promise<VirtualParameters> {
|
||||
const res = await db.getVirtualParameters();
|
||||
|
||||
const virtualParameters = {};
|
||||
@@ -292,7 +304,7 @@ async function fetchPermissions(): Promise<Permissions> {
|
||||
|
||||
permissions[p.role][p.access][p.resource] = {
|
||||
access: p.access,
|
||||
filter: parse(p.filter || "true")
|
||||
filter: parse(p.filter || "true"),
|
||||
};
|
||||
if (p.validate)
|
||||
permissions[p.role][p.access][p.resource].validate = parse(p.validate);
|
||||
@@ -301,7 +313,7 @@ async function fetchPermissions(): Promise<Permissions> {
|
||||
return permissions;
|
||||
}
|
||||
|
||||
async function fetchFiles(): Promise<{}> {
|
||||
async function fetchFiles(): Promise<Files> {
|
||||
const res = await db.getFiles();
|
||||
const files = {};
|
||||
|
||||
@@ -309,14 +321,12 @@ async function fetchFiles(): Promise<{}> {
|
||||
const id = r["filename"] || r["_id"].toString();
|
||||
files[id] = {};
|
||||
files[id].length = r["length"];
|
||||
files[id].md5 = r["md5"];
|
||||
files[id].contentType = r["contentType"];
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function fetchUsers(): Promise<{}> {
|
||||
async function fetchUsers(): Promise<Users> {
|
||||
const _users = await db.getUsers();
|
||||
const users = {};
|
||||
|
||||
@@ -324,14 +334,14 @@ async function fetchUsers(): Promise<{}> {
|
||||
users[user._id] = {
|
||||
password: user.password,
|
||||
salt: user.salt,
|
||||
roles: user.roles.split(",").map(s => s.trim())
|
||||
roles: user.roles.split(",").map((s) => s.trim()),
|
||||
};
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
async function fetchConfig(): Promise<[{}, {}]> {
|
||||
async function fetchConfig(): Promise<[Config, UiConfig]> {
|
||||
const conf = await db.getConfig();
|
||||
|
||||
conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0));
|
||||
@@ -340,7 +350,7 @@ async function fetchConfig(): Promise<[{}, {}]> {
|
||||
filters: {},
|
||||
device: {},
|
||||
index: {},
|
||||
overview: {}
|
||||
overview: {},
|
||||
};
|
||||
const _config = {};
|
||||
|
||||
@@ -350,12 +360,13 @@ async function fetchConfig(): Promise<[{}, {}]> {
|
||||
_config[c.id] = val;
|
||||
if (c.id.startsWith("ui.")) {
|
||||
const keys = c.id.split(".");
|
||||
if (!(keys[1] in ui)) continue;
|
||||
// remove the first key(ui)
|
||||
keys.shift();
|
||||
let ref = ui;
|
||||
while (keys.length > 1) {
|
||||
const k = keys.shift();
|
||||
if (typeof ref[k] !== "object") ref[k] = {};
|
||||
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
|
||||
ref = ref[k];
|
||||
}
|
||||
ref[keys[0]] = val;
|
||||
@@ -367,7 +378,7 @@ async function fetchConfig(): Promise<[{}, {}]> {
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
if (!nextRefresh) {
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
await refresh();
|
||||
return;
|
||||
}
|
||||
@@ -382,7 +393,7 @@ async function refresh(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const unlockOrExtend = await cache.lock("presets_hash_lock", 3);
|
||||
const lockToken = await cache.acquireLock("presets_hash_lock", 5000, 5000);
|
||||
|
||||
const res = await Promise.all([
|
||||
fetchPresets(),
|
||||
@@ -391,7 +402,7 @@ async function refresh(): Promise<void> {
|
||||
fetchFiles(),
|
||||
fetchPermissions(),
|
||||
fetchUsers(),
|
||||
fetchConfig()
|
||||
fetchConfig(),
|
||||
]);
|
||||
|
||||
const snapshot = {
|
||||
@@ -402,7 +413,7 @@ async function refresh(): Promise<void> {
|
||||
permissions: res[4],
|
||||
users: res[5],
|
||||
config: res[6][0],
|
||||
ui: res[6][1]
|
||||
ui: res[6][1],
|
||||
};
|
||||
|
||||
if (currentSnapshot) {
|
||||
@@ -416,7 +427,7 @@ async function refresh(): Promise<void> {
|
||||
currentSnapshot = computeHash(snapshot);
|
||||
snapshots.set(currentSnapshot, snapshot);
|
||||
await cache.set("presets_hash", currentSnapshot, 300);
|
||||
await unlockOrExtend(0);
|
||||
await cache.releaseLock("presets_hash_lock", lockToken);
|
||||
|
||||
nextRefresh = now + (REFRESH - (now % REFRESH));
|
||||
}
|
||||
@@ -426,45 +437,40 @@ export async function getCurrentSnapshot(): Promise<string> {
|
||||
return currentSnapshot;
|
||||
}
|
||||
|
||||
export function hasSnapshot(hash): boolean {
|
||||
export function hasSnapshot(hash: string): boolean {
|
||||
return snapshots.has(hash);
|
||||
}
|
||||
|
||||
export function getPresets(snapshotKey): Preset[] {
|
||||
export function getPresets(snapshotKey: string): Preset[] {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
return snapshot.presets;
|
||||
}
|
||||
|
||||
export function getProvisions(
|
||||
snapshotKey
|
||||
): { [name: string]: { md5: string; script: vm.Script } } {
|
||||
export function getProvisions(snapshotKey: string): Provisions {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
return snapshot.provisions;
|
||||
}
|
||||
|
||||
export function getVirtualParameters(
|
||||
snapshotKey
|
||||
): { [name: string]: { md5: string; script: vm.Script } } {
|
||||
export function getVirtualParameters(snapshotKey: string): VirtualParameters {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
return snapshot.virtualParameters;
|
||||
}
|
||||
|
||||
export function getFiles(
|
||||
snapshotKey
|
||||
): { [name: string]: { length: number; md5: string; contentType: string } } {
|
||||
export function getFiles(snapshotKey: string): Files {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
return snapshot.files;
|
||||
}
|
||||
|
||||
export function getConfig(
|
||||
snapshotKey,
|
||||
key,
|
||||
context?,
|
||||
now?
|
||||
snapshotKey: string,
|
||||
key: string,
|
||||
context: Record<string, unknown>,
|
||||
now: number,
|
||||
cb?: (e: Expression) => Expression
|
||||
): string | number | boolean | null {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
@@ -484,39 +490,52 @@ export function getConfig(
|
||||
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
|
||||
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
|
||||
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
|
||||
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT"
|
||||
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
|
||||
};
|
||||
|
||||
if (!(key in snapshot.config)) {
|
||||
if (key in oldOpts)
|
||||
return config.get(oldOpts[key], context ? context.id : null);
|
||||
else return null;
|
||||
if (key in oldOpts) {
|
||||
let id;
|
||||
if (context?.["id"]) {
|
||||
id = context["id"];
|
||||
} else if (cb) {
|
||||
id = cb(["PARAM", "DeviceID.ID"]);
|
||||
if (Array.isArray(id)) id = null;
|
||||
}
|
||||
return config.get(oldOpts[key], id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return expression.evaluate(snapshot.config[key], context, now || Date.now());
|
||||
|
||||
const v = expression.evaluate(snapshot.config[key], context, now, cb);
|
||||
return Array.isArray(v) ? null : v;
|
||||
}
|
||||
|
||||
export function getConfigExpression(snapshotKey, key): Expression {
|
||||
export function getConfigExpression(
|
||||
snapshotKey: string,
|
||||
key: string
|
||||
): Expression {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
|
||||
return snapshot.config[key];
|
||||
}
|
||||
|
||||
export function getUsers(snapshotKey): {} {
|
||||
export function getUsers(snapshotKey: string): Users {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
|
||||
return snapshot.users;
|
||||
}
|
||||
|
||||
export function getPermissions(snapshotKey): {} {
|
||||
export function getPermissions(snapshotKey: string): Permissions {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
|
||||
return snapshot.permissions;
|
||||
}
|
||||
|
||||
export function getUiConfig(snapshotKey): {} {
|
||||
export function getUiConfig(snapshotKey: string): UiConfig {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
|
||||
|
||||
+77
-48
@@ -21,6 +21,15 @@ import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
|
||||
import * as config from "./config";
|
||||
import { getRequestOrigin } from "./forwarded";
|
||||
import {
|
||||
SessionContext,
|
||||
AcsRequest,
|
||||
CpeRequest,
|
||||
CpeFault,
|
||||
InformRequest,
|
||||
Fault,
|
||||
} from "./types";
|
||||
|
||||
const REOPEN_EVERY = 60000;
|
||||
|
||||
@@ -66,7 +75,7 @@ function reopen(): void {
|
||||
if (!(stat && stat.dev === logStat.dev && stat.ino === logStat.ino)) {
|
||||
logStream.end();
|
||||
logStream = fs.createWriteStream(null, {
|
||||
fd: fs.openSync(LOG_FILE, "a")
|
||||
fd: fs.openSync(LOG_FILE, "a"),
|
||||
});
|
||||
logStat = fs.fstatSync(logStream.fd);
|
||||
}
|
||||
@@ -90,7 +99,7 @@ function reopen(): void {
|
||||
) {
|
||||
accessLogStream.end();
|
||||
accessLogStream = fs.createWriteStream(null, {
|
||||
fd: fs.openSync(ACCESS_LOG_FILE, "a")
|
||||
fd: fs.openSync(ACCESS_LOG_FILE, "a"),
|
||||
});
|
||||
accessLogStat = fs.fstatSync(accessLogStream.fd);
|
||||
}
|
||||
@@ -104,7 +113,7 @@ function reopen(): void {
|
||||
setTimeout(reopen, REOPEN_EVERY - (Date.now() % REOPEN_EVERY)).unref();
|
||||
}
|
||||
|
||||
export function init(service, version): void {
|
||||
export function init(service: string, version: string): void {
|
||||
defaultMeta.hostname = os.hostname();
|
||||
defaultMeta.pid = process.pid;
|
||||
defaultMeta.name = `genieacs-${service}`;
|
||||
@@ -120,7 +129,7 @@ export function init(service, version): void {
|
||||
|
||||
if (ACCESS_LOG_FILE) {
|
||||
accessLogStream = fs.createWriteStream(null, {
|
||||
fd: fs.openSync(ACCESS_LOG_FILE, "a")
|
||||
fd: fs.openSync(ACCESS_LOG_FILE, "a"),
|
||||
});
|
||||
accessLogStat = fs.fstatSync(accessLogStream.fd);
|
||||
}
|
||||
@@ -146,61 +155,75 @@ export function close(): void {
|
||||
logStream.end();
|
||||
}
|
||||
|
||||
export function flatten(details): {} {
|
||||
export function flatten(
|
||||
details: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
if (details.sessionContext) {
|
||||
details.deviceId = details.sessionContext.deviceId;
|
||||
details.remoteAddress =
|
||||
details.sessionContext.httpRequest.connection.remoteAddress;
|
||||
const sessionContext = details.sessionContext as SessionContext;
|
||||
details.deviceId = sessionContext.deviceId;
|
||||
details.remoteAddress = getRequestOrigin(
|
||||
sessionContext.httpRequest
|
||||
).remoteAddress;
|
||||
delete details.sessionContext;
|
||||
}
|
||||
|
||||
if (details.exception) {
|
||||
details.exceptionName = details.exception.name;
|
||||
details.exceptionMessage = details.exception.message;
|
||||
details.exceptionStack = details.exception.stack;
|
||||
const err = details.exception as Error;
|
||||
details.exceptionName = err.name;
|
||||
details.exceptionMessage = err.message;
|
||||
details.exceptionStack = err.stack;
|
||||
delete details.exception;
|
||||
}
|
||||
|
||||
if (details.task) {
|
||||
details.taskId = details.task._id;
|
||||
details.taskId = details.task["_id"];
|
||||
delete details.task;
|
||||
}
|
||||
|
||||
if (details.rpc) {
|
||||
if (details.rpc.acsRequest) {
|
||||
details.acsRequestId = details.rpc.id;
|
||||
details.acsRequestName = details.rpc.acsRequest.name;
|
||||
if (details.rpc.acsRequest.commandKey)
|
||||
details.acsRequestCommandKey = details.rpc.acsRequest.commandKey;
|
||||
} else if (details.rpc.cpeRequest) {
|
||||
details.cpeRequestId = details.rpc.id;
|
||||
if (details.rpc.cpeRequest.name === "Inform") {
|
||||
details.informEvent = details.rpc.cpeRequest.event.join(",");
|
||||
details.informRetryCount = details.rpc.cpeRequest.retryCount;
|
||||
const rpc = details.rpc as {
|
||||
id: string;
|
||||
acsRequest?: AcsRequest;
|
||||
cpeRequest?: CpeRequest;
|
||||
cpeFault?: CpeFault;
|
||||
};
|
||||
if (rpc.acsRequest) {
|
||||
details.acsRequestId = rpc.id;
|
||||
details.acsRequestName = rpc.acsRequest.name;
|
||||
if (rpc.acsRequest["commandKey"])
|
||||
details.acsRequestCommandKey = rpc.acsRequest["commandKey"];
|
||||
} else if (rpc.cpeRequest) {
|
||||
details.cpeRequestId = rpc.id;
|
||||
if (rpc.cpeRequest.name === "Inform") {
|
||||
details.informEvent = (rpc.cpeRequest as InformRequest).event.join(",");
|
||||
details.informRetryCount = (rpc.cpeRequest as InformRequest).retryCount;
|
||||
} else {
|
||||
details.cpeRequestName = details.rpc.cpeRequest.name;
|
||||
if (details.rpc.cpeRequest.commandKey)
|
||||
details.cpeRequestCommandKey = details.rpc.cpeRequest.commandKey;
|
||||
details.cpeRequestName = rpc.cpeRequest.name;
|
||||
if (rpc.cpeRequest["commandKey"])
|
||||
details.cpeRequestCommandKey = rpc.cpeRequest["commandKey"];
|
||||
}
|
||||
} else if (details.rpc.cpeFault) {
|
||||
details.acsRequestId = details.rpc.id;
|
||||
details.cpeFaultCode = details.rpc.cpeFault.detail.faultCode;
|
||||
details.cpeFaultString = details.rpc.cpeFault.detail.faultString;
|
||||
} else if (rpc.cpeFault) {
|
||||
details.acsRequestId = rpc.id;
|
||||
details.cpeFaultCode = rpc.cpeFault.detail.faultCode;
|
||||
details.cpeFaultString = rpc.cpeFault.detail.faultString;
|
||||
}
|
||||
delete details.rpc;
|
||||
}
|
||||
|
||||
if (details.fault) {
|
||||
details.faultCode = details.fault.code;
|
||||
details.faultMessage = details.fault.message;
|
||||
const fault = details.fault as Fault;
|
||||
details.faultCode = fault.code;
|
||||
details.faultMessage = fault.message;
|
||||
delete details.fault;
|
||||
}
|
||||
|
||||
// For genieacs-ui
|
||||
if (details.context) {
|
||||
details.remoteAddress = details.context.request.ip;
|
||||
if (details.context.state.user)
|
||||
details.user = details.context.state.user.username;
|
||||
details.remoteAddress = getRequestOrigin(
|
||||
details.context["req"]
|
||||
).remoteAddress;
|
||||
if (details.context["state"].user)
|
||||
details.user = details.context["state"].user.username;
|
||||
delete details.context;
|
||||
}
|
||||
|
||||
@@ -210,7 +233,10 @@ export function flatten(details): {} {
|
||||
return details;
|
||||
}
|
||||
|
||||
function formatJson(details, systemd): string {
|
||||
function formatJson(
|
||||
details: Record<string, unknown>,
|
||||
systemd: boolean
|
||||
): string {
|
||||
if (systemd) {
|
||||
let severity = "";
|
||||
if (details.severity === "info") severity = "<6>";
|
||||
@@ -223,14 +249,17 @@ function formatJson(details, systemd): string {
|
||||
return `${JSON.stringify(flatten(details))}${os.EOL}`;
|
||||
}
|
||||
|
||||
function formatSimple(details, systemd): string {
|
||||
function formatSimple(
|
||||
details: Record<string, unknown>,
|
||||
systemd: boolean
|
||||
): string {
|
||||
const skip = {
|
||||
user: true,
|
||||
remoteAddress: true,
|
||||
severity: true,
|
||||
timestamp: true,
|
||||
message: true,
|
||||
deviceId: !!details.sessionContext
|
||||
deviceId: !!details.sessionContext,
|
||||
};
|
||||
|
||||
flatten(details);
|
||||
@@ -263,12 +292,12 @@ function formatSimple(details, systemd): string {
|
||||
return `${severity}${remote}${details.message}${meta}${os.EOL}`;
|
||||
}
|
||||
|
||||
return `${details.timestamp} [${details.severity.toUpperCase()}] ${remote}${
|
||||
details.message
|
||||
}${meta}${os.EOL}`;
|
||||
return `${details.timestamp} [${(
|
||||
details.severity as string
|
||||
).toUpperCase()}] ${remote}${details.message}${meta}${os.EOL}`;
|
||||
}
|
||||
|
||||
function log(details): void {
|
||||
function log(details: Record<string, unknown>): void {
|
||||
details.timestamp = new Date().toISOString();
|
||||
if (LOG_FORMAT === "json") {
|
||||
details = Object.assign({}, defaultMeta, details);
|
||||
@@ -278,22 +307,22 @@ function log(details): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function info(details): void {
|
||||
export function info(details: Record<string, unknown>): void {
|
||||
details.severity = "info";
|
||||
log(details);
|
||||
}
|
||||
|
||||
export function warn(details): void {
|
||||
export function warn(details: Record<string, unknown>): void {
|
||||
details.severity = "warn";
|
||||
log(details);
|
||||
}
|
||||
|
||||
export function error(details): void {
|
||||
export function error(details: Record<string, unknown>): void {
|
||||
details.severity = "error";
|
||||
log(details);
|
||||
}
|
||||
|
||||
export function accessLog(details): void {
|
||||
export function accessLog(details: Record<string, unknown>): void {
|
||||
details.timestamp = new Date().toISOString();
|
||||
if (ACCESS_LOG_FORMAT === "json") {
|
||||
Object.assign(details, defaultMeta);
|
||||
@@ -303,17 +332,17 @@ export function accessLog(details): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function accessInfo(details): void {
|
||||
export function accessInfo(details: Record<string, unknown>): void {
|
||||
details.severity = "info";
|
||||
accessLog(details);
|
||||
}
|
||||
|
||||
export function accessWarn(details): void {
|
||||
export function accessWarn(details: Record<string, unknown>): void {
|
||||
details.severity = "warn";
|
||||
accessLog(details);
|
||||
}
|
||||
|
||||
export function accessError(details): void {
|
||||
export function accessError(details: Record<string, unknown>): void {
|
||||
details.severity = "error";
|
||||
accessLog(details);
|
||||
}
|
||||
+302
-247
@@ -17,15 +17,16 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ObjectID } from "mongodb";
|
||||
import { map, parse, stringify } from "./common/expression-parser";
|
||||
import { likePatternToRegExp, evaluate } from "./common/expression";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { map, parse, stringify, parseList } from "./common/expression-parser";
|
||||
import { likePatternToRegExp } from "./common/expression";
|
||||
import { Expression, Fault, Task } from "./types";
|
||||
import { decodeTag, encodeTag } from "./common";
|
||||
|
||||
const isArray = Array.isArray;
|
||||
|
||||
export function processDeviceFilter(filter): Expression {
|
||||
return map(filter, exp => {
|
||||
export function processDeviceFilter(filter: Expression): Expression {
|
||||
return map(filter, (exp) => {
|
||||
if (!isArray(exp)) return exp;
|
||||
|
||||
if (exp[0] === "PARAM") {
|
||||
@@ -34,8 +35,6 @@ export function processDeviceFilter(filter): Expression {
|
||||
else if (p === "DeviceID") return ["PARAM", "_deviceId"];
|
||||
else if (p.startsWith("DeviceID."))
|
||||
return ["PARAM", "_deviceId._" + p.slice(9)];
|
||||
else if (p === "Tags") return ["PARAM", "_tags"];
|
||||
else if (p === "tag") return ["PARAM", "_tags"];
|
||||
else if (p === "Events.Inform") return ["PARAM", "_lastInform"];
|
||||
else if (p === "Events.Registered") return ["PARAM", "_registered"];
|
||||
else if (p === "Events.0_BOOTSTRAP") return ["PARAM", "_lastBootstrap"];
|
||||
@@ -45,15 +44,26 @@ export function processDeviceFilter(filter): Expression {
|
||||
exp[1][0] === "PARAM" &&
|
||||
exp[1][1].startsWith("Tags.")
|
||||
) {
|
||||
const t = exp[1][1].slice(5);
|
||||
if (exp[0] === "IS NULL") return ["<>", ["PARAM", "_tags"], t];
|
||||
else if (exp[0] === "IS NOT NULL") return ["=", ["PARAM", "_tags"], t];
|
||||
else if (exp[0] === "=" && exp[2] === true)
|
||||
return ["=", ["PARAM", "_tags"], t];
|
||||
const t = decodeTag(exp[1][1].slice(5));
|
||||
if (exp[0] === "IS NULL") return ["_tags", t, false];
|
||||
else if (exp[0] === "IS NOT NULL") return ["_tags", t, true];
|
||||
else if (exp[0] === "=" && exp[2] === true) return ["_tags", t, true];
|
||||
else if (exp[0] === "<>" && exp[2] !== true) return ["_tags", t, true];
|
||||
} else if (
|
||||
["=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"].includes(exp[0])
|
||||
[
|
||||
"=",
|
||||
"<>",
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<=",
|
||||
"LIKE",
|
||||
"NOT LIKE",
|
||||
"IS NULL",
|
||||
"IS NOT NULL",
|
||||
].includes(exp[0])
|
||||
) {
|
||||
let e = map(exp, ee => {
|
||||
let e = map(exp, (ee) => {
|
||||
if (
|
||||
isArray(ee) &&
|
||||
ee[0] === "PARAM" &&
|
||||
@@ -75,12 +85,12 @@ export function processDeviceFilter(filter): Expression {
|
||||
});
|
||||
}
|
||||
|
||||
export function processTasksFilter(filter): Expression {
|
||||
return map(filter, exp => {
|
||||
export function processTasksFilter(filter: Expression): Expression {
|
||||
return map(filter, (exp) => {
|
||||
if (!isArray(exp)) return exp;
|
||||
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
|
||||
const e = exp.slice();
|
||||
if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectID(e[2]);
|
||||
if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectId(e[2]);
|
||||
else if (e[1][0] === "PARAM" && e[1][1] === "timestamp")
|
||||
e[2] = new Date(e[2]);
|
||||
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
|
||||
@@ -91,8 +101,8 @@ export function processTasksFilter(filter): Expression {
|
||||
});
|
||||
}
|
||||
|
||||
export function processFaultsFilter(filter): Expression {
|
||||
return map(filter, exp => {
|
||||
export function processFaultsFilter(filter: Expression): Expression {
|
||||
return map(filter, (exp) => {
|
||||
if (!isArray(exp)) return exp;
|
||||
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
|
||||
const e = exp.slice();
|
||||
@@ -105,140 +115,137 @@ export function processFaultsFilter(filter): Expression {
|
||||
});
|
||||
}
|
||||
|
||||
export function filterToMongoQuery(exp: Expression): {} {
|
||||
const ops = {
|
||||
OR: 0,
|
||||
AND: 0,
|
||||
NOT: 0,
|
||||
"=": 1,
|
||||
"<>": 1,
|
||||
">": 1,
|
||||
">=": 1,
|
||||
"<": 1,
|
||||
"<=": 1,
|
||||
LIKE: 1,
|
||||
"NOT LIKE": 1,
|
||||
"IS NULL": 1,
|
||||
"IS NOT NULL": 1
|
||||
};
|
||||
|
||||
function recursive(filter, negate, res = {}): {} {
|
||||
export function filterToMongoQuery(exp: Expression): Record<string, unknown> {
|
||||
function recursive(filter, negate): Record<string, unknown> {
|
||||
const op = filter[0];
|
||||
|
||||
if (ops[op] === 0) {
|
||||
for (let i = 1; i < filter.length; ++i) {
|
||||
if (!isArray(filter[i]) || ops[filter[i][0]] == null)
|
||||
throw new Error(`Invalid expression in ${op} clause`);
|
||||
}
|
||||
|
||||
if ((!negate && op === "AND") || (negate && op === "OR")) {
|
||||
res["$and"] = res["$and"] || [];
|
||||
for (let i = 1; i < filter.length; ++i)
|
||||
res["$and"].push(recursive(filter[i], negate));
|
||||
} else if ((!negate && op === "OR") || (negate && op === "AND")) {
|
||||
res["$or"] = res["$or"] || [];
|
||||
|
||||
for (let i = 1; i < filter.length; ++i)
|
||||
res["$or"].push(recursive(filter[i], negate));
|
||||
} else if (op === "NOT") {
|
||||
recursive(filter[1], !negate, res);
|
||||
}
|
||||
} else if (ops[op] === 1) {
|
||||
if (isArray(filter[2])) {
|
||||
throw new Error(`Right operand of ${op} clause is not a primitive`);
|
||||
} else if (
|
||||
["LIKE", "NOT LIKE"].includes(op) &&
|
||||
isArray(filter[1]) &&
|
||||
filter[1][0] === "FUNC" &&
|
||||
["UPPER", "LOWER"].includes(filter[1][1])
|
||||
) {
|
||||
if (
|
||||
(filter[1][1] === "UPPER" && filter[2] !== filter[2].toUpperCase()) ||
|
||||
(filter[1][1] === "LOWER" && filter[2] !== filter[2].toLowerCase())
|
||||
) {
|
||||
throw new Error(
|
||||
`Cannot compare ${
|
||||
filter[1][1]
|
||||
}() against non-${filter[1][1].toLowerCase()}case pattern`
|
||||
);
|
||||
}
|
||||
} else if (!isArray(filter[1]) || filter[1][0] !== "PARAM") {
|
||||
throw new Error(`Left operand of ${op} clause is not a parameter`);
|
||||
}
|
||||
|
||||
if (op === "=") {
|
||||
const param = filter[1][1];
|
||||
const p = (res[param] = res[param] || {});
|
||||
if (negate) p["$ne"] = filter[2];
|
||||
else p["$eq"] = filter[2];
|
||||
} else if (op === "<>") {
|
||||
const param = filter[1][1];
|
||||
let p = (res[param] = res[param] || {});
|
||||
if (negate) p = p["$not"] = p["$not"] || {};
|
||||
p["$ne"] = filter[2];
|
||||
if (param !== "_tags") p["$exists"] = true;
|
||||
} else if (op === ">") {
|
||||
const param = filter[1][1];
|
||||
let p = (res[param] = res[param] || {});
|
||||
if (negate) p = p["$not"] = p["$not"] || {};
|
||||
p["$gt"] = filter[2];
|
||||
} else if (op === ">=") {
|
||||
const param = filter[1][1];
|
||||
let p = (res[param] = res[param] || {});
|
||||
if (negate) p = p["$not"] = p["$not"] || {};
|
||||
p["$gte"] = filter[2];
|
||||
} else if (op === "<") {
|
||||
const param = filter[1][1];
|
||||
let p = (res[param] = res[param] || {});
|
||||
if (negate) p = p["$not"] = p["$not"] || {};
|
||||
p["$lt"] = filter[2];
|
||||
} else if (op === "<=") {
|
||||
const param = filter[1][1];
|
||||
let p = (res[param] = res[param] || {});
|
||||
if (negate) p = p["$not"] = p["$not"] || {};
|
||||
p["$lte"] = filter[2];
|
||||
} else if (op === "IS NULL") {
|
||||
const param = filter[1][1];
|
||||
res[param] = { $exists: negate };
|
||||
} else if (op === "IS NOT NULL") {
|
||||
const param = filter[1][1];
|
||||
res[param] = { $exists: !negate };
|
||||
} else {
|
||||
if (op === "NOT LIKE") negate = !negate;
|
||||
let param;
|
||||
let flags;
|
||||
if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") {
|
||||
param = filter[1][2][1];
|
||||
flags = "i";
|
||||
} else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") {
|
||||
param = filter[1][2][1];
|
||||
flags = "i";
|
||||
} else {
|
||||
param = filter[1][1];
|
||||
flags = "";
|
||||
}
|
||||
const r = likePatternToRegExp(filter[2], filter[3], flags);
|
||||
if (negate) res[param] = { $not: r };
|
||||
else res[param] = r;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unrecognized operator ${op}`);
|
||||
if (op === "AND") {
|
||||
filter = filter.filter((f) => f !== true);
|
||||
if (filter.length === 2) return recursive(filter[1], negate);
|
||||
}
|
||||
|
||||
return res;
|
||||
if (op === "OR") {
|
||||
filter = filter.filter((f) => f !== false);
|
||||
if (filter.length === 2) return recursive(filter[1], negate);
|
||||
}
|
||||
|
||||
if ((!negate && op === "AND") || (negate && op === "OR"))
|
||||
return { $and: filter.slice(1).map((f) => recursive(f, negate)) };
|
||||
|
||||
if ((!negate && op === "OR") || (negate && op === "AND"))
|
||||
return { $or: filter.slice(1).map((f) => recursive(f, negate)) };
|
||||
|
||||
if (op === "NOT") return recursive(filter[1], !negate);
|
||||
|
||||
if (isArray(filter[2]))
|
||||
throw new Error(`Invalid RHS operand of ${op} clause`);
|
||||
|
||||
if (op === "LIKE" || op === "NOT LIKE") {
|
||||
if (op === "NOT LIKE") negate = !negate;
|
||||
let param;
|
||||
let flags;
|
||||
if (!Array.isArray(filter[1]))
|
||||
throw new Error(`Invalid LHS operand of ${op} clause`);
|
||||
if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") {
|
||||
if (filter[2] !== filter[2].toUpperCase())
|
||||
throw new Error(`Invalid RHS operand of ${op} clause`);
|
||||
param = filter[1][2][1];
|
||||
flags = "i";
|
||||
} else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") {
|
||||
if (filter[2] !== filter[2].toLowerCase())
|
||||
throw new Error(`Invalid RHS operand of ${op} clause`);
|
||||
param = filter[1][2][1];
|
||||
flags = "i";
|
||||
} else if (filter[1][0] === "PARAM") {
|
||||
param = filter[1][1];
|
||||
flags = "";
|
||||
} else {
|
||||
throw new Error(`Invalid LHS operand of ${op} clause`);
|
||||
}
|
||||
const r = likePatternToRegExp(filter[2], filter[3], flags);
|
||||
if (negate) return { [param]: { $nin: [r, null] } };
|
||||
else return { [param]: r };
|
||||
}
|
||||
|
||||
if (op === "_tags") {
|
||||
let t = filter[2];
|
||||
if (negate) t = !t;
|
||||
if (t) return { _tags: filter[1] };
|
||||
else return { _tags: { $ne: filter[1] } };
|
||||
}
|
||||
|
||||
if (!isArray(filter[1]) || filter[1][0] !== "PARAM")
|
||||
throw new Error(`Invalid LHS operand of ${op} clause`);
|
||||
|
||||
if (op === "IS NULL") {
|
||||
const p = filter[1][1];
|
||||
return { [p]: null };
|
||||
}
|
||||
|
||||
if (op === "IS NOT NULL") {
|
||||
const p = filter[1][1];
|
||||
return { [p]: { $ne: null } };
|
||||
}
|
||||
|
||||
if (isArray(filter[2]) || filter[2] == null)
|
||||
throw new Error(`Invalid RHS operand of ${op} clause`);
|
||||
|
||||
if (op === "=") {
|
||||
const p = filter[1][1];
|
||||
const v = filter[2];
|
||||
if (!negate) return { [p]: v };
|
||||
else return { [p]: { $nin: [v, null] } };
|
||||
}
|
||||
|
||||
if (op === "<>") {
|
||||
const p = filter[1][1];
|
||||
const v = filter[2];
|
||||
if (negate) return { [p]: v };
|
||||
else return { [p]: { $nin: [v, null] } };
|
||||
}
|
||||
|
||||
if (op === ">") {
|
||||
const p = filter[1][1];
|
||||
const v = filter[2];
|
||||
if (negate) return { [p]: { $lte: v } };
|
||||
else return { [p]: { $gt: v } };
|
||||
}
|
||||
|
||||
if (op === ">=") {
|
||||
const p = filter[1][1];
|
||||
const v = filter[2];
|
||||
if (negate) return { [p]: { $lt: v } };
|
||||
else return { [p]: { $gte: v } };
|
||||
}
|
||||
|
||||
if (op === "<") {
|
||||
const p = filter[1][1];
|
||||
const v = filter[2];
|
||||
if (negate) return { [p]: { $gte: v } };
|
||||
else return { [p]: { $lt: v } };
|
||||
}
|
||||
|
||||
if (op === "<=") {
|
||||
const p = filter[1][1];
|
||||
const v = filter[2];
|
||||
if (negate) return { [p]: { $gt: v } };
|
||||
else return { [p]: { $lte: v } };
|
||||
}
|
||||
|
||||
throw new Error(`Unrecognized operator ${op}`);
|
||||
}
|
||||
|
||||
const _exp = evaluate(exp, null, Date.now());
|
||||
|
||||
if (!isArray(_exp)) {
|
||||
if (_exp === true) return {};
|
||||
if (!isArray(exp)) {
|
||||
if (exp === true) return {};
|
||||
throw new Error("Primitives are not valid queries");
|
||||
}
|
||||
|
||||
return recursive(_exp, false);
|
||||
return recursive(exp, false);
|
||||
}
|
||||
|
||||
export function processDeviceProjection(projection: {}): {} {
|
||||
export function processDeviceProjection(
|
||||
projection: Record<string, 1>
|
||||
): Record<string, 1> {
|
||||
if (!projection) return projection;
|
||||
const p = {};
|
||||
for (const [k, v] of Object.entries(projection)) {
|
||||
@@ -264,7 +271,9 @@ export function processDeviceProjection(projection: {}): {} {
|
||||
return p;
|
||||
}
|
||||
|
||||
export function processDeviceSort(sort: {}): {} {
|
||||
export function processDeviceSort(
|
||||
sort: Record<string, number>
|
||||
): Record<string, number> {
|
||||
if (!sort) return sort;
|
||||
const s = {};
|
||||
for (const [k, v] of Object.entries(sort)) {
|
||||
@@ -280,70 +289,81 @@ export function processDeviceSort(sort: {}): {} {
|
||||
return s;
|
||||
}
|
||||
|
||||
function parseDate(d): number | string {
|
||||
function parseDate(d: Date): number | string {
|
||||
const n = +d;
|
||||
return isNaN(n) ? "" + d : n;
|
||||
}
|
||||
|
||||
interface FlatDevice {
|
||||
[param: string]: {
|
||||
object: boolean;
|
||||
objectTimestamp: number;
|
||||
writable: boolean;
|
||||
writableTimestamp: number;
|
||||
value: [string | number | boolean, string];
|
||||
valueTimestamp: number;
|
||||
};
|
||||
interface FlatAttributes {
|
||||
object?: boolean;
|
||||
objectTimestamp?: number;
|
||||
writable?: boolean;
|
||||
writableTimestamp?: number;
|
||||
value?: [string | number | boolean, string];
|
||||
valueTimestamp?: number;
|
||||
notification?: number;
|
||||
notificationTimestamp?: number;
|
||||
accessList?: string[];
|
||||
accessListTimestamp?: number;
|
||||
}
|
||||
|
||||
export function flattenDevice(device): FlatDevice {
|
||||
function recursive(input, root, output, timestamp): void {
|
||||
interface FlatDevice {
|
||||
[param: string]: FlatAttributes;
|
||||
}
|
||||
|
||||
export function flattenDevice(device: Record<string, unknown>): FlatDevice {
|
||||
function recursive(
|
||||
input,
|
||||
root: string,
|
||||
output: FlatDevice,
|
||||
timestamp: number
|
||||
): void {
|
||||
for (const [name, tree] of Object.entries(input)) {
|
||||
if (!root) {
|
||||
if (name === "_lastInform") {
|
||||
output["Events.Inform"] = {
|
||||
value: [parseDate(tree), "xsd:dateTime"],
|
||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
} else if (name === "_registered") {
|
||||
output["Events.Registered"] = {
|
||||
value: [parseDate(tree), "xsd:dateTime"],
|
||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
} else if (name === "_lastBoot") {
|
||||
output["Events.1_BOOT"] = {
|
||||
value: [parseDate(tree), "xsd:dateTime"],
|
||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
} else if (name === "_lastBootstrap") {
|
||||
output["Events.0_BOOTSTRAP"] = {
|
||||
value: [parseDate(tree), "xsd:dateTime"],
|
||||
value: [parseDate(tree as Date), "xsd:dateTime"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
} else if (name === "_id") {
|
||||
output["DeviceID.ID"] = {
|
||||
value: [tree, "xsd:string"],
|
||||
value: [tree as string, "xsd:string"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
} else if (name === "_deviceId") {
|
||||
output["DeviceID.Manufacturer"] = {
|
||||
@@ -352,7 +372,7 @@ export function flattenDevice(device): FlatDevice {
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["DeviceID.OUI"] = {
|
||||
value: [tree["_OUI"], "xsd:string"],
|
||||
@@ -360,7 +380,7 @@ export function flattenDevice(device): FlatDevice {
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["DeviceID.ProductClass"] = {
|
||||
value: [tree["_ProductClass"], "xsd:string"],
|
||||
@@ -368,7 +388,7 @@ export function flattenDevice(device): FlatDevice {
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
output["DeviceID.SerialNumber"] = {
|
||||
value: [tree["_SerialNumber"], "xsd:string"],
|
||||
@@ -376,24 +396,24 @@ export function flattenDevice(device): FlatDevice {
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
} else if (name === "_tags") {
|
||||
output["Tags"] = {
|
||||
writable: true,
|
||||
writable: false,
|
||||
writableTimestamp: timestamp,
|
||||
object: true,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
|
||||
for (const t of tree as string[]) {
|
||||
output[`Tags.${t}`] = {
|
||||
output[`Tags.${encodeTag(t)}`] = {
|
||||
value: [true, "xsd:boolean"],
|
||||
valueTimestamp: timestamp,
|
||||
writable: false,
|
||||
writable: true,
|
||||
writableTimestamp: timestamp,
|
||||
object: false,
|
||||
objectTimestamp: timestamp
|
||||
objectTimestamp: timestamp,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -407,55 +427,67 @@ export function flattenDevice(device): FlatDevice {
|
||||
else if (+input["_timestamp"] > timestamp)
|
||||
childrenTimestamp = +input["_timestamp"];
|
||||
|
||||
const attrs = {};
|
||||
const attrs: FlatAttributes = {};
|
||||
if (tree["_value"] != null) {
|
||||
attrs["value"] = [
|
||||
attrs.value = [
|
||||
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
|
||||
tree["_type"]
|
||||
tree["_type"],
|
||||
];
|
||||
attrs["valueTimestamp"] = +(tree["_timestamp"] || childrenTimestamp);
|
||||
attrs["object"] = false;
|
||||
attrs["objectTimestamp"] = childrenTimestamp;
|
||||
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp);
|
||||
attrs.object = false;
|
||||
attrs.objectTimestamp = childrenTimestamp;
|
||||
} else if (tree["_object"] != null) {
|
||||
attrs["object"] = tree["_object"];
|
||||
attrs["objectTimestamp"] = childrenTimestamp;
|
||||
attrs.object = tree["_object"];
|
||||
attrs.objectTimestamp = childrenTimestamp;
|
||||
}
|
||||
|
||||
if (tree["_writable"] != null) {
|
||||
attrs["writable"] = tree["_writable"];
|
||||
attrs["writableTimestamp"] = childrenTimestamp;
|
||||
attrs.writable = tree["_writable"];
|
||||
attrs.writableTimestamp = childrenTimestamp;
|
||||
}
|
||||
|
||||
if (tree["_notification"] != null) {
|
||||
attrs.notification = tree["_notification"];
|
||||
attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1;
|
||||
}
|
||||
|
||||
if (tree["_accessList"] != null) {
|
||||
attrs.accessList = tree["_accessList"];
|
||||
attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1;
|
||||
}
|
||||
|
||||
const r = root ? `${root}.${name}` : name;
|
||||
output[r] = attrs;
|
||||
|
||||
if (attrs["object"] || tree["object"] == null)
|
||||
if (attrs.object || tree["object"] == null)
|
||||
recursive(tree, r, output, childrenTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
const newDevice = {};
|
||||
const timestamp = new Date(device["_lastInform"] || 1).getTime();
|
||||
const newDevice: FlatDevice = {};
|
||||
const timestamp = new Date((device["_lastInform"] as Date) || 1).getTime();
|
||||
recursive(device, "", newDevice, timestamp);
|
||||
return newDevice;
|
||||
}
|
||||
|
||||
export function flattenFault(fault): Fault {
|
||||
const f = Object.assign({}, fault);
|
||||
export function flattenFault(fault: unknown): Fault {
|
||||
const f = Object.assign({}, fault) as Fault;
|
||||
if (f.timestamp) f.timestamp = +f.timestamp;
|
||||
if (f.expiry) f.expiry = +f.expiry;
|
||||
return f;
|
||||
if (f["expiry"]) f["expiry"] = +f["expiry"];
|
||||
return f as Fault;
|
||||
}
|
||||
|
||||
export function flattenTask(task): Task {
|
||||
const t = Object.assign({}, task);
|
||||
export function flattenTask(task: unknown): Task {
|
||||
const t = Object.assign({}, task) as Task;
|
||||
t._id = "" + t._id;
|
||||
if (t.timestamp) t.timestamp = +t.timestamp;
|
||||
if (t["timestamp"]) t["timestamp"] = +t["timestamp"];
|
||||
if (t.expiry) t.expiry = +t.expiry;
|
||||
return t;
|
||||
}
|
||||
|
||||
export function mongoQueryToFilter(query): Expression {
|
||||
export function convertOldPrecondition(
|
||||
query: Record<string, unknown>
|
||||
): Expression {
|
||||
function recursive(_query): Expression {
|
||||
const expressions: Expression[] = [];
|
||||
for (const [k, v] of Object.entries(_query)) {
|
||||
@@ -474,14 +506,27 @@ export function mongoQueryToFilter(query): Expression {
|
||||
} else if (k === "_tags") {
|
||||
if (typeof v === "object") {
|
||||
if (isArray(v)) throw new Error(`Invalid type`);
|
||||
|
||||
if (v.hasOwnProperty("$ne"))
|
||||
expressions.push(["IS NULL", ["PARAM", `Tags.${v["$ne"]}`]]);
|
||||
else if (v.hasOwnProperty("$eq"))
|
||||
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v["$eq"]}`]]);
|
||||
else throw new Error(`Invalid tag query`);
|
||||
const conjs: Expression[] = [];
|
||||
for (const [op, val] of Object.entries(v)) {
|
||||
if (op === "$ne") {
|
||||
if (typeof v["$ne"] !== "string")
|
||||
throw new Error("Only string values are allowed for _tags");
|
||||
conjs.push(["IS NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
|
||||
} else if (op === "$eq") {
|
||||
if (typeof v["$eq"] !== "string")
|
||||
throw new Error("Only string values are allowed for _tags");
|
||||
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
|
||||
} else {
|
||||
throw new Error(`Invalid tag query`);
|
||||
}
|
||||
}
|
||||
if (conjs.length === 1) expressions.push(conjs[0]);
|
||||
else if (conjs.length > 1) expressions.push(["AND", ...conjs]);
|
||||
} else {
|
||||
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]);
|
||||
expressions.push([
|
||||
"IS NOT NULL",
|
||||
["PARAM", `Tags.${encodeTag(v as string)}`],
|
||||
]);
|
||||
}
|
||||
} else if (k.startsWith("Tags.")) {
|
||||
let exists: boolean;
|
||||
@@ -497,41 +542,41 @@ export function mongoQueryToFilter(query): Expression {
|
||||
|
||||
const exps: Expression[] = [];
|
||||
for (const [kk, vv] of Object.entries(v)) {
|
||||
let op;
|
||||
switch (kk) {
|
||||
case "$eq":
|
||||
op = "=";
|
||||
break;
|
||||
case "$ne":
|
||||
op = "<>";
|
||||
break;
|
||||
case "$lt":
|
||||
op = "<";
|
||||
break;
|
||||
case "$lte":
|
||||
op = "<=";
|
||||
break;
|
||||
case "$gt":
|
||||
op = ">";
|
||||
break;
|
||||
case "$gte":
|
||||
op = ">=";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Operator ${kk} not supported`);
|
||||
if (kk === "$eq") {
|
||||
exps.push(["=", ["PARAM", k], vv]);
|
||||
} else if (kk === "$ne") {
|
||||
exps.push([
|
||||
"OR",
|
||||
["<>", ["PARAM", k], vv],
|
||||
["IS NULL", ["PARAM", k]],
|
||||
]);
|
||||
} else if (kk === "$lt") {
|
||||
exps.push(["<", ["PARAM", k], vv]);
|
||||
} else if (kk === "$lte") {
|
||||
exps.push(["<=", ["PARAM", k], vv]);
|
||||
} else if (kk === "$gt") {
|
||||
exps.push([">", ["PARAM", k], vv]);
|
||||
} else if (kk === "$gte") {
|
||||
exps.push([">=", ["PARAM", k], vv]);
|
||||
} else {
|
||||
throw new Error(`Operator ${kk} not supported`);
|
||||
}
|
||||
exps.push([op, ["PARAM", k], vv]);
|
||||
if (!["string", "number", "boolean"].includes(typeof vv))
|
||||
throw new Error(`Invalid value for ${kk} operator`);
|
||||
}
|
||||
if (exps.length === 1) {
|
||||
expressions.push(exps[0]);
|
||||
} else if (exps.length > 1) {
|
||||
const and: Expression = ["AND"];
|
||||
expressions.push(and.concat(exps));
|
||||
}
|
||||
if (exps.length === 1) return exps[0];
|
||||
const and: Expression = ["AND"];
|
||||
expressions.push(and.concat(exps));
|
||||
} else {
|
||||
expressions.push(["=", ["PARAM", k], v]);
|
||||
}
|
||||
}
|
||||
if (expressions.length === 1) return expressions[0];
|
||||
const and: Expression = ["AND"];
|
||||
return and.concat(expressions);
|
||||
if (expressions.length === 0) return true;
|
||||
return ["AND", ...expressions];
|
||||
}
|
||||
|
||||
// empty filter
|
||||
@@ -540,15 +585,21 @@ export function mongoQueryToFilter(query): Expression {
|
||||
return recursive(query);
|
||||
}
|
||||
|
||||
export function flattenPreset(preset): {} {
|
||||
export function flattenPreset(
|
||||
preset: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const p = Object.assign({}, preset);
|
||||
if (p.precondition) {
|
||||
try {
|
||||
// Try parse to check expression validity
|
||||
parse(p.precondition);
|
||||
parse(p.precondition as string);
|
||||
} catch (error) {
|
||||
p.precondition = mongoQueryToFilter(JSON.parse(p.precondition));
|
||||
p.precondition = p.precondition.length ? stringify(p.precondition) : "";
|
||||
p.precondition = convertOldPrecondition(
|
||||
JSON.parse(p.precondition as string)
|
||||
);
|
||||
p.precondition = (p.precondition as string).length
|
||||
? stringify(p.precondition as Expression)
|
||||
: "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,14 +611,14 @@ export function flattenPreset(preset): {} {
|
||||
|
||||
const provision = p.configurations[0];
|
||||
if (
|
||||
p.configurations.length === 1 &&
|
||||
(p.configurations as any[]).length === 1 &&
|
||||
provision.type === "provision" &&
|
||||
provision.name &&
|
||||
provision.name.length
|
||||
) {
|
||||
p.provision = provision.name;
|
||||
p.provisionArgs = provision.args
|
||||
? JSON.stringify(provision.args).slice(1, -1)
|
||||
? provision.args.map((a) => stringify(a)).join(", ")
|
||||
: "";
|
||||
}
|
||||
|
||||
@@ -575,30 +626,34 @@ export function flattenPreset(preset): {} {
|
||||
return p;
|
||||
}
|
||||
|
||||
export function flattenFile(file): {} {
|
||||
export function flattenFile(
|
||||
file: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const f = {};
|
||||
f["_id"] = file["_id"];
|
||||
if (file.metadata) {
|
||||
f["metadata.fileType"] = file.metadata.fileType || "";
|
||||
f["metadata.oui"] = file.metadata.oui || "";
|
||||
f["metadata.productClass"] = file.metadata.productClass || "";
|
||||
f["metadata.version"] = file.metadata.version || "";
|
||||
f["metadata.fileType"] = file["metadata"]["fileType"] || "";
|
||||
f["metadata.oui"] = file["metadata"]["oui"] || "";
|
||||
f["metadata.productClass"] = file["metadata"]["productClass"] || "";
|
||||
f["metadata.version"] = file["metadata"]["version"] || "";
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
export function preProcessPreset(data): {} {
|
||||
export function preProcessPreset(
|
||||
data: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const preset = Object.assign({}, data);
|
||||
|
||||
if (!preset.precondition) preset.precondition = "";
|
||||
// Try parse to check expression validity
|
||||
parse(preset.precondition);
|
||||
parse(preset.precondition as string);
|
||||
|
||||
preset.weight = parseInt(preset.weight) || 0;
|
||||
preset.weight = parseInt(preset.weight as string) || 0;
|
||||
|
||||
const events = {};
|
||||
if (preset.events) {
|
||||
for (let e of preset.events.split(",")) {
|
||||
for (let e of (preset.events as string).split(",")) {
|
||||
let v = true;
|
||||
e = e.trim();
|
||||
if (e.startsWith("-")) {
|
||||
@@ -616,11 +671,11 @@ export function preProcessPreset(data): {} {
|
||||
const configuration = {
|
||||
type: "provision",
|
||||
name: preset.provision,
|
||||
args: null
|
||||
args: null,
|
||||
};
|
||||
|
||||
if (preset.provisionArgs)
|
||||
configuration.args = JSON.parse(`[${preset.provisionArgs}]`);
|
||||
configuration.args = parseList(preset.provisionArgs as string);
|
||||
|
||||
delete preset.provision;
|
||||
delete preset.provisionArgs;
|
||||
|
||||
+320
-179
@@ -18,20 +18,23 @@
|
||||
*/
|
||||
|
||||
import * as url from "url";
|
||||
import * as mongodb from "mongodb";
|
||||
import * as querystring from "querystring";
|
||||
import { Collection, GridFSBucket, ObjectId } from "mongodb";
|
||||
import * as vm from "vm";
|
||||
import { exec } from "child_process";
|
||||
import * as config from "./config";
|
||||
import * as db from "./db";
|
||||
import { onConnect } from "./db";
|
||||
import * as query from "./query";
|
||||
import * as apiFunctions from "./api-functions";
|
||||
import { IncomingMessage, ServerResponse } from "http";
|
||||
import * as cache from "./cache";
|
||||
import { version as VERSION } from "../package.json";
|
||||
import { ping } from "./ping";
|
||||
import * as logger from "./logger";
|
||||
import { flattenDevice } from "./mongodb-functions";
|
||||
|
||||
const DEVICE_TASKS_REGEX = /^\/devices\/([a-zA-Z0-9\-_%]+)\/tasks\/?$/;
|
||||
const 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 OBJECTS_REGEX = /^\/objects\/([a-zA-Z0-9\-_%]+)\/?$/;
|
||||
const FILES_REGEX = /^\/files\/([a-zA-Z0-9%!*'();:@&=+$,?#[\]\-_.~]+)\/?$/;
|
||||
@@ -39,10 +42,31 @@ const PING_REGEX = /^\/ping\/([a-zA-Z0-9\-_.:]+)\/?$/;
|
||||
const QUERY_REGEX = /^\/([a-zA-Z0-9_]+)\/?$/;
|
||||
const DELETE_DEVICE_REGEX = /^\/devices\/([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\-_%:]+)\/?$/;
|
||||
|
||||
function throwError(err, httpResponse?): never {
|
||||
const collections: Record<string, Collection> = {
|
||||
tasks: null as Collection,
|
||||
devices: null as Collection,
|
||||
presets: null as Collection,
|
||||
objects: null as Collection,
|
||||
files: null as Collection,
|
||||
provisions: null as Collection,
|
||||
virtualParameters: null as Collection,
|
||||
faults: null as Collection,
|
||||
};
|
||||
let filesBucket: GridFSBucket;
|
||||
|
||||
onConnect(async (db) => {
|
||||
for (const k of Object.keys(collections)) {
|
||||
if (k === "files") collections[k] = db.collection("fs.files");
|
||||
else collections[k] = db.collection(k);
|
||||
}
|
||||
filesBucket = new GridFSBucket(db);
|
||||
});
|
||||
|
||||
function throwError(err: Error, httpResponse?: ServerResponse): never {
|
||||
if (httpResponse) {
|
||||
httpResponse.writeHead(500, { Connection: "close" });
|
||||
httpResponse.end(`${err.name}: ${err.message}`);
|
||||
@@ -50,7 +74,10 @@ function throwError(err, httpResponse?): never {
|
||||
throw err;
|
||||
}
|
||||
|
||||
export function listener(request, response): void {
|
||||
export function listener(
|
||||
request: IncomingMessage,
|
||||
response: ServerResponse
|
||||
): void {
|
||||
const chunks = [];
|
||||
let bytes = 0;
|
||||
response.setHeader("GenieACS-Version", VERSION);
|
||||
@@ -74,18 +101,33 @@ export function listener(request, response): void {
|
||||
request.addListener("end", () => {
|
||||
const body = getBody();
|
||||
const urlParts = url.parse(request.url, true);
|
||||
|
||||
logger.accessInfo(
|
||||
Object.assign({}, urlParts.query, {
|
||||
remoteAddress: request.socket.remoteAddress,
|
||||
message: `${request.method} ${urlParts.pathname}`,
|
||||
})
|
||||
);
|
||||
|
||||
if (PRESETS_REGEX.test(urlParts.pathname)) {
|
||||
const presetName = querystring.unescape(
|
||||
const presetName = decodeURIComponent(
|
||||
PRESETS_REGEX.exec(urlParts.pathname)[1]
|
||||
);
|
||||
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;
|
||||
db.presetsCollection.replaceOne(
|
||||
collections.presets.replaceOne(
|
||||
{ _id: presetName },
|
||||
preset,
|
||||
{ upsert: true },
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -94,7 +136,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -102,7 +144,7 @@ export function listener(request, response): void {
|
||||
}
|
||||
);
|
||||
} else if (request.method === "DELETE") {
|
||||
db.presetsCollection.deleteOne({ _id: presetName }, err => {
|
||||
collections.presets.deleteOne({ _id: presetName }, (err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -111,7 +153,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -122,17 +164,24 @@ export function listener(request, response): void {
|
||||
response.end("405 Method Not Allowed");
|
||||
}
|
||||
} else if (OBJECTS_REGEX.test(urlParts.pathname)) {
|
||||
const objectName = querystring.unescape(
|
||||
const objectName = decodeURIComponent(
|
||||
OBJECTS_REGEX.exec(urlParts.pathname)[1]
|
||||
);
|
||||
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;
|
||||
db.objectsCollection.replaceOne(
|
||||
collections.objects.replaceOne(
|
||||
{ _id: objectName },
|
||||
object,
|
||||
{ upsert: true },
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -141,7 +190,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -149,7 +198,7 @@ export function listener(request, response): void {
|
||||
}
|
||||
);
|
||||
} else if (request.method === "DELETE") {
|
||||
db.objectsCollection.deleteOne({ _id: objectName }, err => {
|
||||
collections.objects.deleteOne({ _id: objectName }, (err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -158,7 +207,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -169,13 +218,13 @@ export function listener(request, response): void {
|
||||
response.end("405 Method Not Allowed");
|
||||
}
|
||||
} else if (PROVISIONS_REGEX.test(urlParts.pathname)) {
|
||||
const provisionName = querystring.unescape(
|
||||
const provisionName = decodeURIComponent(
|
||||
PROVISIONS_REGEX.exec(urlParts.pathname)[1]
|
||||
);
|
||||
if (request.method === "PUT") {
|
||||
const object = {
|
||||
_id: provisionName,
|
||||
script: body.toString()
|
||||
script: body.toString(),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -186,11 +235,11 @@ export function listener(request, response): void {
|
||||
return;
|
||||
}
|
||||
|
||||
db.provisionsCollection.replaceOne(
|
||||
collections.provisions.replaceOne(
|
||||
{ _id: provisionName },
|
||||
object,
|
||||
{ upsert: true },
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -199,7 +248,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -207,7 +256,7 @@ export function listener(request, response): void {
|
||||
}
|
||||
);
|
||||
} else if (request.method === "DELETE") {
|
||||
db.provisionsCollection.deleteOne({ _id: provisionName }, err => {
|
||||
collections.provisions.deleteOne({ _id: provisionName }, (err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -216,7 +265,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -227,13 +276,13 @@ export function listener(request, response): void {
|
||||
response.end("405 Method Not Allowed");
|
||||
}
|
||||
} else if (VIRTUAL_PARAMETERS_REGEX.test(urlParts.pathname)) {
|
||||
const virtualParameterName = querystring.unescape(
|
||||
const virtualParameterName = decodeURIComponent(
|
||||
VIRTUAL_PARAMETERS_REGEX.exec(urlParts.pathname)[1]
|
||||
);
|
||||
if (request.method === "PUT") {
|
||||
const object = {
|
||||
_id: virtualParameterName,
|
||||
script: body.toString()
|
||||
script: body.toString(),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -244,11 +293,11 @@ export function listener(request, response): void {
|
||||
return;
|
||||
}
|
||||
|
||||
db.virtualParametersCollection.replaceOne(
|
||||
collections.virtualParameters.replaceOne(
|
||||
{ _id: virtualParameterName },
|
||||
object,
|
||||
{ upsert: true },
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -257,7 +306,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -265,9 +314,9 @@ export function listener(request, response): void {
|
||||
}
|
||||
);
|
||||
} else if (request.method === "DELETE") {
|
||||
db.virtualParametersCollection.deleteOne(
|
||||
collections.virtualParameters.deleteOne(
|
||||
{ _id: virtualParameterName },
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -276,7 +325,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -289,23 +338,23 @@ export function listener(request, response): void {
|
||||
}
|
||||
} else if (TAGS_REGEX.test(urlParts.pathname)) {
|
||||
const r = TAGS_REGEX.exec(urlParts.pathname);
|
||||
const deviceId = querystring.unescape(r[1]);
|
||||
const tag = querystring.unescape(r[2]);
|
||||
const deviceId = decodeURIComponent(r[1]);
|
||||
const tag = decodeURIComponent(r[2]);
|
||||
if (request.method === "POST") {
|
||||
db.devicesCollection.updateOne(
|
||||
collections.devices.updateOne(
|
||||
{ _id: deviceId },
|
||||
{ $addToSet: { _tags: tag } },
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
}
|
||||
);
|
||||
} else if (request.method === "DELETE") {
|
||||
db.devicesCollection.updateOne(
|
||||
collections.devices.updateOne(
|
||||
{ _id: deviceId },
|
||||
{ $pull: { _tags: tag } },
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
response.writeHead(200);
|
||||
@@ -318,17 +367,17 @@ export function listener(request, response): void {
|
||||
}
|
||||
} else if (FAULTS_REGEX.test(urlParts.pathname)) {
|
||||
if (request.method === "DELETE") {
|
||||
const faultId = querystring.unescape(
|
||||
const faultId = decodeURIComponent(
|
||||
FAULTS_REGEX.exec(urlParts.pathname)[1]
|
||||
);
|
||||
const deviceId = faultId.split(":", 1)[0];
|
||||
const channel = faultId.slice(deviceId.length + 1);
|
||||
db.faultsCollection.deleteOne({ _id: faultId }, err => {
|
||||
collections.faults.deleteOne({ _id: faultId }, (err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
if (channel.startsWith("task_")) {
|
||||
const objId = new mongodb.ObjectID(channel.slice(5));
|
||||
return void db.tasksCollection.deleteOne({ _id: objId }, err => {
|
||||
const objId = new ObjectId(channel.slice(5));
|
||||
return void collections.tasks.deleteOne({ _id: objId }, (err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -337,7 +386,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -351,7 +400,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -363,100 +412,169 @@ export function listener(request, response): void {
|
||||
}
|
||||
} else if (DEVICE_TASKS_REGEX.test(urlParts.pathname)) {
|
||||
if (request.method === "POST") {
|
||||
const deviceId = querystring.unescape(
|
||||
const deviceId = decodeURIComponent(
|
||||
DEVICE_TASKS_REGEX.exec(urlParts.pathname)[1]
|
||||
);
|
||||
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;
|
||||
apiFunctions
|
||||
.insertTasks(task)
|
||||
.then(() => {
|
||||
cache
|
||||
.del(`${deviceId}_tasks_faults_operations`)
|
||||
.then(() => {
|
||||
if (urlParts.query.connection_request != null) {
|
||||
apiFunctions
|
||||
.connectionRequest(deviceId)
|
||||
.then(() => {
|
||||
const taskTimeout =
|
||||
(urlParts.query.timeout &&
|
||||
parseInt(urlParts.query.timeout as string)) ||
|
||||
config.get("DEVICE_ONLINE_THRESHOLD", deviceId);
|
||||
collections.devices
|
||||
.findOne({ _id: deviceId })
|
||||
.then(async (dev) => {
|
||||
if (!dev) {
|
||||
response.writeHead(404);
|
||||
response.end("No such device");
|
||||
return;
|
||||
}
|
||||
|
||||
apiFunctions
|
||||
.watchTask(deviceId, task._id, taskTimeout)
|
||||
.then(status => {
|
||||
if (status === "timeout") {
|
||||
response.writeHead(
|
||||
202,
|
||||
"Task queued but not processed",
|
||||
{
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
);
|
||||
response.end(JSON.stringify(task));
|
||||
} else if (status === "fault") {
|
||||
db.tasksCollection.findOne(
|
||||
{ _id: task._id },
|
||||
(err, task2) => {
|
||||
if (err)
|
||||
return void throwError(err, response);
|
||||
const device = flattenDevice(dev);
|
||||
await apiFunctions.insertTasks(task);
|
||||
|
||||
response.writeHead(202, "Task faulted", {
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
response.end(JSON.stringify(task2));
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
response.end(JSON.stringify(task));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
response.writeHead(202, err.message, {
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
response.end(JSON.stringify(task));
|
||||
});
|
||||
} else {
|
||||
response.writeHead(202, {
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
response.end(JSON.stringify(task));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
const lastInform = Date.now();
|
||||
|
||||
const socketTimeout: number = request.socket["timeout"];
|
||||
|
||||
// Disable socket timeout while waiting for session
|
||||
if (socketTimeout) request.socket.setTimeout(0);
|
||||
|
||||
const notInSession = await apiFunctions.awaitSessionEnd(
|
||||
deviceId,
|
||||
30000
|
||||
);
|
||||
await cache.del(`${deviceId}_tasks_faults_operations`);
|
||||
if (urlParts.query.connection_request == null) {
|
||||
if (socketTimeout) request.socket.setTimeout(socketTimeout);
|
||||
response.writeHead(202, {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
response.end(JSON.stringify(task));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!notInSession) {
|
||||
if (socketTimeout) request.socket.setTimeout(socketTimeout);
|
||||
response.writeHead(202, "Task queued but not processed", {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
response.end(JSON.stringify(task));
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await apiFunctions.connectionRequest(
|
||||
deviceId,
|
||||
device
|
||||
);
|
||||
|
||||
if (status) {
|
||||
if (socketTimeout) request.socket.setTimeout(socketTimeout);
|
||||
response.writeHead(202, status, {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
response.end(JSON.stringify(task));
|
||||
return;
|
||||
}
|
||||
|
||||
const onlineThreshold =
|
||||
(urlParts.query.timeout &&
|
||||
parseInt(urlParts.query.timeout as string)) ||
|
||||
(config.get("DEVICE_ONLINE_THRESHOLD", deviceId) as number);
|
||||
|
||||
const sessionStarted = await apiFunctions.awaitSessionStart(
|
||||
deviceId,
|
||||
lastInform,
|
||||
onlineThreshold
|
||||
);
|
||||
if (!sessionStarted) {
|
||||
if (socketTimeout) request.socket.setTimeout(socketTimeout);
|
||||
response.writeHead(202, "Task queued but not processed", {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
response.end(JSON.stringify(task));
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionEnded = await apiFunctions.awaitSessionEnd(
|
||||
deviceId,
|
||||
120000
|
||||
);
|
||||
if (!sessionEnded) {
|
||||
if (socketTimeout) request.socket.setTimeout(socketTimeout);
|
||||
response.writeHead(202, "Task queued but not processed", {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
response.end(JSON.stringify(task));
|
||||
return;
|
||||
}
|
||||
|
||||
const prom1 = collections.tasks.findOne(
|
||||
{ _id: task._id },
|
||||
{ projection: { _id: 1 } }
|
||||
);
|
||||
const prom2 = collections.faults.findOne(
|
||||
{ _id: `${deviceId}:task_${task._id}` },
|
||||
{
|
||||
projection: { _id: 1 },
|
||||
}
|
||||
);
|
||||
|
||||
const [t, f] = await Promise.all([prom1, prom2]);
|
||||
|
||||
// Restore socket timeout
|
||||
if (socketTimeout) request.socket.setTimeout(socketTimeout);
|
||||
|
||||
if (f) {
|
||||
response.writeHead(202, "Task faulted", {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
response.end(JSON.stringify(t || task));
|
||||
} else if (t) {
|
||||
response.writeHead(202, "Task queued but not processed", {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
response.end(JSON.stringify(t));
|
||||
} else {
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
response.end(JSON.stringify(task));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
});
|
||||
} else if (urlParts.query.connection_request != null) {
|
||||
// No task, send connection request only
|
||||
apiFunctions
|
||||
.connectionRequest(deviceId)
|
||||
.then(() => {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
collections.devices
|
||||
.findOne({ _id: deviceId })
|
||||
.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.end();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
response.writeHead(504);
|
||||
response.end(`${err.name}: ${err.message}`);
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
response.writeHead(400);
|
||||
@@ -468,12 +586,12 @@ export function listener(request, response): void {
|
||||
}
|
||||
} else if (TASKS_REGEX.test(urlParts.pathname)) {
|
||||
const r = TASKS_REGEX.exec(urlParts.pathname);
|
||||
const taskId = querystring.unescape(r[1]);
|
||||
const taskId = decodeURIComponent(r[1]);
|
||||
const action = r[2];
|
||||
if (!action || action === "/") {
|
||||
if (request.method === "DELETE") {
|
||||
db.tasksCollection.findOne(
|
||||
{ _id: new mongodb.ObjectID(taskId) },
|
||||
collections.tasks.findOne(
|
||||
{ _id: new ObjectId(taskId) },
|
||||
{ projection: { device: 1 } },
|
||||
(err, task) => {
|
||||
if (err) return void throwError(err, response);
|
||||
@@ -485,14 +603,14 @@ export function listener(request, response): void {
|
||||
}
|
||||
|
||||
const deviceId = task.device;
|
||||
db.tasksCollection.deleteOne(
|
||||
{ _id: new mongodb.ObjectID(taskId) },
|
||||
err => {
|
||||
collections.tasks.deleteOne(
|
||||
{ _id: new ObjectId(taskId) },
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
db.faultsCollection.deleteOne(
|
||||
collections.faults.deleteOne(
|
||||
{ _id: `${deviceId}:task_${taskId}` },
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -501,7 +619,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -518,16 +636,16 @@ export function listener(request, response): void {
|
||||
}
|
||||
} else if (action === "/retry") {
|
||||
if (request.method === "POST") {
|
||||
db.tasksCollection.findOne(
|
||||
{ _id: new mongodb.ObjectID(taskId) },
|
||||
collections.tasks.findOne(
|
||||
{ _id: new ObjectId(taskId) },
|
||||
{ projection: { device: 1 } },
|
||||
(err, task) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
const deviceId = task.device;
|
||||
db.faultsCollection.deleteOne(
|
||||
collections.faults.deleteOne(
|
||||
{ _id: `${deviceId}:task_${taskId}` },
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err, response);
|
||||
|
||||
cache
|
||||
@@ -536,7 +654,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -554,7 +672,7 @@ export function listener(request, response): void {
|
||||
response.end();
|
||||
}
|
||||
} else if (FILES_REGEX.test(urlParts.pathname)) {
|
||||
const filename = querystring.unescape(
|
||||
const filename = decodeURIComponent(
|
||||
FILES_REGEX.exec(urlParts.pathname)[1]
|
||||
);
|
||||
if (request.method === "PUT") {
|
||||
@@ -562,25 +680,36 @@ export function listener(request, response): void {
|
||||
fileType: request.headers.filetype,
|
||||
oui: request.headers.oui,
|
||||
productClass: request.headers.productclass,
|
||||
version: request.headers.version
|
||||
version: request.headers.version,
|
||||
};
|
||||
const bucket = new mongodb.GridFSBucket(db.client.db());
|
||||
const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
|
||||
metadata: metadata
|
||||
});
|
||||
filesBucket.delete(filename as unknown as ObjectId, () => {
|
||||
const uploadStream = filesBucket.openUploadStreamWithId(
|
||||
filename as unknown as ObjectId,
|
||||
filename,
|
||||
{
|
||||
metadata: metadata,
|
||||
}
|
||||
);
|
||||
|
||||
uploadStream.on("error", err => {
|
||||
throwError(err, response);
|
||||
});
|
||||
uploadStream.on("error", (err) => {
|
||||
throwError(err, response);
|
||||
});
|
||||
|
||||
uploadStream.end(body, () => {
|
||||
response.writeHead(201);
|
||||
response.end();
|
||||
uploadStream.end(body, () => {
|
||||
response.writeHead(201);
|
||||
response.end();
|
||||
});
|
||||
});
|
||||
} else if (request.method === "DELETE") {
|
||||
const bucket = new mongodb.GridFSBucket(db.client.db());
|
||||
bucket.delete((filename as unknown) as mongodb.ObjectId, err => {
|
||||
if (err) return void throwError(err, response);
|
||||
filesBucket.delete(filename as unknown as ObjectId, (err) => {
|
||||
if (err) {
|
||||
if (err.message.startsWith("FileNotFound")) {
|
||||
response.writeHead(404);
|
||||
response.end("404 Not Found");
|
||||
return;
|
||||
}
|
||||
return void throwError(err, response);
|
||||
}
|
||||
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
@@ -590,9 +719,14 @@ export function listener(request, response): void {
|
||||
response.end("405 Method Not Allowed");
|
||||
}
|
||||
} else if (PING_REGEX.test(urlParts.pathname)) {
|
||||
const host = querystring.unescape(PING_REGEX.exec(urlParts.pathname)[1]);
|
||||
exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => {
|
||||
const host = decodeURIComponent(PING_REGEX.exec(urlParts.pathname)[1]);
|
||||
ping(host, (err, res, stdout) => {
|
||||
if (err) {
|
||||
if (!res) {
|
||||
response.writeHead(500, { Connection: "close" });
|
||||
response.end(`${err.name}: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
response.writeHead(404, { "Cache-Control": "no-cache" });
|
||||
response.end(`${err.name}: ${err.message}`);
|
||||
return;
|
||||
@@ -600,7 +734,7 @@ export function listener(request, response): void {
|
||||
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "text/plain",
|
||||
"Cache-Control": "no-cache"
|
||||
"Cache-Control": "no-cache",
|
||||
});
|
||||
response.end(stdout);
|
||||
});
|
||||
@@ -611,7 +745,7 @@ export function listener(request, response): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceId = querystring.unescape(
|
||||
const deviceId = decodeURIComponent(
|
||||
DELETE_DEVICE_REGEX.exec(urlParts.pathname)[1]
|
||||
);
|
||||
apiFunctions
|
||||
@@ -620,7 +754,7 @@ export function listener(request, response): void {
|
||||
response.writeHead(200);
|
||||
response.end();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
setTimeout(() => {
|
||||
throwError(err, response);
|
||||
});
|
||||
@@ -644,7 +778,7 @@ export function listener(request, response): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const collection = db[`${collectionName}Collection`];
|
||||
const collection = collections[collectionName];
|
||||
if (!collection) {
|
||||
response.writeHead(404);
|
||||
response.end("404 Not Found");
|
||||
@@ -668,15 +802,15 @@ export function listener(request, response): void {
|
||||
break;
|
||||
case "tasks":
|
||||
q = query.sanitizeQueryTypes(q, {
|
||||
_id: v => new mongodb.ObjectID(v),
|
||||
timestamp: v => new Date(v),
|
||||
retries: Number
|
||||
_id: (v) => new ObjectId(v as string),
|
||||
timestamp: (v) => new Date(v as number),
|
||||
retries: Number,
|
||||
});
|
||||
break;
|
||||
case "faults":
|
||||
q = query.sanitizeQueryTypes(q, {
|
||||
timestamp: v => new Date(v),
|
||||
retries: Number
|
||||
timestamp: (v) => new Date(v as number),
|
||||
retries: Number,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -690,7 +824,14 @@ export function listener(request, response): void {
|
||||
const cur = collection.find(q, { projection: projection });
|
||||
|
||||
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 = {};
|
||||
for (const [k, v] of Object.entries(s)) {
|
||||
if (k[k.lastIndexOf(".") + 1] !== "_" && collectionName === "devices")
|
||||
@@ -701,18 +842,12 @@ export function listener(request, response): void {
|
||||
cur.sort(sort);
|
||||
}
|
||||
|
||||
if (urlParts.query.skip)
|
||||
cur.skip(parseInt(urlParts.query.skip as string));
|
||||
|
||||
if (urlParts.query.limit)
|
||||
cur.limit(parseInt(urlParts.query.limit as string));
|
||||
|
||||
cur.count(false, (err, total) => {
|
||||
cur.count((err, total) => {
|
||||
if (err) return void throwError(err);
|
||||
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "application/json",
|
||||
total: total
|
||||
total: total,
|
||||
});
|
||||
|
||||
if (request.method === "HEAD") {
|
||||
@@ -720,14 +855,20 @@ export function listener(request, response): void {
|
||||
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");
|
||||
i = 0;
|
||||
cur.forEach(
|
||||
item => {
|
||||
(item) => {
|
||||
if (i++) response.write(",\n");
|
||||
response.write(JSON.stringify(item));
|
||||
},
|
||||
err => {
|
||||
(err) => {
|
||||
if (err) return void throwError(err);
|
||||
response.end("\n]");
|
||||
}
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright 2013-2019 GenieACS Inc.
|
||||
*
|
||||
* This file is part of GenieACS.
|
||||
*
|
||||
* GenieACS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* GenieACS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { platform } from "os";
|
||||
import { exec } from "child_process";
|
||||
|
||||
export interface PingResult {
|
||||
packetsTransmitted: number;
|
||||
packetsReceived: number;
|
||||
packetLoss: number;
|
||||
min: number;
|
||||
avg: number;
|
||||
max: number;
|
||||
mdev: number;
|
||||
}
|
||||
|
||||
export function ping(
|
||||
host: string,
|
||||
callback: (err: Error, res?: PingResult, stdout?: string) => void
|
||||
): void {
|
||||
let cmd: string, parseRegExp1: RegExp, parseRegExp2: RegExp;
|
||||
host = host.replace("[", "").replace("]", "");
|
||||
switch (platform()) {
|
||||
case "linux":
|
||||
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
|
||||
parseRegExp1 =
|
||||
/(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
|
||||
parseRegExp2 =
|
||||
/(\d+) packets transmitted, (\d+) received, ([\d.]+)% packet loss/;
|
||||
break;
|
||||
|
||||
case "freebsd":
|
||||
// Send a single packet because on FreeBSD only superuser can send
|
||||
// packets that are only 200 ms apart.
|
||||
cmd = `ping -t 1 -c 3 ${host}`;
|
||||
parseRegExp1 =
|
||||
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
|
||||
parseRegExp2 =
|
||||
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
|
||||
break;
|
||||
|
||||
default:
|
||||
return callback(new Error("Platform not supported"));
|
||||
}
|
||||
|
||||
exec(cmd, (err, stdout) => {
|
||||
let parsed: PingResult;
|
||||
if (stdout) {
|
||||
const m1 = stdout.match(parseRegExp1);
|
||||
if (m1) {
|
||||
parsed = {
|
||||
packetsTransmitted: +m1[1],
|
||||
packetsReceived: +m1[2],
|
||||
packetLoss: +m1[3],
|
||||
min: +m1[4],
|
||||
avg: +m1[5],
|
||||
max: +m1[6],
|
||||
mdev: +m1[7],
|
||||
};
|
||||
} else {
|
||||
const m2 = stdout.match(parseRegExp2);
|
||||
if (m2) {
|
||||
parsed = {
|
||||
packetsTransmitted: +m2[1],
|
||||
packetsReceived: +m2[2],
|
||||
packetLoss: +m2[3],
|
||||
min: null,
|
||||
avg: null,
|
||||
max: null,
|
||||
mdev: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback(err, parsed, stdout);
|
||||
});
|
||||
}
|
||||
+18
-98
@@ -17,7 +17,9 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as common from "./common";
|
||||
function isObject(obj: any): boolean {
|
||||
return Object.prototype.toString.call(obj) === "[object Object]";
|
||||
}
|
||||
|
||||
function stringToRegexp(input, flags?): RegExp | false {
|
||||
if (input.indexOf("*") === -1) return false;
|
||||
@@ -34,8 +36,8 @@ function stringToRegexp(input, flags?): RegExp | false {
|
||||
}
|
||||
|
||||
function normalize(input): any {
|
||||
if (common.typeOf(input) === common.STRING_TYPE) {
|
||||
const vals = [input];
|
||||
if (typeof input === "string") {
|
||||
const vals: any = [input];
|
||||
const m = /^\/(.*?)\/(g?i?m?y?)$/.exec(input);
|
||||
if (m) vals.push({ $regex: new RegExp(m[1], m[2]) });
|
||||
|
||||
@@ -52,14 +54,14 @@ function normalize(input): any {
|
||||
return input;
|
||||
}
|
||||
|
||||
export function expandValue(value): any[] {
|
||||
if (common.typeOf(value) === common.ARRAY_TYPE) {
|
||||
function expandValue(value: unknown): unknown[] {
|
||||
if (Array.isArray(value)) {
|
||||
let a = [];
|
||||
for (const j of value) a = a.concat(expandValue(j));
|
||||
return [a];
|
||||
} else if (common.typeOf(value) !== common.OBJECT_TYPE) {
|
||||
} else if (!isObject(value)) {
|
||||
const n = normalize(value);
|
||||
if (common.typeOf(n) !== common.ARRAY_TYPE) return [n];
|
||||
if (!Array.isArray(n)) return [n];
|
||||
else return n;
|
||||
}
|
||||
|
||||
@@ -103,12 +105,14 @@ function permute(param, val): any[] {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
export function expand(query): {} {
|
||||
export function expand(
|
||||
query: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const newQuery = {};
|
||||
for (const [k, v] of Object.entries(query)) {
|
||||
if (k[0] === "$") {
|
||||
// Operator
|
||||
newQuery[k] = (v as any[]).map(e => expand(e));
|
||||
newQuery[k] = (v as any[]).map((e) => expand(e));
|
||||
} else {
|
||||
const conditions = permute(k, v);
|
||||
if (conditions.length > 1) {
|
||||
@@ -129,100 +133,16 @@ export function expand(query): {} {
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
function testExpressions(params, expressions, lop): boolean {
|
||||
for (const f of expressions) {
|
||||
const res = test(params, f);
|
||||
switch (lop) {
|
||||
case "$and":
|
||||
if (!res) return false;
|
||||
break;
|
||||
case "$or":
|
||||
if (res) return true;
|
||||
break;
|
||||
case "$nor":
|
||||
if (res) return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (lop) {
|
||||
case "$and":
|
||||
return true;
|
||||
case "$or":
|
||||
return false;
|
||||
case "$nor":
|
||||
return true;
|
||||
default:
|
||||
throw new Error("Unknown logical operator");
|
||||
}
|
||||
}
|
||||
|
||||
export function test(params, query): boolean {
|
||||
let res;
|
||||
for (const [k, v] of Object.entries(query)) {
|
||||
if (k.charAt(0) === "$") {
|
||||
// Logical operator
|
||||
res = testExpressions(params, v, k);
|
||||
} else {
|
||||
const value = params[k];
|
||||
if (common.typeOf(v) !== common.OBJECT_TYPE) {
|
||||
if (common.typeOf(value) === common.ARRAY_TYPE) {
|
||||
// TODO comparing array to regex, array to array, and object to object
|
||||
res = value.indexOf(v) !== -1;
|
||||
} else {
|
||||
if (common.typeOf(v) === common.REGEXP_TYPE)
|
||||
res = (v as RegExp).test(value);
|
||||
else res = v === value;
|
||||
}
|
||||
} else {
|
||||
for (const [k2, v2] of Object.entries(v)) {
|
||||
switch (k2) {
|
||||
case "$ne":
|
||||
if (common.typeOf(value) === common.ARRAY_TYPE)
|
||||
res = value.indexOf(v2) === -1;
|
||||
else res = value !== v2;
|
||||
break;
|
||||
case "$lt":
|
||||
res = value < v2;
|
||||
break;
|
||||
case "$lte":
|
||||
res = value <= v2;
|
||||
break;
|
||||
case "$gt":
|
||||
res = value > v2;
|
||||
break;
|
||||
case "$gte":
|
||||
res = value >= v2;
|
||||
break;
|
||||
case "$regex":
|
||||
res = v2.test(value);
|
||||
break;
|
||||
case "$in":
|
||||
throw new Error("Operator not supported");
|
||||
case "$nin":
|
||||
throw new Error("Operator not supported");
|
||||
case "$all":
|
||||
throw new Error("Operator not supported");
|
||||
case "$exists":
|
||||
throw new Error("Operator not supported");
|
||||
default:
|
||||
throw new Error("Operator not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!res) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function sanitizeQueryTypes(query, types): {} {
|
||||
export function sanitizeQueryTypes(
|
||||
query: Record<string, unknown>,
|
||||
types: Record<string, (v: unknown) => unknown>
|
||||
): Record<string, unknown> {
|
||||
for (const [k, v] of Object.entries(query)) {
|
||||
if (k[0] === "$") {
|
||||
// Logical operator
|
||||
for (const vv of v as any[]) sanitizeQueryTypes(vv, types);
|
||||
} else if (k in types) {
|
||||
if (common.typeOf(v) === common.OBJECT_TYPE) {
|
||||
if (isObject(v)) {
|
||||
for (const [kk, vv] of Object.entries(v)) {
|
||||
switch (kk) {
|
||||
case "$in":
|
||||
|
||||
+30
-20
@@ -109,6 +109,16 @@ class SandboxDate {
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
public static parse(dateString: string): number {
|
||||
return Date.parse(dateString);
|
||||
}
|
||||
|
||||
public static UTC(
|
||||
...args: [number, number?, number?, number?, number?, number?, number?]
|
||||
): number {
|
||||
return Date.UTC(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function random(): number {
|
||||
@@ -117,7 +127,7 @@ function random(): number {
|
||||
return state.rng();
|
||||
}
|
||||
|
||||
random.seed = function(s) {
|
||||
random.seed = function (s) {
|
||||
state.rng = seedrandom(s);
|
||||
};
|
||||
|
||||
@@ -125,7 +135,7 @@ class ParameterWrapper {
|
||||
public constructor(path: Path, attributes, unpacked?, unpackedRevision?) {
|
||||
for (const attrName of attributes) {
|
||||
Object.defineProperty(this, attrName, {
|
||||
get: function() {
|
||||
get: function () {
|
||||
if (state.uncommitted) commit();
|
||||
|
||||
if (state.revision !== unpackedRevision) {
|
||||
@@ -147,12 +157,12 @@ class ParameterWrapper {
|
||||
if (!attr) return UNDEFINED;
|
||||
|
||||
return attr[1];
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(this, "path", {
|
||||
get: function() {
|
||||
get: function () {
|
||||
if (state.uncommitted) commit();
|
||||
|
||||
if (state.revision !== unpackedRevision) {
|
||||
@@ -167,11 +177,11 @@ class ParameterWrapper {
|
||||
if (!unpacked.length) return UNDEFINED;
|
||||
|
||||
return unpacked[0].toString();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "size", {
|
||||
get: function() {
|
||||
get: function () {
|
||||
if (state.uncommitted) commit();
|
||||
|
||||
if (state.revision !== unpackedRevision) {
|
||||
@@ -186,10 +196,10 @@ class ParameterWrapper {
|
||||
if (!unpacked.length) return UNDEFINED;
|
||||
|
||||
return unpacked.length;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this[Symbol.iterator] = function*() {
|
||||
this[Symbol.iterator] = function* () {
|
||||
if (state.uncommitted) commit();
|
||||
|
||||
if (state.revision !== unpackedRevision) {
|
||||
@@ -225,7 +235,7 @@ function declare(
|
||||
pathSet: null,
|
||||
attrGet: null,
|
||||
attrSet: null,
|
||||
defer: true
|
||||
defer: true,
|
||||
};
|
||||
|
||||
const attrs = new Set();
|
||||
@@ -280,9 +290,9 @@ function commit(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function ext(): any {
|
||||
function ext(...args: unknown[]): any {
|
||||
++state.extCounter;
|
||||
const extCall = Array.from(arguments).map(String);
|
||||
const extCall = args.map(String);
|
||||
const key = `${state.revision}: ${JSON.stringify(extCall)}`;
|
||||
|
||||
if (key in state.sessionContext.extensionsCache)
|
||||
@@ -292,11 +302,11 @@ function ext(): any {
|
||||
throw EXT;
|
||||
}
|
||||
|
||||
function log(msg: string, meta: {}): void {
|
||||
function log(msg: string, meta: Record<string, unknown>): void {
|
||||
if (state.revision === state.maxRevision && state.extCounter >= 0) {
|
||||
const details = Object.assign({}, meta, {
|
||||
sessionContext: state.sessionContext,
|
||||
message: `Script: ${msg}`
|
||||
message: `Script: ${msg}`,
|
||||
});
|
||||
|
||||
delete details["hostname"];
|
||||
@@ -332,8 +342,8 @@ function errorToFault(err: Error): Fault {
|
||||
message: err.message,
|
||||
detail: {
|
||||
name: err.name,
|
||||
message: err.message
|
||||
}
|
||||
message: err.message,
|
||||
},
|
||||
};
|
||||
|
||||
if (err.stack) {
|
||||
@@ -355,7 +365,7 @@ function errorToFault(err: Error): Fault {
|
||||
|
||||
export async function run(
|
||||
script: vm.Script,
|
||||
globals: {},
|
||||
globals: Record<string, unknown>,
|
||||
sessionContext: SessionContext,
|
||||
startRevision: number,
|
||||
maxRevision: number,
|
||||
@@ -370,7 +380,7 @@ export async function run(
|
||||
extensions: {},
|
||||
clear: [],
|
||||
rng: null,
|
||||
extCounter: extCounter
|
||||
extCounter: extCounter,
|
||||
};
|
||||
|
||||
for (const n of Object.keys(context)) delete context[n];
|
||||
@@ -393,7 +403,7 @@ export async function run(
|
||||
clear: null,
|
||||
declare: null,
|
||||
done: false,
|
||||
returnValue: null
|
||||
returnValue: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -413,7 +423,7 @@ export async function run(
|
||||
clear: null,
|
||||
declare: null,
|
||||
done: false,
|
||||
returnValue: null
|
||||
returnValue: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -433,6 +443,6 @@ export async function run(
|
||||
clear: _state.clear,
|
||||
declare: _state.declarations,
|
||||
done: status === 0,
|
||||
returnValue: ret
|
||||
returnValue: ret,
|
||||
};
|
||||
}
|
||||
+15
-10
@@ -21,10 +21,7 @@ import * as crypto from "crypto";
|
||||
import * as later from "later";
|
||||
|
||||
function md532(str): number {
|
||||
const digest = crypto
|
||||
.createHash("md5")
|
||||
.update(str)
|
||||
.digest();
|
||||
const digest = crypto.createHash("md5").update(str).digest();
|
||||
return (
|
||||
digest.readUInt32LE(0) ^
|
||||
digest.readUInt32LE(4) ^
|
||||
@@ -33,29 +30,37 @@ function md532(str): number {
|
||||
);
|
||||
}
|
||||
|
||||
export function variance(deviceId, vrnc): number {
|
||||
export function variance(deviceId: string, vrnc: number): number {
|
||||
return (md532(deviceId) >>> 0) % vrnc;
|
||||
}
|
||||
|
||||
export function interval(timestamp, intrvl, offset = 0): number {
|
||||
export function interval(
|
||||
timestamp: number,
|
||||
intrvl: number,
|
||||
offset = 0
|
||||
): number {
|
||||
return Math.trunc((timestamp + offset) / intrvl) * intrvl - offset;
|
||||
}
|
||||
|
||||
export function parseCron(cronExp): any {
|
||||
export function parseCron(cronExp: string): any {
|
||||
const parts = cronExp.trim().split(/\s+/);
|
||||
if (parts.length === 5) parts.unshift("*");
|
||||
|
||||
return later.schedule(later.parse.cron(parts.join(" "), true));
|
||||
}
|
||||
|
||||
export function cron(timestamp, schedule, offset = 0): number[] {
|
||||
export function cron(
|
||||
timestamp: number,
|
||||
schedule: unknown,
|
||||
offset = 0
|
||||
): number[] {
|
||||
// TODO later.js doesn't throw erorr if expression is invalid!
|
||||
const ret = [0, 0];
|
||||
|
||||
const prev = schedule.prev(1, new Date(timestamp + offset));
|
||||
const prev = (schedule as any).prev(1, new Date(timestamp + offset));
|
||||
if (prev) ret[0] = prev.setMilliseconds(0) - offset;
|
||||
|
||||
const next = schedule.next(1, new Date(timestamp + offset + 1000));
|
||||
const next = (schedule as any).next(1, new Date(timestamp + offset + 1000));
|
||||
if (next) ret[1] = next.setMilliseconds(0) - offset;
|
||||
|
||||
return ret;
|
||||
|
||||
+28
-18
@@ -20,6 +20,7 @@
|
||||
import * as fs from "fs";
|
||||
import * as http from "http";
|
||||
import * as https from "https";
|
||||
import { Socket } from "net";
|
||||
import * as path from "path";
|
||||
import { ROOT_DIR } from "./config";
|
||||
|
||||
@@ -46,39 +47,48 @@ function closeServer(timeout, callback): void {
|
||||
|
||||
const cb = callback;
|
||||
callback = null;
|
||||
cb();
|
||||
// Allow some time for connection close events to fire
|
||||
setTimeout(cb, 50);
|
||||
});
|
||||
}
|
||||
|
||||
interface ServerOptions {
|
||||
port?: number;
|
||||
host?: string;
|
||||
ssl?: { key: string; cert: string };
|
||||
timeout?: number;
|
||||
keepAliveTimeout?: number;
|
||||
onConnection?: (socket: Socket) => void;
|
||||
}
|
||||
|
||||
export function start(
|
||||
port,
|
||||
networkInterface,
|
||||
ssl,
|
||||
_listener,
|
||||
onConnection?,
|
||||
keepAliveTimeout: number = 0
|
||||
options: ServerOptions,
|
||||
_listener: http.RequestListener
|
||||
): void {
|
||||
listener = _listener;
|
||||
|
||||
if (ssl && ssl.key && ssl.cert) {
|
||||
const options = {
|
||||
key: ssl.key
|
||||
if (options.ssl) {
|
||||
const opts = {
|
||||
key: options.ssl.key
|
||||
.split(":")
|
||||
.map(f => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
|
||||
cert: ssl.cert
|
||||
.map((f) => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
|
||||
cert: options.ssl.cert
|
||||
.split(":")
|
||||
.map(f => fs.readFileSync(path.resolve(ROOT_DIR, f.trim())))
|
||||
.map((f) => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
|
||||
};
|
||||
|
||||
server = https.createServer(options, listener);
|
||||
if (onConnection != null) server.on("secureConnection", onConnection);
|
||||
server = https.createServer(opts, listener);
|
||||
if (options.onConnection)
|
||||
server.on("secureConnection", options.onConnection);
|
||||
} else {
|
||||
server = http.createServer(listener);
|
||||
if (onConnection != null) server.on("connection", onConnection);
|
||||
if (options.onConnection) server.on("connection", options.onConnection);
|
||||
}
|
||||
|
||||
if (keepAliveTimeout) server.keepAliveTimeout = keepAliveTimeout;
|
||||
server.listen(port, networkInterface);
|
||||
server.timeout = options.timeout || 0;
|
||||
if (options.keepAliveTimeout != null)
|
||||
server.keepAliveTimeout = options.keepAliveTimeout;
|
||||
server.listen({ port: options.port, host: options.host });
|
||||
}
|
||||
|
||||
export function stop(): Promise<void> {
|
||||
|
||||
+852
-456
File diff suppressed because it is too large.
Load diff
@@ -1,745 +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 { parseXml, Document, Element, ParserOptions } from "libxmljs";
|
||||
import * as config from "./config";
|
||||
import { version as VERSION } from "../package.json";
|
||||
import {
|
||||
CpeGetResponse,
|
||||
CpeSetResponse,
|
||||
InformRequest,
|
||||
AcsResponse,
|
||||
CpeRequest,
|
||||
FaultStruct,
|
||||
SpvFault,
|
||||
CpeFault,
|
||||
SoapMessage,
|
||||
TransferCompleteRequest
|
||||
} from "./types";
|
||||
|
||||
const SERVER_NAME = `GenieACS/${VERSION}`;
|
||||
|
||||
const NAMESPACES = {
|
||||
"1.0": {
|
||||
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-0"
|
||||
},
|
||||
"1.1": {
|
||||
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-1"
|
||||
},
|
||||
"1.2": {
|
||||
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-2"
|
||||
},
|
||||
"1.3": {
|
||||
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-2"
|
||||
},
|
||||
"1.4": {
|
||||
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-3"
|
||||
}
|
||||
};
|
||||
|
||||
const LIBXMLJS_PARSE_OPTIONS: ParserOptions = {
|
||||
nocdata: true,
|
||||
recover: !!config.get("XML_RECOVER"),
|
||||
/* eslint-disable-next-line @typescript-eslint/camelcase */
|
||||
ignore_enc: !!config.get("XML_IGNORE_ENC")
|
||||
};
|
||||
|
||||
interface WriterOptions {
|
||||
declaration?: boolean;
|
||||
format?: boolean;
|
||||
selfCloseEmpty?: boolean;
|
||||
whitespace?: boolean;
|
||||
type?: "XML" | "HTML" | "XHTML";
|
||||
}
|
||||
|
||||
declare module "libxmljs" {
|
||||
interface Document {
|
||||
toString(formatting: boolean | WriterOptions): string;
|
||||
}
|
||||
}
|
||||
|
||||
const LIBXMLJS_SAVE_OPTIONS: WriterOptions = {
|
||||
format: !!config.get("XML_FORMAT"),
|
||||
declaration: !config.get("XML_NO_DECL"),
|
||||
selfCloseEmpty: !config.get("XML_NO_EMPTY")
|
||||
};
|
||||
|
||||
let warnings;
|
||||
|
||||
function parseBool(v): boolean {
|
||||
v = "" + v;
|
||||
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
|
||||
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
|
||||
return false;
|
||||
else return null;
|
||||
}
|
||||
|
||||
function event(xml): string[] {
|
||||
return xml
|
||||
.childNodes()
|
||||
.filter(n => n.name() === "EventStruct")
|
||||
.map(c =>
|
||||
c
|
||||
.childNodes()
|
||||
.find(n => n.name() === "EventCode")
|
||||
.text()
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
function parameterInfoList(xml): [string, boolean][] {
|
||||
return xml
|
||||
.childNodes()
|
||||
.filter(e => e.name() === "ParameterInfoStruct")
|
||||
.map(e => {
|
||||
let param, value;
|
||||
for (const c of e.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "Name":
|
||||
param = c.text();
|
||||
break;
|
||||
case "Writable":
|
||||
value = c.text();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let parsed = parseBool(value);
|
||||
|
||||
if (parsed === null) {
|
||||
warnings.push({
|
||||
message: "Invalid writable attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = false;
|
||||
}
|
||||
|
||||
return [param, parsed];
|
||||
});
|
||||
}
|
||||
|
||||
function parameterValueList(
|
||||
xml
|
||||
): [string, string | number | boolean, string][] {
|
||||
return xml
|
||||
.childNodes()
|
||||
.filter(e => e.name() === "ParameterValueStruct")
|
||||
.map(e => {
|
||||
let valueElement, param;
|
||||
for (const c of e.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "Name":
|
||||
param = c.text();
|
||||
break;
|
||||
case "Value":
|
||||
valueElement = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const valueType = valueElement.attr("type").value();
|
||||
const value = valueElement.text();
|
||||
let parsed = value;
|
||||
if (valueType === "xsd:boolean") {
|
||||
parsed = parseBool(value);
|
||||
if (parsed === null) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
} else if (valueType === "xsd:int" || valueType === "xsd:unsignedInt") {
|
||||
parsed = parseInt(value);
|
||||
if (isNaN(parsed)) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
} else if (valueType === "xsd:dateTime") {
|
||||
parsed = Date.parse(value);
|
||||
if (isNaN(parsed)) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
}
|
||||
|
||||
return [param, parsed, valueType];
|
||||
});
|
||||
}
|
||||
|
||||
function GetParameterNames(xml, methodRequest): void {
|
||||
const el = xml.node("cwmp:GetParameterNames");
|
||||
el.node("ParameterPath").text(methodRequest.parameterPath);
|
||||
el.node("NextLevel").text(+methodRequest.nextLevel);
|
||||
}
|
||||
|
||||
function GetParameterNamesResponse(xml): CpeGetResponse {
|
||||
return {
|
||||
name: "GetParameterNamesResponse",
|
||||
parameterList: parameterInfoList(
|
||||
xml.childNodes().find(n => n.name() === "ParameterList")
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function GetParameterValues(xml, methodRequest): void {
|
||||
const el = xml.node("cwmp:GetParameterValues").node("ParameterNames");
|
||||
el.attr({
|
||||
"soap-enc:arrayType": `xsd:string[${methodRequest.parameterNames.length}]`
|
||||
});
|
||||
|
||||
for (const p of methodRequest.parameterNames) el.node("string").text(p);
|
||||
}
|
||||
|
||||
function GetParameterValuesResponse(xml): CpeGetResponse {
|
||||
return {
|
||||
name: "GetParameterValuesResponse",
|
||||
parameterList: parameterValueList(
|
||||
xml.childNodes().find(n => n.name() === "ParameterList")
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function SetParameterValues(xml, methodRequest): void {
|
||||
const el = xml.node("cwmp:SetParameterValues");
|
||||
const paramList = el.node("ParameterList");
|
||||
paramList.attr({
|
||||
"soap-enc:arrayType": `cwmp:ParameterValueStruct[${methodRequest.parameterList.length}]`
|
||||
});
|
||||
|
||||
for (const p of methodRequest.parameterList) {
|
||||
const pvs = paramList.node("ParameterValueStruct");
|
||||
pvs.node("Name").text(p[0]);
|
||||
let val = p[1];
|
||||
if (p[2] === "xsd:dateTime" && typeof val === "number") {
|
||||
val = new Date(val).toISOString();
|
||||
if (methodRequest.DATETIME_MILLISECONDS === false)
|
||||
val = val.replace(".000", "");
|
||||
}
|
||||
if (p[2] === "xsd:boolean" && typeof val === "boolean")
|
||||
if (methodRequest.BOOLEAN_LITERAL === false) val = +val;
|
||||
|
||||
pvs
|
||||
.node("Value")
|
||||
.attr({ "xsi:type": p[2] })
|
||||
.text(val);
|
||||
}
|
||||
|
||||
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
||||
}
|
||||
|
||||
function SetParameterValuesResponse(xml): CpeSetResponse {
|
||||
return {
|
||||
name: "SetParameterValuesResponse",
|
||||
status: parseInt(
|
||||
xml
|
||||
.childNodes()
|
||||
.find(n => n.name() === "Status")
|
||||
.text()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function AddObject(xml, methodRequest): void {
|
||||
const el = xml.node("cwmp:AddObject");
|
||||
el.node("ObjectName").text(methodRequest.objectName);
|
||||
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
||||
}
|
||||
|
||||
function AddObjectResponse(xml): CpeSetResponse {
|
||||
let instanceNumber, status;
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "InstanceNumber":
|
||||
instanceNumber = parseInt(c.text());
|
||||
break;
|
||||
case "Status":
|
||||
status = parseInt(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "AddObjectResponse",
|
||||
instanceNumber: instanceNumber,
|
||||
status: status
|
||||
};
|
||||
}
|
||||
|
||||
function DeleteObject(xml, methodRequest): void {
|
||||
const el = xml.node("cwmp:DeleteObject");
|
||||
el.node("ObjectName").text(methodRequest.objectName);
|
||||
el.node("ParameterKey").text(methodRequest.parameterKey || "");
|
||||
}
|
||||
|
||||
function DeleteObjectResponse(xml): CpeSetResponse {
|
||||
return {
|
||||
name: "DeleteObjectResponse",
|
||||
status: parseInt(
|
||||
xml
|
||||
.childNodes()
|
||||
.find(n => n.name() === "Status")
|
||||
.text()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function Reboot(xml, methodRequest): void {
|
||||
const el = xml.node("cwmp:Reboot");
|
||||
el.node("CommandKey").text(methodRequest.commandKey || "");
|
||||
}
|
||||
|
||||
function RebootResponse(): CpeSetResponse {
|
||||
return {
|
||||
name: "RebootResponse"
|
||||
};
|
||||
}
|
||||
|
||||
function FactoryReset(xml): void {
|
||||
xml.node("cwmp:FactoryReset");
|
||||
}
|
||||
|
||||
function FactoryResetResponse(): CpeSetResponse {
|
||||
return {
|
||||
name: "FactoryResetResponse"
|
||||
};
|
||||
}
|
||||
|
||||
function Download(xml, methodRequest): void {
|
||||
const el = xml.node("cwmp:Download");
|
||||
el.node("CommandKey").text(methodRequest.commandKey || "");
|
||||
el.node("FileType").text(methodRequest.fileType);
|
||||
el.node("URL").text(methodRequest.url);
|
||||
el.node("Username").text(methodRequest.username || "");
|
||||
el.node("Password").text(methodRequest.password || "");
|
||||
el.node("FileSize").text(methodRequest.fileSize || "0");
|
||||
el.node("TargetFileName").text(methodRequest.targetFileName || "");
|
||||
el.node("DelaySeconds").text(methodRequest.delaySeconds || "0");
|
||||
el.node("SuccessURL").text(methodRequest.successUrl || "");
|
||||
el.node("FailureURL").text(methodRequest.failureUrl || "");
|
||||
}
|
||||
|
||||
function DownloadResponse(xml): CpeSetResponse {
|
||||
let status, startTime, completeTime;
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "Status":
|
||||
status = parseInt(c.text());
|
||||
break;
|
||||
case "StartTime":
|
||||
startTime = Date.parse(c.text());
|
||||
break;
|
||||
case "CompleteTime":
|
||||
completeTime = Date.parse(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "DownloadResponse",
|
||||
status: status,
|
||||
startTime: startTime,
|
||||
completeTime: completeTime
|
||||
};
|
||||
}
|
||||
|
||||
function Inform(xml): InformRequest {
|
||||
let retryCount, evnt, parameterList;
|
||||
const deviceId = {
|
||||
Manufacturer: null,
|
||||
OUI: null,
|
||||
ProductClass: null,
|
||||
SerialNumber: null
|
||||
};
|
||||
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "ParameterList":
|
||||
parameterList = parameterValueList(c);
|
||||
break;
|
||||
case "DeviceId":
|
||||
for (const cc of c.childNodes()) {
|
||||
const n = cc.name();
|
||||
if (n in deviceId) deviceId[n] = cc.text();
|
||||
}
|
||||
break;
|
||||
case "Event":
|
||||
evnt = event(c);
|
||||
break;
|
||||
case "RetryCount":
|
||||
retryCount = parseInt(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Inform",
|
||||
parameterList: parameterList,
|
||||
deviceId: deviceId,
|
||||
event: evnt,
|
||||
retryCount: retryCount
|
||||
};
|
||||
}
|
||||
|
||||
function InformResponse(xml): void {
|
||||
xml
|
||||
.node("cwmp:InformResponse")
|
||||
.node("MaxEnvelopes")
|
||||
.text(1);
|
||||
}
|
||||
|
||||
function GetRPCMethods(): AcsResponse {
|
||||
return { name: "GetRPCMethods" };
|
||||
}
|
||||
|
||||
function GetRPCMethodsResponse(xml, methodResponse): void {
|
||||
const el = xml.node("cwmp:GetRPCMethodsResponse").node("MethodList");
|
||||
el.attr({
|
||||
"soap-enc:arrayType": `xsd:string[${methodResponse.methodList.length}]`
|
||||
});
|
||||
|
||||
for (const m of methodResponse.methodList) el.node("string").text(m);
|
||||
}
|
||||
|
||||
function TransferComplete(xml): TransferCompleteRequest {
|
||||
let commandKey, _faultStruct, startTime, completeTime;
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "CommandKey":
|
||||
commandKey = c.text();
|
||||
break;
|
||||
case "FaultStruct":
|
||||
_faultStruct = faultStruct(c);
|
||||
break;
|
||||
case "StartTime":
|
||||
startTime = Date.parse(c.text());
|
||||
break;
|
||||
case "CompleteTime":
|
||||
completeTime = Date.parse(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: "TransferComplete",
|
||||
commandKey: commandKey,
|
||||
faultStruct: _faultStruct,
|
||||
startTime: startTime,
|
||||
completeTime: completeTime
|
||||
};
|
||||
}
|
||||
|
||||
function TransferCompleteResponse(xml): void {
|
||||
xml.node("cwmp:TransferCompleteResponse").text("");
|
||||
}
|
||||
|
||||
function RequestDownload(xml): CpeRequest {
|
||||
return {
|
||||
name: "RequestDownload",
|
||||
fileType: xml
|
||||
.childNodes()
|
||||
.find(n => n.name() === "FileType")
|
||||
.text()
|
||||
};
|
||||
}
|
||||
|
||||
function RequestDownloadResponse(xml): void {
|
||||
xml.node("cwmp:RequestDownloadResponse").text("");
|
||||
}
|
||||
|
||||
function faultStruct(xml): FaultStruct {
|
||||
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "FaultCode":
|
||||
faultCode = c.text();
|
||||
break;
|
||||
case "FaultString":
|
||||
faultString = c.text();
|
||||
break;
|
||||
case "SetParameterValuesFault":
|
||||
setParameterValuesFault = setParameterValuesFault || [];
|
||||
for (const cc of c.childNodes()) {
|
||||
switch (cc.name()) {
|
||||
case "ParameterName":
|
||||
pn = cc.text();
|
||||
break;
|
||||
case "FaultCode":
|
||||
fc = cc.text();
|
||||
break;
|
||||
case "FaultString":
|
||||
fs = cc.text();
|
||||
break;
|
||||
}
|
||||
}
|
||||
setParameterValuesFault.push({
|
||||
parameterName: pn,
|
||||
faultCode: fc,
|
||||
faultString: fs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { faultCode, faultString, setParameterValuesFault };
|
||||
}
|
||||
|
||||
function fault(xml): CpeFault {
|
||||
let faultCode, faultString, detail;
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "faultcode":
|
||||
faultCode = c.text();
|
||||
break;
|
||||
case "faultstring":
|
||||
faultString = c.text();
|
||||
break;
|
||||
case "detail":
|
||||
detail = faultStruct(c.childNodes().find(n => n.name() === "Fault"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { faultCode, faultString, detail };
|
||||
}
|
||||
|
||||
export function request(data, cwmpVersion, warn): SoapMessage {
|
||||
warnings = warn;
|
||||
|
||||
const rpc = {
|
||||
id: null,
|
||||
cwmpVersion: cwmpVersion,
|
||||
sessionTimeout: null,
|
||||
cpeRequest: null,
|
||||
cpeFault: null,
|
||||
cpeResponse: null
|
||||
};
|
||||
|
||||
if (!data.length) return rpc;
|
||||
|
||||
const xml = parseXml(data, LIBXMLJS_PARSE_OPTIONS);
|
||||
|
||||
let headerElement, bodyElement;
|
||||
|
||||
for (const c of xml.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "Header":
|
||||
headerElement = c;
|
||||
break;
|
||||
case "Body":
|
||||
bodyElement = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (headerElement) {
|
||||
for (const c of headerElement.childNodes()) {
|
||||
switch (c.name()) {
|
||||
case "ID":
|
||||
rpc.id = c.text();
|
||||
break;
|
||||
case "sessionTimeout":
|
||||
rpc.sessionTimeout = parseInt(c.text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const methodElement of bodyElement.childNodes()) {
|
||||
const methodName = methodElement.name();
|
||||
if (methodName === "text") continue;
|
||||
|
||||
if (!rpc.cwmpVersion && methodName !== "Fault") {
|
||||
switch (methodElement.namespace().href()) {
|
||||
case "urn:dslforum-org:cwmp-1-0":
|
||||
rpc.cwmpVersion = "1.0";
|
||||
break;
|
||||
case "urn:dslforum-org:cwmp-1-1":
|
||||
rpc.cwmpVersion = "1.1";
|
||||
break;
|
||||
case "urn:dslforum-org:cwmp-1-2":
|
||||
if (rpc.sessionTimeout) rpc.cwmpVersion = "1.3";
|
||||
else rpc.cwmpVersion = "1.2";
|
||||
|
||||
break;
|
||||
case "urn:dslforum-org:cwmp-1-3":
|
||||
rpc.cwmpVersion = "1.4";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unrecognized CWMP version");
|
||||
}
|
||||
}
|
||||
|
||||
switch (methodName) {
|
||||
case "Inform":
|
||||
rpc.cpeRequest = Inform(methodElement);
|
||||
break;
|
||||
case "GetRPCMethods":
|
||||
rpc.cpeRequest = GetRPCMethods();
|
||||
break;
|
||||
case "TransferComplete":
|
||||
rpc.cpeRequest = TransferComplete(methodElement);
|
||||
break;
|
||||
case "RequestDownload":
|
||||
rpc.cpeRequest = RequestDownload(methodElement);
|
||||
break;
|
||||
case "GetParameterNamesResponse":
|
||||
rpc.cpeResponse = GetParameterNamesResponse(methodElement);
|
||||
break;
|
||||
case "GetParameterValuesResponse":
|
||||
rpc.cpeResponse = GetParameterValuesResponse(methodElement);
|
||||
break;
|
||||
case "SetParameterValuesResponse":
|
||||
rpc.cpeResponse = SetParameterValuesResponse(methodElement);
|
||||
break;
|
||||
case "AddObjectResponse":
|
||||
rpc.cpeResponse = AddObjectResponse(methodElement);
|
||||
break;
|
||||
case "DeleteObjectResponse":
|
||||
rpc.cpeResponse = DeleteObjectResponse(methodElement);
|
||||
break;
|
||||
case "RebootResponse":
|
||||
rpc.cpeResponse = RebootResponse();
|
||||
break;
|
||||
case "FactoryResetResponse":
|
||||
rpc.cpeResponse = FactoryResetResponse();
|
||||
break;
|
||||
case "DownloadResponse":
|
||||
rpc.cpeResponse = DownloadResponse(methodElement);
|
||||
break;
|
||||
case "Fault":
|
||||
rpc.cpeFault = fault(methodElement);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`8000 Method not supported ${methodName}`);
|
||||
}
|
||||
}
|
||||
|
||||
return rpc;
|
||||
}
|
||||
|
||||
function createSoapEnv(cwmpVersion: string): Element {
|
||||
const xml = new Document();
|
||||
const env = xml.node("soap-env:Envelope");
|
||||
|
||||
for (const prefix of Object.keys(NAMESPACES[cwmpVersion]))
|
||||
env.defineNamespace(prefix, NAMESPACES[cwmpVersion][prefix]);
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
export function response(rpc): { code: number; headers: {}; data: string } {
|
||||
const headers = {
|
||||
Server: SERVER_NAME,
|
||||
SOAPServer: SERVER_NAME
|
||||
};
|
||||
|
||||
if (!rpc) return { code: 204, headers: headers, data: "" };
|
||||
|
||||
const env = createSoapEnv(rpc.cwmpVersion);
|
||||
const header = env.node("soap-env:Header");
|
||||
header
|
||||
.node("cwmp:ID")
|
||||
.attr({
|
||||
"soap-env:mustUnderstand": "1"
|
||||
})
|
||||
.text(rpc.id);
|
||||
|
||||
const body = env.node("soap-env:Body");
|
||||
|
||||
if (rpc.acsResponse) {
|
||||
switch (rpc.acsResponse.name) {
|
||||
case "InformResponse":
|
||||
InformResponse(body);
|
||||
break;
|
||||
case "GetRPCMethodsResponse":
|
||||
GetRPCMethodsResponse(body, rpc.acsResponse);
|
||||
break;
|
||||
case "TransferCompleteResponse":
|
||||
TransferCompleteResponse(body);
|
||||
break;
|
||||
case "RequestDownloadResponse":
|
||||
RequestDownloadResponse(body);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
|
||||
}
|
||||
} else if (rpc.acsRequest) {
|
||||
switch (rpc.acsRequest.name) {
|
||||
case "GetParameterNames":
|
||||
GetParameterNames(body, rpc.acsRequest);
|
||||
break;
|
||||
case "GetParameterValues":
|
||||
GetParameterValues(body, rpc.acsRequest);
|
||||
break;
|
||||
case "SetParameterValues":
|
||||
SetParameterValues(body, rpc.acsRequest);
|
||||
break;
|
||||
case "AddObject":
|
||||
AddObject(body, rpc.acsRequest);
|
||||
break;
|
||||
case "DeleteObject":
|
||||
DeleteObject(body, rpc.acsRequest);
|
||||
break;
|
||||
case "Reboot":
|
||||
Reboot(body, rpc.acsRequest);
|
||||
break;
|
||||
case "FactoryReset":
|
||||
FactoryReset(body);
|
||||
break;
|
||||
case "Download":
|
||||
Download(body, rpc.acsRequest);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown method request ${rpc.acsRequest.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
headers["Content-Type"] = 'text/xml; charset="utf-8"';
|
||||
return {
|
||||
code: 200,
|
||||
headers: headers,
|
||||
data: env.doc().toString(LIBXMLJS_SAVE_OPTIONS)
|
||||
};
|
||||
}
|
||||
+456
-112
@@ -22,22 +22,34 @@ import {
|
||||
Element,
|
||||
parseAttrs,
|
||||
encodeEntities,
|
||||
decodeEntities
|
||||
decodeEntities,
|
||||
} from "./xml-parser";
|
||||
import memoize from "./common/memoize";
|
||||
import { version as VERSION } from "../package.json";
|
||||
import {
|
||||
CpeGetResponse,
|
||||
CpeSetResponse,
|
||||
InformRequest,
|
||||
AcsResponse,
|
||||
CpeRequest,
|
||||
FaultStruct,
|
||||
SpvFault,
|
||||
CpeFault,
|
||||
SoapMessage,
|
||||
TransferCompleteRequest
|
||||
TransferCompleteRequest,
|
||||
AcsRequest,
|
||||
GetParameterNamesResponse,
|
||||
GetParameterValuesResponse,
|
||||
GetParameterAttributesResponse,
|
||||
SetParameterValuesResponse,
|
||||
SetParameterAttributesResponse,
|
||||
AddObjectResponse,
|
||||
DeleteObject,
|
||||
DeleteObjectResponse,
|
||||
RebootResponse,
|
||||
FactoryResetResponse,
|
||||
DownloadResponse,
|
||||
GetRPCMethodsRequest,
|
||||
RequestDownloadRequest,
|
||||
AcsResponse,
|
||||
} from "./types";
|
||||
import Path from "./common/path";
|
||||
|
||||
const SERVER_NAME = `GenieACS/${VERSION}`;
|
||||
|
||||
@@ -47,60 +59,58 @@ const NAMESPACES = {
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-0"
|
||||
cwmp: "urn:dslforum-org:cwmp-1-0",
|
||||
},
|
||||
"1.1": {
|
||||
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-1"
|
||||
cwmp: "urn:dslforum-org:cwmp-1-1",
|
||||
},
|
||||
"1.2": {
|
||||
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-2"
|
||||
cwmp: "urn:dslforum-org:cwmp-1-2",
|
||||
},
|
||||
"1.3": {
|
||||
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-2"
|
||||
cwmp: "urn:dslforum-org:cwmp-1-2",
|
||||
},
|
||||
"1.4": {
|
||||
"soap-enc": "http://schemas.xmlsoap.org/soap/encoding/",
|
||||
"soap-env": "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
xsd: "http://www.w3.org/2001/XMLSchema",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cwmp: "urn:dslforum-org:cwmp-1-3"
|
||||
}
|
||||
cwmp: "urn:dslforum-org:cwmp-1-3",
|
||||
},
|
||||
};
|
||||
|
||||
let warnings;
|
||||
let warnings: Record<string, unknown>[];
|
||||
|
||||
const memoizedParseAttrs = memoize(parseAttrs);
|
||||
|
||||
function parseBool(v): boolean {
|
||||
v = "" + v;
|
||||
if (v === "true" || v === "TRUE" || v === "True" || v === "1") return true;
|
||||
else if (v === "false" || v === "FALSE" || v === "False" || v === "0")
|
||||
return false;
|
||||
else return null;
|
||||
function parseBool(v: string): boolean {
|
||||
if (v === "true" || v === "1") return true;
|
||||
if (v === "false" || v === "0") return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
function event(xml: Element): string[] {
|
||||
return xml.children
|
||||
.filter(n => n.localName === "EventStruct")
|
||||
.map(c => c.children.find(n => n.localName === "EventCode").text.trim());
|
||||
.filter((n) => n.localName === "EventStruct")
|
||||
.map((c) => c.children.find((n) => n.localName === "EventCode").text);
|
||||
}
|
||||
|
||||
function parameterInfoList(xml: Element): [string, boolean][] {
|
||||
function parameterInfoList(xml: Element): [Path, boolean, boolean][] {
|
||||
return xml.children
|
||||
.filter(e => e.localName === "ParameterInfoStruct")
|
||||
.map<[string, boolean]>(e => {
|
||||
.map<[Path, boolean, boolean]>((e) => {
|
||||
if (e.localName !== "ParameterInfoStruct") return null;
|
||||
let param: string, value: string;
|
||||
for (const c of e.children) {
|
||||
switch (c.localName) {
|
||||
@@ -115,30 +125,44 @@ function parameterInfoList(xml: Element): [string, boolean][] {
|
||||
|
||||
let parsed: boolean = parseBool(value);
|
||||
|
||||
if (parsed === null) {
|
||||
if (parsed == null) {
|
||||
warnings.push({
|
||||
message: "Invalid writable attribute",
|
||||
parameter: param
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Writable",
|
||||
parameter: param,
|
||||
});
|
||||
parsed = false;
|
||||
}
|
||||
|
||||
return [param, parsed];
|
||||
});
|
||||
try {
|
||||
if (param && !param.endsWith("."))
|
||||
return [Path.parse(param), false, parsed];
|
||||
else return [Path.parse(param.slice(0, -1)), true, parsed];
|
||||
} catch (err) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Name",
|
||||
parameter: param,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((e) => e != null);
|
||||
}
|
||||
|
||||
const getValueType = memoize((str: string) =>
|
||||
parseAttrs(str)
|
||||
.find(s => s.localName === "type")
|
||||
.value.trim()
|
||||
);
|
||||
const getValueType = memoize((str: string) => {
|
||||
const attrs = parseAttrs(str);
|
||||
for (const attr of attrs) if (attr.localName === "type") return attr.value;
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
function parameterValueList(
|
||||
xml: Element
|
||||
): [string, string | number | boolean, string][] {
|
||||
): [Path, string | number | boolean, string][] {
|
||||
return xml.children
|
||||
.filter(e => e.localName === "ParameterValueStruct")
|
||||
.map<[string, string | number | boolean, string]>(e => {
|
||||
.map<[Path, string | number | boolean, string]>((e) => {
|
||||
if (e.localName !== "ParameterValueStruct") return null;
|
||||
let valueElement: Element, param: string;
|
||||
for (const c of e.children) {
|
||||
switch (c.localName) {
|
||||
@@ -151,16 +175,25 @@ function parameterValueList(
|
||||
}
|
||||
}
|
||||
|
||||
const valueType = getValueType(valueElement.attrs);
|
||||
let valueType = getValueType(valueElement.attrs);
|
||||
if (!valueType) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
attribute: "type",
|
||||
parameter: param,
|
||||
});
|
||||
valueType = "xsd:string";
|
||||
}
|
||||
|
||||
const value = decodeEntities(valueElement.text);
|
||||
let parsed: string | number | boolean = value;
|
||||
if (valueType === "xsd:boolean") {
|
||||
parsed = parseBool(value);
|
||||
if (parsed === null) {
|
||||
if (parsed == null) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Value",
|
||||
parameter: param,
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
@@ -168,8 +201,9 @@ function parameterValueList(
|
||||
parsed = parseInt(value);
|
||||
if (isNaN(parsed)) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Value",
|
||||
parameter: param,
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
@@ -177,15 +211,74 @@ function parameterValueList(
|
||||
parsed = Date.parse(value);
|
||||
if (isNaN(parsed)) {
|
||||
warnings.push({
|
||||
message: "Invalid value attribute",
|
||||
parameter: param
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Value",
|
||||
parameter: param,
|
||||
});
|
||||
parsed = value;
|
||||
}
|
||||
}
|
||||
try {
|
||||
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);
|
||||
}
|
||||
|
||||
return [param, parsed, valueType];
|
||||
});
|
||||
function parameterAttributeList(xml: Element): [Path, number, string[]][] {
|
||||
return xml.children
|
||||
.map<[Path, number, string[]]>((e) => {
|
||||
if (e.localName !== "ParameterAttributeStruct") return null;
|
||||
let notificationElement: Element,
|
||||
accessListElement: Element,
|
||||
param: string;
|
||||
for (const c of e.children) {
|
||||
switch (c.localName) {
|
||||
case "Name":
|
||||
param = c.text;
|
||||
break;
|
||||
case "Notification":
|
||||
notificationElement = c;
|
||||
break;
|
||||
case "AccessList":
|
||||
accessListElement = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let notification = parseInt(notificationElement.text);
|
||||
if (isNaN(notification)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Notification",
|
||||
parameter: param,
|
||||
});
|
||||
notification = 0;
|
||||
}
|
||||
|
||||
const accessList = accessListElement.children
|
||||
.filter((c) => c.localName === "string")
|
||||
.map((c) => decodeEntities(c.text));
|
||||
|
||||
try {
|
||||
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 {
|
||||
@@ -194,12 +287,12 @@ function GetParameterNames(methodRequest): string {
|
||||
}</ParameterPath><NextLevel>${+methodRequest.nextLevel}</NextLevel></cwmp:GetParameterNames>`;
|
||||
}
|
||||
|
||||
function GetParameterNamesResponse(xml): CpeGetResponse {
|
||||
function GetParameterNamesResponse(xml): GetParameterNamesResponse {
|
||||
return {
|
||||
name: "GetParameterNamesResponse",
|
||||
parameterList: parameterInfoList(
|
||||
xml.children.find(n => n.localName === "ParameterList")
|
||||
)
|
||||
xml.children.find((n) => n.localName === "ParameterList")
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,21 +300,40 @@ function GetParameterValues(methodRequest): string {
|
||||
return `<cwmp:GetParameterValues><ParameterNames soap-enc:arrayType="xsd:string[${
|
||||
methodRequest.parameterNames.length
|
||||
}]">${methodRequest.parameterNames
|
||||
.map(p => `<string>${p}</string>`)
|
||||
.map((p) => `<string>${p}</string>`)
|
||||
.join("")}</ParameterNames></cwmp:GetParameterValues>`;
|
||||
}
|
||||
|
||||
function GetParameterValuesResponse(xml: Element): CpeGetResponse {
|
||||
function GetParameterValuesResponse(xml: Element): GetParameterValuesResponse {
|
||||
return {
|
||||
name: "GetParameterValuesResponse",
|
||||
parameterList: parameterValueList(
|
||||
xml.children.find(n => n.localName === "ParameterList")
|
||||
)
|
||||
xml.children.find((n) => n.localName === "ParameterList")
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function GetParameterAttributes(methodRequest): string {
|
||||
return `<cwmp:GetParameterAttributes><ParameterNames soap-enc:arrayType="xsd:string[${
|
||||
methodRequest.parameterNames.length
|
||||
}]">${methodRequest.parameterNames
|
||||
.map((p) => `<string>${p}</string>`)
|
||||
.join("")}</ParameterNames></cwmp:GetParameterAttributes>`;
|
||||
}
|
||||
|
||||
function GetParameterAttributesResponse(
|
||||
xml: Element
|
||||
): GetParameterAttributesResponse {
|
||||
return {
|
||||
name: "GetParameterAttributesResponse",
|
||||
parameterList: parameterAttributeList(
|
||||
xml.children.find((n) => n.localName === "ParameterList")
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function SetParameterValues(methodRequest): string {
|
||||
const params = methodRequest.parameterList.map(p => {
|
||||
const params = methodRequest.parameterList.map((p) => {
|
||||
let val = p[1];
|
||||
if (p[2] === "xsd:dateTime" && typeof val === "number") {
|
||||
val = new Date(val).toISOString();
|
||||
@@ -237,32 +349,80 @@ function SetParameterValues(methodRequest): string {
|
||||
|
||||
return `<cwmp:SetParameterValues><ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[${
|
||||
methodRequest.parameterList.length
|
||||
}]">${params.join(
|
||||
""
|
||||
)}</ParameterList><ParameterKey>${methodRequest.parameterKey ||
|
||||
""}</ParameterKey></cwmp:SetParameterValues>`;
|
||||
}]">${params.join("")}</ParameterList><ParameterKey>${
|
||||
methodRequest.parameterKey || ""
|
||||
}</ParameterKey></cwmp:SetParameterValues>`;
|
||||
}
|
||||
|
||||
function SetParameterValuesResponse(xml: Element): CpeSetResponse {
|
||||
function SetParameterValuesResponse(xml: Element): SetParameterValuesResponse {
|
||||
let status: number;
|
||||
|
||||
for (const c of xml.children) {
|
||||
switch (c.localName) {
|
||||
case "Status":
|
||||
status = parseInt(c.text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(status >= 0)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Status",
|
||||
});
|
||||
status = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
name: "SetParameterValuesResponse",
|
||||
status: parseInt(xml.children.find(n => n.localName === "Status").text)
|
||||
status: status,
|
||||
};
|
||||
}
|
||||
|
||||
function SetParameterAttributes(methodRequest): string {
|
||||
const params = methodRequest.parameterList.map((p) => {
|
||||
return `<SetParameterAttributesStruct><Name>${
|
||||
p[0]
|
||||
}</Name><NotificationChange>${
|
||||
p[1] == null ? "false" : "true"
|
||||
}</NotificationChange><Notification>${
|
||||
p[1] == null ? "" : p[1]
|
||||
}</Notification><AccessListChange>${
|
||||
p[2] == null ? "false" : "true"
|
||||
}</AccessListChange><AccessList soap-enc:arrayType="xsd:string[${
|
||||
(p[2] || []).length
|
||||
}]">${
|
||||
p[2] == null
|
||||
? ""
|
||||
: p[2].map((s) => `<string>${encodeEntities(s)}</string>`).join("")
|
||||
}</AccessList></SetParameterAttributesStruct>`;
|
||||
});
|
||||
|
||||
return `<cwmp:SetParameterAttributes><ParameterList soap-enc:arrayType="cwmp:SetParameterAttributesStruct[${
|
||||
methodRequest.parameterList.length
|
||||
}]">${params.join("")}</ParameterList></cwmp:SetParameterAttributes>`;
|
||||
}
|
||||
|
||||
function SetParameterAttributesResponse(): SetParameterAttributesResponse {
|
||||
return {
|
||||
name: "SetParameterAttributesResponse",
|
||||
};
|
||||
}
|
||||
|
||||
function AddObject(methodRequest): string {
|
||||
return `<cwmp:AddObject><ObjectName>${
|
||||
methodRequest.objectName
|
||||
}</ObjectName><ParameterKey>${methodRequest.parameterKey ||
|
||||
""}</ParameterKey></cwmp:AddObject>`;
|
||||
}</ObjectName><ParameterKey>${
|
||||
methodRequest.parameterKey || ""
|
||||
}</ParameterKey></cwmp:AddObject>`;
|
||||
}
|
||||
|
||||
function AddObjectResponse(xml: Element): CpeSetResponse {
|
||||
let instanceNumber, status;
|
||||
function AddObjectResponse(xml: Element): AddObjectResponse {
|
||||
let instanceNumber: string, status: number;
|
||||
for (const c of xml.children) {
|
||||
switch (c.localName) {
|
||||
case "InstanceNumber":
|
||||
instanceNumber = parseInt(c.text);
|
||||
instanceNumber = c.text;
|
||||
break;
|
||||
case "Status":
|
||||
status = parseInt(c.text);
|
||||
@@ -270,35 +430,66 @@ function AddObjectResponse(xml: Element): CpeSetResponse {
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^[0-9]+$/.test(instanceNumber))
|
||||
throw new Error("Missing or invalid instance number");
|
||||
|
||||
if (!(status >= 0)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Status",
|
||||
});
|
||||
status = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
name: "AddObjectResponse",
|
||||
instanceNumber: instanceNumber,
|
||||
status: status
|
||||
status: status,
|
||||
};
|
||||
}
|
||||
|
||||
function DeleteObject(methodRequest): string {
|
||||
return `<cwmp:DeleteObject><ObjectName>${
|
||||
methodRequest.objectName
|
||||
}</ObjectName><ParameterKey>${methodRequest.parameterKey ||
|
||||
""}</ParameterKey></cwmp:DeleteObject>`;
|
||||
}</ObjectName><ParameterKey>${
|
||||
methodRequest.parameterKey || ""
|
||||
}</ParameterKey></cwmp:DeleteObject>`;
|
||||
}
|
||||
|
||||
function DeleteObjectResponse(xml: Element): CpeSetResponse {
|
||||
function DeleteObjectResponse(xml: Element): DeleteObjectResponse {
|
||||
let status: number;
|
||||
|
||||
for (const c of xml.children) {
|
||||
switch (c.localName) {
|
||||
case "Status":
|
||||
status = parseInt(c.text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(status >= 0)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Status",
|
||||
});
|
||||
status = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
name: "DeleteObjectResponse",
|
||||
status: parseInt(xml.children.find(n => n.localName === "Status").text)
|
||||
status: status,
|
||||
};
|
||||
}
|
||||
|
||||
function Reboot(methodRequest): string {
|
||||
return `<cwmp:Reboot><CommandKey>${methodRequest.commandKey ||
|
||||
""}</CommandKey></cwmp:Reboot>`;
|
||||
return `<cwmp:Reboot><CommandKey>${
|
||||
methodRequest.commandKey || ""
|
||||
}</CommandKey></cwmp:Reboot>`;
|
||||
}
|
||||
|
||||
function RebootResponse(): CpeSetResponse {
|
||||
function RebootResponse(): RebootResponse {
|
||||
return {
|
||||
name: "RebootResponse"
|
||||
name: "RebootResponse",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -306,33 +497,36 @@ function FactoryReset(): string {
|
||||
return "<cwmp:FactoryReset></cwmp:FactoryReset>";
|
||||
}
|
||||
|
||||
function FactoryResetResponse(): CpeSetResponse {
|
||||
function FactoryResetResponse(): FactoryResetResponse {
|
||||
return {
|
||||
name: "FactoryResetResponse"
|
||||
name: "FactoryResetResponse",
|
||||
};
|
||||
}
|
||||
|
||||
function Download(methodRequest): string {
|
||||
return `<cwmp:Download><CommandKey>${methodRequest.commandKey ||
|
||||
""}</CommandKey><FileType>${methodRequest.fileType}</FileType><URL>${
|
||||
return `<cwmp:Download><CommandKey>${
|
||||
methodRequest.commandKey || ""
|
||||
}</CommandKey><FileType>${methodRequest.fileType}</FileType><URL>${
|
||||
methodRequest.url
|
||||
}</URL><Username>${encodeEntities(
|
||||
methodRequest.username || ""
|
||||
)}</Username><Password>${encodeEntities(
|
||||
methodRequest.password || ""
|
||||
)}</Password><FileSize>${methodRequest.fileSize ||
|
||||
"0"}</FileSize><TargetFileName>${encodeEntities(
|
||||
)}</Password><FileSize>${
|
||||
methodRequest.fileSize || "0"
|
||||
}</FileSize><TargetFileName>${encodeEntities(
|
||||
methodRequest.targetFileName || ""
|
||||
)}</TargetFileName><DelaySeconds>${methodRequest.delaySeconds ||
|
||||
"0"}</DelaySeconds><SuccessURL>${encodeEntities(
|
||||
)}</TargetFileName><DelaySeconds>${
|
||||
methodRequest.delaySeconds || "0"
|
||||
}</DelaySeconds><SuccessURL>${encodeEntities(
|
||||
methodRequest.successUrl || ""
|
||||
)}</SuccessURL><FailureURL>${encodeEntities(
|
||||
methodRequest.failureUrl || ""
|
||||
)}</FailureURL></cwmp:Download>`;
|
||||
}
|
||||
|
||||
function DownloadResponse(xml: Element): CpeSetResponse {
|
||||
let status, startTime, completeTime;
|
||||
function DownloadResponse(xml: Element): DownloadResponse {
|
||||
let status: number, startTime: number, completeTime: number;
|
||||
for (const c of xml.children) {
|
||||
switch (c.localName) {
|
||||
case "Status":
|
||||
@@ -347,21 +541,46 @@ function DownloadResponse(xml: Element): CpeSetResponse {
|
||||
}
|
||||
}
|
||||
|
||||
if (!(status >= 0)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "Status",
|
||||
});
|
||||
status = 0;
|
||||
}
|
||||
|
||||
if (startTime == null || isNaN(startTime)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "StartTime",
|
||||
});
|
||||
startTime = Date.parse("0001-01-01T00:00:00Z");
|
||||
}
|
||||
|
||||
if (completeTime == null || isNaN(completeTime)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "CompleteTime",
|
||||
});
|
||||
completeTime = Date.parse("0001-01-01T00:00:00Z");
|
||||
}
|
||||
|
||||
return {
|
||||
name: "DownloadResponse",
|
||||
status: status,
|
||||
startTime: startTime,
|
||||
completeTime: completeTime
|
||||
completeTime: completeTime,
|
||||
};
|
||||
}
|
||||
|
||||
function Inform(xml: Element): InformRequest {
|
||||
let retryCount, evnt, parameterList;
|
||||
let retryCount: number, evnt: string[];
|
||||
let parameterList: [Path, string | number | boolean, string][];
|
||||
const deviceId = {
|
||||
Manufacturer: null,
|
||||
OUI: null,
|
||||
ProductClass: null,
|
||||
SerialNumber: null
|
||||
SerialNumber: null,
|
||||
};
|
||||
|
||||
for (const c of xml.children) {
|
||||
@@ -384,12 +603,36 @@ function Inform(xml: Element): InformRequest {
|
||||
}
|
||||
}
|
||||
|
||||
if (!deviceId || !deviceId.SerialNumber || !deviceId.OUI)
|
||||
throw new Error("Missing or invalid DeviceId element");
|
||||
|
||||
if (!parameterList) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "ParameterList",
|
||||
});
|
||||
parameterList = [];
|
||||
}
|
||||
|
||||
if (!evnt) {
|
||||
warnings.push({ message: "Missing or invalid XML node", element: "Event" });
|
||||
evnt = [];
|
||||
}
|
||||
|
||||
if (retryCount == null || isNaN(retryCount)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "RetryCount",
|
||||
});
|
||||
retryCount = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
name: "Inform",
|
||||
parameterList: parameterList,
|
||||
deviceId: deviceId,
|
||||
event: evnt,
|
||||
retryCount: retryCount
|
||||
retryCount: retryCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -397,7 +640,7 @@ function InformResponse(): string {
|
||||
return "<cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse>";
|
||||
}
|
||||
|
||||
function GetRPCMethods(): AcsResponse {
|
||||
function GetRPCMethods(): GetRPCMethodsRequest {
|
||||
return { name: "GetRPCMethods" };
|
||||
}
|
||||
|
||||
@@ -405,12 +648,15 @@ function GetRPCMethodsResponse(methodResponse): string {
|
||||
return `<cwmp:GetRPCMethodsResponse><MethodList soap-enc:arrayType="xsd:string[${
|
||||
methodResponse.methodList.length
|
||||
}]">${methodResponse.methodList
|
||||
.map(m => `<string>${m}</string>`)
|
||||
.map((m) => `<string>${m}</string>`)
|
||||
.join("")}</MethodList></cwmp:GetRPCMethodsResponse>`;
|
||||
}
|
||||
|
||||
function TransferComplete(xml: Element): TransferCompleteRequest {
|
||||
let commandKey, _faultStruct, startTime, completeTime;
|
||||
let commandKey: string,
|
||||
_faultStruct: FaultStruct,
|
||||
startTime: number,
|
||||
completeTime: number;
|
||||
for (const c of xml.children) {
|
||||
switch (c.localName) {
|
||||
case "CommandKey":
|
||||
@@ -428,12 +674,44 @@ function TransferComplete(xml: Element): TransferCompleteRequest {
|
||||
}
|
||||
}
|
||||
|
||||
if (commandKey == null) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "CommandKey",
|
||||
});
|
||||
commandKey = "";
|
||||
}
|
||||
|
||||
if (!_faultStruct) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "FaultStruct",
|
||||
});
|
||||
_faultStruct = { faultCode: "0", faultString: "" };
|
||||
}
|
||||
|
||||
if (startTime == null || isNaN(startTime)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "StartTime",
|
||||
});
|
||||
startTime = Date.parse("0001-01-01T00:00:00Z");
|
||||
}
|
||||
|
||||
if (completeTime == null || isNaN(completeTime)) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "CompleteTime",
|
||||
});
|
||||
completeTime = Date.parse("0001-01-01T00:00:00Z");
|
||||
}
|
||||
|
||||
return {
|
||||
name: "TransferComplete",
|
||||
commandKey: commandKey,
|
||||
faultStruct: _faultStruct,
|
||||
startTime: startTime,
|
||||
completeTime: completeTime
|
||||
completeTime: completeTime,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -441,10 +719,10 @@ function TransferCompleteResponse(): string {
|
||||
return "<cwmp:TransferCompleteResponse></cwmp:TransferCompleteResponse>";
|
||||
}
|
||||
|
||||
function RequestDownload(xml: Element): CpeRequest {
|
||||
function RequestDownload(xml: Element): RequestDownloadRequest {
|
||||
return {
|
||||
name: "RequestDownload",
|
||||
fileType: xml.children.find(n => n.localName === "FileType").text
|
||||
fileType: xml.children.find((n) => n.localName === "FileType").text,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -453,7 +731,12 @@ function RequestDownloadResponse(): string {
|
||||
}
|
||||
|
||||
function faultStruct(xml: Element): FaultStruct {
|
||||
let faultCode, faultString, setParameterValuesFault: SpvFault[], pn, fc, fs;
|
||||
let faultCode: string,
|
||||
faultString: string,
|
||||
setParameterValuesFault: SpvFault[],
|
||||
pn: string,
|
||||
fc: string,
|
||||
fs: string;
|
||||
for (const c of xml.children) {
|
||||
switch (c.localName) {
|
||||
case "FaultCode":
|
||||
@@ -464,6 +747,7 @@ function faultStruct(xml: Element): FaultStruct {
|
||||
break;
|
||||
case "SetParameterValuesFault":
|
||||
setParameterValuesFault = setParameterValuesFault || [];
|
||||
pn = fc = fs = null;
|
||||
for (const cc of c.children) {
|
||||
switch (cc.localName) {
|
||||
case "ParameterName":
|
||||
@@ -480,16 +764,32 @@ function faultStruct(xml: Element): FaultStruct {
|
||||
setParameterValuesFault.push({
|
||||
parameterName: pn,
|
||||
faultCode: fc,
|
||||
faultString: fs
|
||||
faultString: fs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (faultCode == null) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "FaultCode",
|
||||
});
|
||||
faultCode = "";
|
||||
}
|
||||
|
||||
if (faultString == null) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "FaultString",
|
||||
});
|
||||
faultString = "";
|
||||
}
|
||||
|
||||
return { faultCode, faultString, setParameterValuesFault };
|
||||
}
|
||||
|
||||
function fault(xml: Element): CpeFault {
|
||||
let faultCode, faultString, detail;
|
||||
let faultCode: string, faultString: string, detail: FaultStruct;
|
||||
for (const c of xml.children) {
|
||||
switch (c.localName) {
|
||||
case "faultcode":
|
||||
@@ -499,24 +799,45 @@ function fault(xml: Element): CpeFault {
|
||||
faultString = decodeEntities(c.text);
|
||||
break;
|
||||
case "detail":
|
||||
detail = faultStruct(c.children.find(n => n.localName === "Fault"));
|
||||
detail = faultStruct(c.children.find((n) => n.localName === "Fault"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { faultCode, faultString, detail };
|
||||
if (!detail) throw new Error("Missing detail element");
|
||||
|
||||
if (faultCode == null) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "faultcode",
|
||||
});
|
||||
faultCode = "Client";
|
||||
}
|
||||
|
||||
if (faultString == null) {
|
||||
warnings.push({
|
||||
message: "Missing or invalid XML node",
|
||||
element: "faultstring",
|
||||
});
|
||||
faultString = "CWMP fault";
|
||||
}
|
||||
|
||||
return { faultCode, faultString, detail } as CpeFault;
|
||||
}
|
||||
|
||||
export function request(body: string, cwmpVersion, warn): SoapMessage {
|
||||
export function request(
|
||||
body: string,
|
||||
warn: Record<string, unknown>[]
|
||||
): SoapMessage {
|
||||
warnings = warn;
|
||||
|
||||
const rpc = {
|
||||
id: null,
|
||||
cwmpVersion: cwmpVersion,
|
||||
cwmpVersion: null,
|
||||
sessionTimeout: null,
|
||||
cpeRequest: null,
|
||||
cpeFault: null,
|
||||
cpeResponse: null
|
||||
cpeResponse: null,
|
||||
};
|
||||
|
||||
if (!body.length) return rpc;
|
||||
@@ -555,7 +876,7 @@ export function request(body: string, cwmpVersion, warn): SoapMessage {
|
||||
|
||||
const methodElement = bodyElement.children[0];
|
||||
|
||||
if (!rpc.cwmpVersion && methodElement.localName !== "Fault") {
|
||||
if (methodElement.localName === "Inform") {
|
||||
let namespace, namespaceHref;
|
||||
for (const e of [methodElement, bodyElement, envelope]) {
|
||||
namespace = namespace || e.namespace;
|
||||
@@ -563,9 +884,9 @@ export function request(body: string, cwmpVersion, warn): SoapMessage {
|
||||
const attrs = memoizedParseAttrs(e.attrs);
|
||||
const attr = namespace
|
||||
? attrs.find(
|
||||
s => s.namespace === "xmlns" && s.localName === namespace
|
||||
(s) => s.namespace === "xmlns" && s.localName === namespace
|
||||
)
|
||||
: attrs.find(s => s.name === "xmlns");
|
||||
: attrs.find((s) => s.name === "xmlns");
|
||||
|
||||
if (attr) namespaceHref = attr.value;
|
||||
}
|
||||
@@ -610,9 +931,15 @@ export function request(body: string, cwmpVersion, warn): SoapMessage {
|
||||
case "GetParameterValuesResponse":
|
||||
rpc.cpeResponse = GetParameterValuesResponse(methodElement);
|
||||
break;
|
||||
case "GetParameterAttributesResponse":
|
||||
rpc.cpeResponse = GetParameterAttributesResponse(methodElement);
|
||||
break;
|
||||
case "SetParameterValuesResponse":
|
||||
rpc.cpeResponse = SetParameterValuesResponse(methodElement);
|
||||
break;
|
||||
case "SetParameterAttributesResponse":
|
||||
rpc.cpeResponse = SetParameterAttributesResponse();
|
||||
break;
|
||||
case "AddObjectResponse":
|
||||
rpc.cpeResponse = AddObjectResponse(methodElement);
|
||||
break;
|
||||
@@ -653,13 +980,18 @@ const namespacesAttrs = {
|
||||
.join(" "),
|
||||
"1.4": Object.entries(NAMESPACES["1.4"])
|
||||
.map(([k, v]) => `xmlns:${k}="${v}"`)
|
||||
.join(" ")
|
||||
.join(" "),
|
||||
};
|
||||
|
||||
export function response(rpc): { code: number; headers: {}; data: string } {
|
||||
export function response(rpc: {
|
||||
id: string;
|
||||
acsRequest?: AcsRequest;
|
||||
acsResponse?: AcsResponse;
|
||||
cwmpVersion?: string;
|
||||
}): { code: number; headers: Record<string, string>; data: string } {
|
||||
const headers = {
|
||||
Server: SERVER_NAME,
|
||||
SOAPServer: SERVER_NAME
|
||||
SOAPServer: SERVER_NAME,
|
||||
};
|
||||
|
||||
if (!rpc) return { code: 204, headers: headers, data: "" };
|
||||
@@ -680,7 +1012,11 @@ export function response(rpc): { code: number; headers: {}; data: string } {
|
||||
body = RequestDownloadResponse();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown method response type ${rpc.acsResponse.name}`);
|
||||
throw new Error(
|
||||
`Unknown method response type ${
|
||||
(rpc.acsResponse as AcsResponse).name
|
||||
}`
|
||||
);
|
||||
}
|
||||
} else if (rpc.acsRequest) {
|
||||
switch (rpc.acsRequest.name) {
|
||||
@@ -690,9 +1026,15 @@ export function response(rpc): { code: number; headers: {}; data: string } {
|
||||
case "GetParameterValues":
|
||||
body = GetParameterValues(rpc.acsRequest);
|
||||
break;
|
||||
case "GetParameterAttributes":
|
||||
body = GetParameterAttributes(rpc.acsRequest);
|
||||
break;
|
||||
case "SetParameterValues":
|
||||
body = SetParameterValues(rpc.acsRequest);
|
||||
break;
|
||||
case "SetParameterAttributes":
|
||||
body = SetParameterAttributes(rpc.acsRequest);
|
||||
break;
|
||||
case "AddObject":
|
||||
body = AddObject(rpc.acsRequest);
|
||||
break;
|
||||
@@ -709,7 +1051,9 @@ export function response(rpc): { code: number; headers: {}; data: string } {
|
||||
body = Download(rpc.acsRequest);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown method request ${rpc.acsRequest.name}`);
|
||||
throw new Error(
|
||||
`Unknown method request ${(rpc.acsRequest as AcsRequest).name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,6 +1065,6 @@ export function response(rpc): { code: number; headers: {}; data: string } {
|
||||
namespacesAttrs[rpc.cwmpVersion]
|
||||
}><soap-env:Header><cwmp:ID soap-env:mustUnderstand="1">${
|
||||
rpc.id
|
||||
}</cwmp:ID></soap-env:Header><soap-env:Body>${body}</soap-env:Body></soap-env:Envelope>`
|
||||
}</cwmp:ID></soap-env:Header><soap-env:Body>${body}</soap-env:Body></soap-env:Envelope>`,
|
||||
};
|
||||
}
|
||||
+252
-66
@@ -22,6 +22,7 @@ import PathSet from "./common/path-set";
|
||||
import VersionedMap from "./versioned-map";
|
||||
import InstanceSet from "./instance-set";
|
||||
import { IncomingMessage, ServerResponse } from "http";
|
||||
import { Script } from "vm";
|
||||
|
||||
export type Expression = string | number | boolean | null | any[];
|
||||
|
||||
@@ -51,18 +52,24 @@ export interface Attributes {
|
||||
object?: [number, 1 | 0];
|
||||
writable?: [number, 1 | 0];
|
||||
value?: [number, [string | number | boolean, string]];
|
||||
notification?: [number, number];
|
||||
accessList?: [number, string[]];
|
||||
}
|
||||
|
||||
export interface AttributeTimestamps {
|
||||
object?: number;
|
||||
writable?: number;
|
||||
value?: number;
|
||||
notification?: number;
|
||||
accessList?: number;
|
||||
}
|
||||
|
||||
export interface AttributeValues {
|
||||
object?: boolean;
|
||||
writable?: boolean;
|
||||
value?: [string | number | boolean, string?];
|
||||
notification?: number;
|
||||
accessList?: string[];
|
||||
}
|
||||
|
||||
export interface DeviceData {
|
||||
@@ -75,12 +82,21 @@ export interface DeviceData {
|
||||
|
||||
export type VirtualParameterDeclaration = [
|
||||
Path,
|
||||
{ path?: number; object?: number; writable?: number; value?: number }?,
|
||||
{
|
||||
path?: number;
|
||||
object?: number;
|
||||
writable?: number;
|
||||
value?: number;
|
||||
notification?: number;
|
||||
accessList?: number;
|
||||
}?,
|
||||
{
|
||||
path?: [number, number];
|
||||
object?: boolean;
|
||||
writable?: boolean;
|
||||
value?: [string | number | boolean, string?];
|
||||
notification?: number;
|
||||
accessList?: string[];
|
||||
}?
|
||||
];
|
||||
|
||||
@@ -90,8 +106,11 @@ export interface SyncState {
|
||||
object: Set<Path>;
|
||||
writable: Set<Path>;
|
||||
value: Set<Path>;
|
||||
notification: Set<Path>;
|
||||
accessList: Set<Path>;
|
||||
};
|
||||
spv: Map<Path, [string | number | boolean, string]>;
|
||||
spa: Map<Path, { notification: number; accessList: string[] }>;
|
||||
gpn: Set<Path>;
|
||||
gpnPatterns: Map<Path, number>;
|
||||
tags: Map<Path, boolean>;
|
||||
@@ -131,7 +150,6 @@ export interface SessionContext {
|
||||
faults?: { [channel: string]: SessionFault };
|
||||
retries?: { [channel: string]: number };
|
||||
cacheSnapshot?: string;
|
||||
configContext?: { [name: string]: any };
|
||||
httpResponse?: ServerResponse;
|
||||
httpRequest?: IncomingMessage;
|
||||
faultsTouched?: { [channel: string]: boolean };
|
||||
@@ -139,11 +157,13 @@ export interface SessionContext {
|
||||
new?: boolean;
|
||||
debug?: boolean;
|
||||
state: number;
|
||||
authState: number;
|
||||
tasks?: Task[];
|
||||
operations?: { [commandKey: string]: Operation };
|
||||
cacheUntil?: number;
|
||||
syncState?: SyncState;
|
||||
lastActivity?: number;
|
||||
extendLock?: number;
|
||||
rpcRequest?: AcsRequest;
|
||||
operationsTouched?: { [commandKey: string]: 1 | 0 };
|
||||
provisionsRet?: any[];
|
||||
@@ -151,7 +171,7 @@ export interface SessionContext {
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
_id: string;
|
||||
_id?: string;
|
||||
name: string;
|
||||
parameterNames?: string[];
|
||||
parameterValues?: [string, string | number | boolean, string?][];
|
||||
@@ -160,6 +180,7 @@ export interface Task {
|
||||
fileName?: string;
|
||||
targetFileName?: string;
|
||||
expiry?: number;
|
||||
provisions?: (string | number | boolean)[][];
|
||||
}
|
||||
|
||||
export interface Operation {
|
||||
@@ -176,44 +197,84 @@ export interface Operation {
|
||||
};
|
||||
}
|
||||
|
||||
export interface AcsRequest {
|
||||
name: string;
|
||||
next?: string;
|
||||
export type AcsRequest =
|
||||
| GetParameterNames
|
||||
| GetParameterValues
|
||||
| GetParameterAttributes
|
||||
| SetParameterValues
|
||||
| SetParameterAttributes
|
||||
| AddObject
|
||||
| DeleteObject
|
||||
| FactoryReset
|
||||
| Reboot
|
||||
| Download;
|
||||
|
||||
export interface GetParameterNames {
|
||||
name: "GetParameterNames";
|
||||
parameterPath: string;
|
||||
nextLevel: boolean;
|
||||
}
|
||||
|
||||
export interface GetAcsRequest extends AcsRequest {
|
||||
name: "GetParameterNames" | "GetParameterValues";
|
||||
objectName?: string;
|
||||
parameterNames?: string[];
|
||||
parameterPath?: string;
|
||||
nextLevel?: boolean;
|
||||
instanceValues?: { [name: string]: string };
|
||||
export interface GetParameterValues {
|
||||
name: "GetParameterValues";
|
||||
parameterNames: string[];
|
||||
}
|
||||
|
||||
export interface SetAcsRequest extends AcsRequest {
|
||||
name:
|
||||
| "SetParameterValues"
|
||||
| "AddObject"
|
||||
| "DeleteObject"
|
||||
| "FactoryReset"
|
||||
| "Reboot"
|
||||
| "Download";
|
||||
parameterList?: [string, string | number | boolean, string][];
|
||||
instanceValues?: { [param: string]: string | number | boolean };
|
||||
objectName?: string;
|
||||
export interface GetParameterAttributes {
|
||||
name: "GetParameterAttributes";
|
||||
parameterNames: string[];
|
||||
}
|
||||
|
||||
export interface SetParameterValues {
|
||||
name: "SetParameterValues";
|
||||
parameterList: [string, boolean | number | string, string][];
|
||||
parameterKey?: string;
|
||||
DATETIME_MILLISECONDS?: boolean;
|
||||
BOOLEAN_LITERAL?: boolean;
|
||||
commandKey?: string;
|
||||
instance?: string;
|
||||
fileType?: string;
|
||||
fileSize?: number;
|
||||
url?: string;
|
||||
fileName?: string;
|
||||
targetFileName?: string;
|
||||
}
|
||||
|
||||
export interface CpeResponse {
|
||||
name: string;
|
||||
export interface SetParameterAttributes {
|
||||
name: "SetParameterAttributes";
|
||||
parameterList: [string, number, string[]][];
|
||||
}
|
||||
|
||||
export interface AddObject {
|
||||
name: "AddObject";
|
||||
objectName: string;
|
||||
parameterKey?: string;
|
||||
instanceValues: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DeleteObject {
|
||||
name: "DeleteObject";
|
||||
objectName: string;
|
||||
parameterKey?: string;
|
||||
}
|
||||
|
||||
export interface FactoryReset {
|
||||
name: "FactoryReset";
|
||||
commandKey?: string;
|
||||
}
|
||||
|
||||
export interface Reboot {
|
||||
name: "Reboot";
|
||||
commandKey?: string;
|
||||
}
|
||||
|
||||
export interface Download {
|
||||
name: "Download";
|
||||
commandKey: string;
|
||||
instance: string;
|
||||
fileType: string;
|
||||
fileName?: string;
|
||||
url?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
fileSize?: number;
|
||||
targetFileName?: string;
|
||||
delaySecods?: number;
|
||||
successUrl?: string;
|
||||
failureUrl?: string;
|
||||
}
|
||||
|
||||
export interface SpvFault {
|
||||
@@ -229,38 +290,80 @@ export interface FaultStruct {
|
||||
}
|
||||
|
||||
export interface CpeFault {
|
||||
faultCode: string;
|
||||
faultString: string;
|
||||
faultCode: "Client" | "Server";
|
||||
faultString: "CWMP fault";
|
||||
detail?: FaultStruct;
|
||||
}
|
||||
|
||||
export interface CpeGetResponse extends CpeResponse {
|
||||
name: "GetParameterNamesResponse" | "GetParameterValuesResponse";
|
||||
parameterList?:
|
||||
| [string, boolean][]
|
||||
| [string, string | number | boolean, string][];
|
||||
export type CpeResponse =
|
||||
| GetParameterNamesResponse
|
||||
| GetParameterValuesResponse
|
||||
| GetParameterAttributesResponse
|
||||
| SetParameterValuesResponse
|
||||
| SetParameterAttributesResponse
|
||||
| AddObjectResponse
|
||||
| DeleteObjectResponse
|
||||
| RebootResponse
|
||||
| FactoryResetResponse
|
||||
| DownloadResponse;
|
||||
|
||||
export interface GetParameterNamesResponse {
|
||||
name: "GetParameterNamesResponse";
|
||||
parameterList: [Path, boolean, boolean][];
|
||||
}
|
||||
|
||||
export interface CpeSetResponse extends CpeResponse {
|
||||
name:
|
||||
| "SetParameterValuesResponse"
|
||||
| "AddObjectResponse"
|
||||
| "DeleteObjectResponse"
|
||||
| "RebootResponse"
|
||||
| "FactoryResetResponse"
|
||||
| "DownloadResponse";
|
||||
status?: number;
|
||||
instanceNumber?: string;
|
||||
export interface GetParameterValuesResponse {
|
||||
name: "GetParameterValuesResponse";
|
||||
parameterList: [Path, string | number | boolean, string][];
|
||||
}
|
||||
|
||||
export interface GetParameterAttributesResponse {
|
||||
name: "GetParameterAttributesResponse";
|
||||
parameterList: [Path, number, string[]][];
|
||||
}
|
||||
|
||||
export interface SetParameterValuesResponse {
|
||||
name: "SetParameterValuesResponse";
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface SetParameterAttributesResponse {
|
||||
name: "SetParameterAttributesResponse";
|
||||
}
|
||||
|
||||
export interface AddObjectResponse {
|
||||
name: "AddObjectResponse";
|
||||
instanceNumber: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface DeleteObjectResponse {
|
||||
name: "DeleteObjectResponse";
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface RebootResponse {
|
||||
name: "RebootResponse";
|
||||
}
|
||||
|
||||
export interface FactoryResetResponse {
|
||||
name: "FactoryResetResponse";
|
||||
}
|
||||
|
||||
export interface DownloadResponse {
|
||||
name: "DownloadResponse";
|
||||
status: number;
|
||||
startTime?: number;
|
||||
completeTime?: number;
|
||||
}
|
||||
|
||||
export interface CpeRequest {
|
||||
name: string;
|
||||
fileType?: string;
|
||||
}
|
||||
export type CpeRequest =
|
||||
| InformRequest
|
||||
| TransferCompleteRequest
|
||||
| GetRPCMethodsRequest
|
||||
| RequestDownloadRequest;
|
||||
|
||||
export interface InformRequest extends CpeRequest {
|
||||
export interface InformRequest {
|
||||
name: "Inform";
|
||||
deviceId: {
|
||||
Manufacturer: string;
|
||||
@@ -270,10 +373,10 @@ export interface InformRequest extends CpeRequest {
|
||||
};
|
||||
event: string[];
|
||||
retryCount: number;
|
||||
parameterList: [string, string | number | boolean, string][];
|
||||
parameterList: [Path, string | number | boolean, string][];
|
||||
}
|
||||
|
||||
export interface TransferCompleteRequest extends CpeRequest {
|
||||
export interface TransferCompleteRequest {
|
||||
name: "TransferComplete";
|
||||
commandKey?: string;
|
||||
faultStruct?: FaultStruct;
|
||||
@@ -281,10 +384,36 @@ export interface TransferCompleteRequest extends CpeRequest {
|
||||
completeTime?: number;
|
||||
}
|
||||
|
||||
export interface AcsResponse {
|
||||
name: string;
|
||||
commandKey?: string;
|
||||
faultStruct?: FaultStruct;
|
||||
export interface GetRPCMethodsRequest {
|
||||
name: "GetRPCMethods";
|
||||
}
|
||||
|
||||
export interface RequestDownloadRequest {
|
||||
name: "RequestDownload";
|
||||
fileType: string;
|
||||
}
|
||||
|
||||
export type AcsResponse =
|
||||
| InformResponse
|
||||
| GetRPCMethodsResponse
|
||||
| TransferCompleteResponse
|
||||
| RequestDownloadResponse;
|
||||
|
||||
export interface InformResponse {
|
||||
name: "InformResponse";
|
||||
}
|
||||
|
||||
export interface GetRPCMethodsResponse {
|
||||
name: "GetRPCMethodsResponse";
|
||||
methodList: string[];
|
||||
}
|
||||
|
||||
export interface TransferCompleteResponse {
|
||||
name: "TransferCompleteResponse";
|
||||
}
|
||||
|
||||
export interface RequestDownloadResponse {
|
||||
name: "RequestDownloadResponse";
|
||||
}
|
||||
|
||||
export interface QueryOptions {
|
||||
@@ -299,12 +428,20 @@ export interface QueryOptions {
|
||||
export interface Declaration {
|
||||
path: Path;
|
||||
pathGet: number;
|
||||
pathSet?: [number, number];
|
||||
attrGet?: { object?: number; writable?: number; value?: number };
|
||||
pathSet?: number | [number, number];
|
||||
attrGet?: {
|
||||
object?: number;
|
||||
writable?: number;
|
||||
value?: number;
|
||||
notification?: number;
|
||||
accessList?: number;
|
||||
};
|
||||
attrSet?: {
|
||||
object?: boolean;
|
||||
writable?: boolean;
|
||||
value?: [string | number | boolean, string?];
|
||||
notification?: number;
|
||||
accessList?: string[];
|
||||
};
|
||||
defer: boolean;
|
||||
}
|
||||
@@ -312,7 +449,13 @@ export interface Declaration {
|
||||
export type Clear = [
|
||||
Path,
|
||||
number,
|
||||
{ object?: number; writable?: number; value?: number }?,
|
||||
{
|
||||
object?: number;
|
||||
writable?: number;
|
||||
value?: number;
|
||||
notification?: number;
|
||||
accessList?: number;
|
||||
}?,
|
||||
number?
|
||||
];
|
||||
|
||||
@@ -321,8 +464,36 @@ export interface Preset {
|
||||
channel: string;
|
||||
schedule?: { md5: string; duration: number; schedule: any };
|
||||
events?: { [event: string]: boolean };
|
||||
precondition?: {};
|
||||
provisions: string[][];
|
||||
precondition?: Expression;
|
||||
provisions: [string, ...Expression[]][];
|
||||
}
|
||||
|
||||
export interface Provisions {
|
||||
[name: string]: { md5: string; script: Script };
|
||||
}
|
||||
|
||||
export interface VirtualParameters {
|
||||
[name: string]: { md5: string; script: Script };
|
||||
}
|
||||
|
||||
export interface Files {
|
||||
[name: string]: { length: number };
|
||||
}
|
||||
|
||||
export interface Users {
|
||||
[name: string]: { password: string; salt: string; roles: string[] };
|
||||
}
|
||||
|
||||
export interface Permissions {
|
||||
[role: string]: {
|
||||
[access: number]: {
|
||||
[resource: string]: {
|
||||
access: number;
|
||||
filter: Expression;
|
||||
validate?: Expression;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type PermissionSet = {
|
||||
@@ -333,6 +504,21 @@ export type PermissionSet = {
|
||||
};
|
||||
}[];
|
||||
|
||||
export interface Config {
|
||||
[name: string]: Expression;
|
||||
}
|
||||
|
||||
export interface UiConfig {
|
||||
filters: Record<string, unknown>;
|
||||
device: Record<string, unknown>;
|
||||
index: Record<string, unknown>;
|
||||
overview: {
|
||||
charts?: Record<string, unknown>;
|
||||
groups?: Record<string, unknown>;
|
||||
};
|
||||
pageSize?: Expression;
|
||||
}
|
||||
|
||||
export interface SoapMessage {
|
||||
id: string;
|
||||
cwmpVersion: string;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Z_SYNC_FLUSH } from "zlib";
|
||||
import { constants } from "zlib";
|
||||
import Koa from "koa";
|
||||
import Router from "koa-router";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
@@ -31,9 +31,10 @@ import Authorizer from "./common/authorizer";
|
||||
import * as logger from "./logger";
|
||||
import * as localCache from "./local-cache";
|
||||
import { PermissionSet } from "./types";
|
||||
import { authSimple } from "./ui/api-functions";
|
||||
import { authLocal } from "./ui/api-functions";
|
||||
import * as init from "./init";
|
||||
import { version as VERSION } from "../package.json";
|
||||
import memoize from "./common/memoize";
|
||||
|
||||
declare module "koa" {
|
||||
interface Request {
|
||||
@@ -47,18 +48,23 @@ const router = new Router();
|
||||
const JWT_SECRET = "" + config.get("UI_JWT_SECRET");
|
||||
const JWT_COOKIE = "genieacs-ui-jwt";
|
||||
|
||||
function getPermissionSets(ctx): PermissionSet[] {
|
||||
const allPermissions = localCache.getPermissions(ctx.state.configSnapshot);
|
||||
const users = localCache.getUsers(ctx.state.configSnapshot);
|
||||
const user = users[ctx.state.user.username];
|
||||
const permissionSets = user.roles.map(role =>
|
||||
Object.values(allPermissions[role] || {})
|
||||
);
|
||||
return permissionSets;
|
||||
}
|
||||
const getAuthorizer = memoize(
|
||||
(snapshot: string, rolesStr: string): Authorizer => {
|
||||
const roles: string[] = JSON.parse(rolesStr);
|
||||
const allPermissions = localCache.getPermissions(snapshot);
|
||||
const permissionSets: PermissionSet[] = roles.map((r) =>
|
||||
Object.values(allPermissions[r] || {})
|
||||
);
|
||||
return new Authorizer(permissionSets);
|
||||
}
|
||||
);
|
||||
|
||||
koa.on("error", async err => {
|
||||
throw err;
|
||||
koa.on("error", async (err, ctx) => {
|
||||
setTimeout(() => {
|
||||
// Ignored errors resulting from aborted requests
|
||||
if (ctx?.req.aborted) return;
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
|
||||
koa.use(async (ctx, next) => {
|
||||
@@ -73,19 +79,43 @@ koa.use(
|
||||
koaJwt({
|
||||
secret: JWT_SECRET,
|
||||
passthrough: true,
|
||||
cookie: JWT_COOKIE
|
||||
cookie: JWT_COOKIE,
|
||||
isRevoked: async (ctx, token) => {
|
||||
if (token["authMethod"] === "local") {
|
||||
return !localCache.getUsers(ctx.state.configSnapshot)[
|
||||
token["username"]
|
||||
];
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
koa.use(async (ctx, next) => {
|
||||
if (ctx.state.user && ctx.state.user.username)
|
||||
ctx.state.authorizer = new Authorizer(getPermissionSets(ctx));
|
||||
else ctx.state.authorizer = new Authorizer([]);
|
||||
let roles: string[] = [];
|
||||
|
||||
if (ctx.state.user?.username) {
|
||||
let user;
|
||||
if (ctx.state.user.authMethod === "local") {
|
||||
user = localCache.getUsers(ctx.state.configSnapshot)[
|
||||
ctx.state.user.username
|
||||
];
|
||||
} else {
|
||||
throw new Error("Invalid auth method");
|
||||
}
|
||||
roles = user.roles || [];
|
||||
}
|
||||
|
||||
ctx.state.authorizer = getAuthorizer(
|
||||
ctx.state.configSnapshot,
|
||||
JSON.stringify(roles)
|
||||
);
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
router.post("/login", async ctx => {
|
||||
router.post("/login", async (ctx) => {
|
||||
if (!JWT_SECRET) {
|
||||
ctx.status = 500;
|
||||
ctx.body = "UI_JWT_SECRET is not set";
|
||||
@@ -100,12 +130,12 @@ router.post("/login", async ctx => {
|
||||
message: "Log in",
|
||||
context: ctx,
|
||||
username: username,
|
||||
method: null
|
||||
method: null,
|
||||
};
|
||||
|
||||
function success(method): void {
|
||||
log.method = method;
|
||||
const token = jwt.sign({ username }, JWT_SECRET);
|
||||
function success(authMethod): void {
|
||||
log.method = authMethod;
|
||||
const token = jwt.sign({ username, authMethod }, JWT_SECRET);
|
||||
ctx.cookies.set(JWT_COOKIE, token, { sameSite: "lax" });
|
||||
ctx.body = JSON.stringify(token);
|
||||
logger.accessInfo(log);
|
||||
@@ -118,20 +148,19 @@ router.post("/login", async ctx => {
|
||||
logger.accessWarn(log);
|
||||
}
|
||||
|
||||
const roles = await authSimple(ctx.state.configSnapshot, username, password);
|
||||
|
||||
if (roles) return void success("simple");
|
||||
if (await authLocal(ctx.state.configSnapshot, username, password))
|
||||
return void success("local");
|
||||
|
||||
failure();
|
||||
});
|
||||
|
||||
router.post("/logout", async ctx => {
|
||||
router.post("/logout", async (ctx) => {
|
||||
ctx.cookies.set(JWT_COOKIE); // Delete cookie
|
||||
ctx.body = "";
|
||||
|
||||
logger.accessInfo({
|
||||
message: "Log out",
|
||||
context: ctx
|
||||
context: ctx,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -145,11 +174,11 @@ koa.use(async (ctx, next) => {
|
||||
koa.use(koaBodyParser());
|
||||
router.use("/api", api.routes(), api.allowedMethods());
|
||||
|
||||
router.get("/status", ctx => {
|
||||
router.get("/status", (ctx) => {
|
||||
ctx.body = "OK";
|
||||
});
|
||||
|
||||
router.get("/init", async ctx => {
|
||||
router.get("/init", async (ctx) => {
|
||||
const status = await init.getStatus();
|
||||
if (Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) {
|
||||
if (!ctx.state.authorizer.hasAccess("users", 3)) status["users"] = false;
|
||||
@@ -170,7 +199,7 @@ router.get("/init", async ctx => {
|
||||
ctx.body = status;
|
||||
});
|
||||
|
||||
router.post("/init", async ctx => {
|
||||
router.post("/init", async (ctx) => {
|
||||
const status = ctx.request.body;
|
||||
if (Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length) {
|
||||
if (!ctx.state.authorizer.hasAccess("users", 3)) status["users"] = false;
|
||||
@@ -191,10 +220,9 @@ router.post("/init", async ctx => {
|
||||
ctx.body = "";
|
||||
});
|
||||
|
||||
router.get("/", async ctx => {
|
||||
let permissionSets = [];
|
||||
if (ctx.state.user && ctx.state.user.username)
|
||||
permissionSets = getPermissionSets(ctx);
|
||||
router.get("/", async (ctx) => {
|
||||
const permissionSets: PermissionSet[] =
|
||||
ctx.state.authorizer.getPermissionSets();
|
||||
|
||||
let wizard = "";
|
||||
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
|
||||
@@ -211,7 +239,7 @@ router.get("/", async ctx => {
|
||||
<noscript>GenieACS UI requires JavaScript to work. Please enable JavaScript in your browser.</noscript>
|
||||
<script>
|
||||
window.clientConfig = ${JSON.stringify({
|
||||
ui: localCache.getUiConfig(ctx.state.configSnapshot)
|
||||
ui: localCache.getUiConfig(ctx.state.configSnapshot),
|
||||
})};
|
||||
window.configSnapshot = ${JSON.stringify(ctx.state.configSnapshot)};
|
||||
window.genieacsVersion = ${JSON.stringify(VERSION)};
|
||||
@@ -220,7 +248,7 @@ router.get("/", async ctx => {
|
||||
)};
|
||||
window.permissionSets = ${JSON.stringify(permissionSets)};
|
||||
</script>
|
||||
<script src="app.js"></script>${wizard}
|
||||
<script type="module" src="app.js"></script>${wizard}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
@@ -228,7 +256,17 @@ router.get("/", async ctx => {
|
||||
|
||||
koa.use(
|
||||
koaCompress({
|
||||
flush: Z_SYNC_FLUSH
|
||||
gzip: {
|
||||
flush: constants.Z_SYNC_FLUSH,
|
||||
},
|
||||
deflate: {
|
||||
flush: constants.Z_SYNC_FLUSH,
|
||||
},
|
||||
br: {
|
||||
params: {
|
||||
[constants.BROTLI_PARAM_QUALITY]: 5,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
+70
-66
@@ -17,18 +17,19 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { exec } from "child_process";
|
||||
import { ObjectID } from "mongodb";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as db from "./db";
|
||||
import { del } from "../cache";
|
||||
import { getUsers } from "../local-cache";
|
||||
import { hashPassword } from "../auth";
|
||||
import {
|
||||
insertTasks,
|
||||
watchTask,
|
||||
awaitSessionEnd,
|
||||
awaitSessionStart,
|
||||
connectionRequest,
|
||||
deleteDevice
|
||||
deleteDevice,
|
||||
} from "../api-functions";
|
||||
import { Task } from "../types";
|
||||
|
||||
async function deleteFault(id): Promise<void> {
|
||||
const deviceId = id.split(":", 1)[0];
|
||||
@@ -37,14 +38,17 @@ async function deleteFault(id): Promise<void> {
|
||||
await Promise.all([
|
||||
db.deleteFault(id),
|
||||
channel.startsWith("task_")
|
||||
? db.deleteTask(new ObjectID(channel.slice(5)))
|
||||
: null
|
||||
? db.deleteTask(new ObjectId(channel.slice(5)))
|
||||
: null,
|
||||
]);
|
||||
|
||||
await del(`${deviceId}_tasks_faults_operations`);
|
||||
}
|
||||
|
||||
export async function deleteResource(resource, id): Promise<void> {
|
||||
export async function deleteResource(
|
||||
resource: string,
|
||||
id: string
|
||||
): Promise<void> {
|
||||
switch (resource) {
|
||||
case "devices":
|
||||
await deleteDevice(id);
|
||||
@@ -87,35 +91,59 @@ export async function deleteResource(resource, id): Promise<void> {
|
||||
}
|
||||
|
||||
export async function postTasks(
|
||||
deviceId,
|
||||
tasks,
|
||||
timeout
|
||||
deviceId: string,
|
||||
tasks: Task[],
|
||||
timeout: number,
|
||||
device: Record<string, { value?: [boolean | number | string, string] }>
|
||||
): Promise<{ connectionRequest: string; tasks: any[] }> {
|
||||
for (const task of tasks) {
|
||||
delete task._id;
|
||||
task.device = deviceId;
|
||||
task["device"] = deviceId;
|
||||
}
|
||||
|
||||
tasks = await insertTasks(tasks);
|
||||
const statuses = tasks.map(t => {
|
||||
const statuses = tasks.map((t) => {
|
||||
return { _id: t._id, status: "pending" };
|
||||
});
|
||||
|
||||
await del(`${deviceId}_tasks_faults_operations`);
|
||||
|
||||
try {
|
||||
await connectionRequest(deviceId);
|
||||
} catch (err) {
|
||||
const lastInform = Date.now();
|
||||
const notInSession = await awaitSessionEnd(deviceId, 30000);
|
||||
if (!notInSession) {
|
||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||
return {
|
||||
connectionRequest: err.message,
|
||||
tasks: statuses
|
||||
connectionRequest: "Session took too long to complete",
|
||||
tasks: statuses,
|
||||
};
|
||||
}
|
||||
|
||||
const sample = tasks[tasks.length - 1];
|
||||
await del(`${deviceId}_tasks_faults_operations`);
|
||||
|
||||
// Waiting for session to finish or timeout
|
||||
await watchTask(deviceId, sample._id, timeout);
|
||||
const status = await connectionRequest(deviceId, device);
|
||||
|
||||
if (status) {
|
||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||
return {
|
||||
connectionRequest: status,
|
||||
tasks: statuses,
|
||||
};
|
||||
}
|
||||
|
||||
const sessionStarted = await awaitSessionStart(deviceId, lastInform, timeout);
|
||||
if (!sessionStarted) {
|
||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||
return {
|
||||
connectionRequest: "No contact from CPE",
|
||||
tasks: statuses,
|
||||
};
|
||||
}
|
||||
|
||||
const sessionEnded = await awaitSessionEnd(deviceId, 120000);
|
||||
if (!sessionEnded) {
|
||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||
return {
|
||||
connectionRequest: "Session took too long to complete",
|
||||
tasks: statuses,
|
||||
};
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
for (const s of statuses) {
|
||||
@@ -131,48 +159,20 @@ export async function postTasks(
|
||||
r.status = "done";
|
||||
} else if (res[i * 2 + 1].length === 1) {
|
||||
r.status = "fault";
|
||||
r.fault = res[i * 2 + 1][0];
|
||||
r["fault"] = res[i * 2 + 1][0];
|
||||
}
|
||||
db.deleteTask(r._id);
|
||||
}
|
||||
|
||||
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
|
||||
|
||||
return { connectionRequest: "OK", tasks: statuses };
|
||||
}
|
||||
|
||||
interface PingResponse {
|
||||
packetsTransmitted: number;
|
||||
packetsReceived: number;
|
||||
packetLoss: string;
|
||||
min: number;
|
||||
avg: number;
|
||||
max: number;
|
||||
mdev: number;
|
||||
}
|
||||
|
||||
export function ping(host): Promise<PingResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`ping -w 1 -i 0.2 -c 3 ${host}`, (err, stdout) => {
|
||||
if (err) return void reject(err);
|
||||
|
||||
const m = stdout.match(
|
||||
/(\d) packets transmitted, (\d) received, ([\d.%]+) packet loss[^]*([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/
|
||||
);
|
||||
if (!m) return void reject(new Error("Could not parse ping response"));
|
||||
|
||||
resolve({
|
||||
packetsTransmitted: +m[1],
|
||||
packetsReceived: +m[2],
|
||||
packetLoss: m[3],
|
||||
min: +m[4],
|
||||
avg: +m[5],
|
||||
max: +m[6],
|
||||
mdev: +m[7]
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function putResource(resource, id, data): Promise<void> {
|
||||
export async function putResource(
|
||||
resource: string,
|
||||
id: string,
|
||||
data: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
if (resource === "presets") {
|
||||
await db.putPreset(id, data);
|
||||
} else if (resource === "provisions") {
|
||||
@@ -184,23 +184,27 @@ export async function putResource(resource, id, data): Promise<void> {
|
||||
} else if (resource === "permissions") {
|
||||
await db.putPermission(id, data);
|
||||
} else if (resource === "users") {
|
||||
delete data.password;
|
||||
delete data.salt;
|
||||
delete data["password"];
|
||||
delete data["salt"];
|
||||
await db.putUser(id, data);
|
||||
}
|
||||
|
||||
await del("presets_hash");
|
||||
}
|
||||
|
||||
export function authSimple(snapshot, username, password): Promise<string[]> {
|
||||
export function authLocal(
|
||||
snapshot: string,
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const users = getUsers(snapshot);
|
||||
const user = users[username];
|
||||
if (!user || !user.password) return void resolve(null);
|
||||
if (!user?.password) return void resolve(null);
|
||||
hashPassword(password, user.salt)
|
||||
.then(hash => {
|
||||
if (hash === user.password) resolve(user.roles);
|
||||
else resolve(null);
|
||||
.then((hash) => {
|
||||
if (hash === user.password) resolve(true);
|
||||
else resolve(false);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
+255
-114
@@ -28,6 +28,10 @@ import { getConfig } from "../local-cache";
|
||||
import { QueryOptions, Expression } from "../types";
|
||||
import { generateSalt, hashPassword } from "../auth";
|
||||
import { del } from "../cache";
|
||||
import Authorizer from "../common/authorizer";
|
||||
import { ping } from "../ping";
|
||||
import { decodeTag } from "../common";
|
||||
import { stringify as yamlStringify } from "../common/yaml";
|
||||
|
||||
const router = new Router();
|
||||
export default router;
|
||||
@@ -50,7 +54,7 @@ const RESOURCE_IDS = {
|
||||
permissions: "_id",
|
||||
users: "_id",
|
||||
faults: "_id",
|
||||
tasks: "_id"
|
||||
tasks: "_id",
|
||||
};
|
||||
|
||||
const resources = {
|
||||
@@ -63,21 +67,30 @@ const resources = {
|
||||
permissions: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||
users: 0 | RESOURCE_DELETE | RESOURCE_PUT,
|
||||
faults: 0 | RESOURCE_DELETE,
|
||||
tasks: 0
|
||||
tasks: 0,
|
||||
};
|
||||
|
||||
router.get(`/devices/:id.csv`, async ctx => {
|
||||
function singleParam(p: string | string[]): string {
|
||||
return Array.isArray(p) ? p[p.length - 1] : p;
|
||||
}
|
||||
|
||||
router.get(`/devices/:id.csv`, async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const log = {
|
||||
message: "Query device (CSV)",
|
||||
context: ctx,
|
||||
id: ctx.params.id
|
||||
id: ctx.params.id,
|
||||
};
|
||||
|
||||
const filter = ["=", ["PARAM", RESOURCE_IDS.devices], ctx.params.id];
|
||||
const filter = and(authorizer.getFilter("devices", 2), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS.devices],
|
||||
ctx.params.id,
|
||||
]);
|
||||
|
||||
if (!ctx.state.authorizer.hasAccess("devices", 2)) {
|
||||
if (!authorizer.hasAccess("devices", 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void (ctx.status = 404);
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const res = await db.query("devices", filter);
|
||||
@@ -92,49 +105,65 @@ router.get(`/devices/:id.csv`, async ctx => {
|
||||
|
||||
ctx.body = new stream.PassThrough();
|
||||
ctx.body.write(
|
||||
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp\n"
|
||||
"Parameter,Object,Object timestamp,Writable,Writable timestamp,Value,Value type,Value timestamp,Notification,Notification timestamp,Access list,Access list timestamp\n"
|
||||
);
|
||||
|
||||
for (const k of Object.keys(res[0]).sort()) {
|
||||
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 = [
|
||||
k,
|
||||
p.object,
|
||||
p.objectTimestamp,
|
||||
new Date(p.objectTimestamp).toJSON(),
|
||||
p.writable,
|
||||
p.writableTimestamp,
|
||||
p.value != null ? `"${p.value[0].toString().replace(/"/g, '""')}"` : "",
|
||||
p.value != null ? p.value[1] : "",
|
||||
p.valueTimestamp
|
||||
new Date(p.writableTimestamp).toJSON(),
|
||||
`"${value.toString().replace(/"/g, '""')}"`,
|
||||
type,
|
||||
new Date(p.valueTimestamp).toJSON(),
|
||||
p.notification,
|
||||
new Date(p.notificationTimestamp).toJSON(),
|
||||
p.accessList ? p.accessList.join(", ") : "",
|
||||
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");
|
||||
}
|
||||
ctx.body.end();
|
||||
logger.accessInfo(log);
|
||||
});
|
||||
|
||||
for (const [resource, flags] of Object.entries(resources)) {
|
||||
router.head(`/${resource}`, async (ctx, next) => {
|
||||
let filter: Expression = true;
|
||||
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
|
||||
router.head(`/${resource}`, async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
let filter: Expression = authorizer.getFilter(resource, 1);
|
||||
if (ctx.request.query.filter)
|
||||
filter = and(filter, parse(singleParam(ctx.request.query.filter)));
|
||||
|
||||
const log = {
|
||||
message: `Count ${resource}`,
|
||||
context: ctx,
|
||||
filter: ctx.request.query.filter,
|
||||
count: null
|
||||
count: null,
|
||||
};
|
||||
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 1)) {
|
||||
if (!authorizer.hasAccess(resource, 1)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
// Exclude temporary tasks and faults
|
||||
if (resource === "tasks" || resource === "faults") {
|
||||
filter = and(filter, [
|
||||
"NOT",
|
||||
["<", ["PARAM", "expiry"], Date.now() + 60000]
|
||||
"OR",
|
||||
[">=", ["PARAM", "expiry"], Date.now() + 60000],
|
||||
["IS NULL", ["PARAM", "expiry"]],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -147,16 +176,18 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
logger.accessInfo(log);
|
||||
});
|
||||
|
||||
router.get(`/${resource}`, async (ctx, next) => {
|
||||
router.get(`/${resource}`, async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const options: QueryOptions = {};
|
||||
let filter: Expression = true;
|
||||
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
|
||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||
if (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.skip) options.skip = +ctx.request.query.skip;
|
||||
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) {
|
||||
options.projection = ctx.request.query.projection
|
||||
options.projection = singleParam(ctx.request.query.projection)
|
||||
.split(",")
|
||||
.reduce((obj, k) => Object.assign(obj, { [k]: 1 }), {});
|
||||
}
|
||||
@@ -168,19 +199,20 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
limit: options.limit,
|
||||
skip: options.skip,
|
||||
sort: options.sort,
|
||||
projection: options.projection
|
||||
projection: options.projection,
|
||||
};
|
||||
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
||||
if (!authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
// Exclude temporary tasks and faults
|
||||
if (resource === "tasks" || resource === "faults") {
|
||||
filter = and(filter, [
|
||||
"NOT",
|
||||
["<", ["PARAM", "expiry"], Date.now() + 60000]
|
||||
"OR",
|
||||
[">=", ["PARAM", "expiry"], Date.now() + 60000],
|
||||
["IS NULL", ["PARAM", "expiry"]],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -189,7 +221,7 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
|
||||
let c = 0;
|
||||
ctx.body.write("[\n");
|
||||
await db.query(resource, filter, options, obj => {
|
||||
await db.query(resource, filter, options, (obj) => {
|
||||
ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n");
|
||||
});
|
||||
ctx.body.end("]");
|
||||
@@ -198,14 +230,16 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
});
|
||||
|
||||
// CSV download
|
||||
router.get(`/${resource}.csv`, async (ctx, next) => {
|
||||
router.get(`/${resource}.csv`, async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const options: QueryOptions = { projection: {} };
|
||||
let filter: Expression = true;
|
||||
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
|
||||
let filter: Expression = authorizer.getFilter(resource, 2);
|
||||
if (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.skip) options.skip = +ctx.request.query.skip;
|
||||
if (ctx.request.query.sort)
|
||||
options.sort = JSON.parse(ctx.request.query.sort);
|
||||
options.sort = JSON.parse(singleParam(ctx.request.query.sort));
|
||||
|
||||
const log = {
|
||||
message: `Query ${resource} (CSV)`,
|
||||
@@ -213,15 +247,17 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
filter: ctx.request.query.filter,
|
||||
limit: options.limit,
|
||||
skip: options.skip,
|
||||
sort: options.sort
|
||||
sort: options.sort,
|
||||
};
|
||||
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
||||
if (!authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const columns = JSON.parse(ctx.request.query.columns);
|
||||
const columns: Record<string, Expression> = JSON.parse(
|
||||
singleParam(ctx.request.query.columns)
|
||||
);
|
||||
const now = Date.now();
|
||||
|
||||
for (const [k, v] of Object.entries(columns)) {
|
||||
@@ -234,8 +270,9 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
// Exclude temporary tasks and faults
|
||||
if (resource === "tasks" || resource === "faults") {
|
||||
filter = and(filter, [
|
||||
"NOT",
|
||||
["<", ["PARAM", "expiry"], Date.now() + 60000]
|
||||
"OR",
|
||||
[">=", ["PARAM", "expiry"], Date.now() + 60000],
|
||||
["IS NULL", ["PARAM", "expiry"]],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -246,26 +283,38 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
);
|
||||
|
||||
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 => {
|
||||
const arr = Object.values(columns).map(exp => {
|
||||
const v = evaluate(exp, obj, null, e => {
|
||||
await db.query(resource, filter, options, (obj) => {
|
||||
const arr = Object.values(columns).map((exp) => {
|
||||
const v = evaluate(exp, obj, null, (e) => {
|
||||
if (Array.isArray(e)) {
|
||||
if (e[0] === "PARAM") {
|
||||
if (resource === "devices") {
|
||||
if (e[1] === "Tags") {
|
||||
const tags = [];
|
||||
for (const p in obj)
|
||||
if (p.startsWith("Tags.")) tags.push(p.slice(5));
|
||||
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5)));
|
||||
|
||||
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") {
|
||||
if (e[1] === "DATE_STRING") {
|
||||
if (e[2] && !Array.isArray(e[2]))
|
||||
return new Date(e[2]).toISOString();
|
||||
return new Date(e[2]).toJSON();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,65 +333,78 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
logger.accessInfo(log);
|
||||
});
|
||||
|
||||
router.head(`/${resource}/:id`, async (ctx, next) => {
|
||||
router.head(`/${resource}/:id`, async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const log = {
|
||||
message: `Count ${resource}`,
|
||||
context: ctx,
|
||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
|
||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
|
||||
};
|
||||
|
||||
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
||||
const filter = and(authorizer.getFilter(resource, 2), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS[resource]],
|
||||
ctx.params.id,
|
||||
]);
|
||||
if (!authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const res = await db.query(resource, filter);
|
||||
|
||||
if (!res.length) return void next();
|
||||
if (!res.length) return void (ctx.status = 404);
|
||||
|
||||
logger.accessInfo(log);
|
||||
ctx.body = "";
|
||||
});
|
||||
|
||||
router.get(`/${resource}/:id`, async (ctx, next) => {
|
||||
router.get(`/${resource}/:id`, async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const log = {
|
||||
message: `Query ${resource}`,
|
||||
context: ctx,
|
||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`
|
||||
filter: `${RESOURCE_IDS[resource]} = "${ctx.params.id}"`,
|
||||
};
|
||||
|
||||
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
|
||||
if (!ctx.state.authorizer.hasAccess(resource, 2)) {
|
||||
const filter = and(authorizer.getFilter(resource, 2), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS[resource]],
|
||||
ctx.params.id,
|
||||
]);
|
||||
if (!authorizer.hasAccess(resource, 2)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const res = await db.query(resource, filter);
|
||||
|
||||
if (!res.length) return void next();
|
||||
if (!res.length) return void (ctx.status = 404);
|
||||
|
||||
logger.accessInfo(log);
|
||||
ctx.body = res[0];
|
||||
});
|
||||
|
||||
// TODO add PUT, PATCH routes
|
||||
if (flags & RESOURCE_DELETE) {
|
||||
router.delete(`/${resource}/:id`, async (ctx, next) => {
|
||||
router.delete(`/${resource}/:id`, async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const log = {
|
||||
message: `Delete ${resource}`,
|
||||
context: ctx,
|
||||
id: ctx.params.id
|
||||
id: ctx.params.id,
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
const filter = ["=", ["PARAM", RESOURCE_IDS[resource]], ctx.params.id];
|
||||
const filter = and(authorizer.getFilter(resource, 3), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS[resource]],
|
||||
ctx.params.id,
|
||||
]);
|
||||
if (!authorizer.hasAccess(resource, 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
const res = await db.query(resource, filter);
|
||||
if (!res.length) return void next();
|
||||
if (!res.length) return void (ctx.status = 404);
|
||||
|
||||
const validate = authorizer.getValidator(resource, res[0]);
|
||||
if (!validate("delete")) {
|
||||
@@ -359,19 +421,19 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
}
|
||||
|
||||
if (flags & RESOURCE_PUT) {
|
||||
router.put(`/${resource}/:id`, async (ctx, next) => {
|
||||
router.put(`/${resource}/:id`, async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const id = ctx.params.id;
|
||||
|
||||
const log = {
|
||||
message: `Put ${resource}`,
|
||||
context: ctx,
|
||||
id: id
|
||||
id: id,
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
if (!authorizer.hasAccess(resource, 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const obj = ctx.request.body;
|
||||
@@ -398,7 +460,38 @@ for (const [resource, flags] of Object.entries(resources)) {
|
||||
}
|
||||
}
|
||||
|
||||
router.put("/files/:id", async (ctx, next) => {
|
||||
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) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const resource = "files";
|
||||
const id = ctx.params.id;
|
||||
|
||||
@@ -406,20 +499,20 @@ router.put("/files/:id", async (ctx, next) => {
|
||||
message: `Upload ${resource}`,
|
||||
context: ctx,
|
||||
id: id,
|
||||
metadata: null
|
||||
metadata: null,
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
if (!authorizer.hasAccess(resource, 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
fileType: ctx.request.headers["metadata.filetype"] || "",
|
||||
oui: ctx.request.headers["metadata.oui"] || "",
|
||||
productClass: ctx.request.headers["metadata.productclass"] || "",
|
||||
version: ctx.request.headers["metadata.version"] || ""
|
||||
fileType: singleParam(ctx.request.headers["metadata-filetype"]) || "",
|
||||
oui: singleParam(ctx.request.headers["metadata-oui"]) || "",
|
||||
productClass:
|
||||
singleParam(ctx.request.headers["metadata-productclass"]) || "",
|
||||
version: singleParam(ctx.request.headers["metadata-version"]) || "",
|
||||
};
|
||||
|
||||
const validate = authorizer.getValidator(resource, metadata);
|
||||
@@ -428,6 +521,12 @@ router.put("/files/:id", async (ctx, next) => {
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
try {
|
||||
await db.deleteFile(id);
|
||||
} catch (err) {
|
||||
// File doesn't exist, ignore
|
||||
}
|
||||
|
||||
await db.putFile(id, metadata, ctx.req);
|
||||
log.metadata = metadata;
|
||||
logger.accessInfo(log);
|
||||
@@ -435,24 +534,29 @@ router.put("/files/:id", async (ctx, next) => {
|
||||
ctx.body = "";
|
||||
});
|
||||
|
||||
router.post("/devices/:id/tasks", async (ctx, next) => {
|
||||
router.post("/devices/:id/tasks", async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const log = {
|
||||
message: "Commit tasks",
|
||||
context: ctx,
|
||||
deviceId: ctx.params.id,
|
||||
tasks: null
|
||||
tasks: null,
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
|
||||
const filter = and(authorizer.getFilter("devices", 3), [
|
||||
"=",
|
||||
["PARAM", "DeviceID.ID"],
|
||||
ctx.params.id,
|
||||
]);
|
||||
if (!authorizer.hasAccess("devices", 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
const devices = await db.query("devices", filter);
|
||||
if (!devices.length) return void next();
|
||||
if (!devices.length) return void (ctx.status = 404);
|
||||
const device = devices[0];
|
||||
|
||||
const validate = authorizer.getValidator("devices", devices[0]);
|
||||
const validate = authorizer.getValidator("devices", device);
|
||||
for (const t of ctx.request.body) {
|
||||
if (!validate("task", t)) {
|
||||
logUnauthorizedWarning(log);
|
||||
@@ -460,19 +564,45 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Pass context and timestamp
|
||||
const onlineThreshold = getConfig(
|
||||
ctx.state.configSnapshot,
|
||||
"cwmp.deviceOnlineThreshold"
|
||||
);
|
||||
"cwmp.deviceOnlineThreshold",
|
||||
{},
|
||||
Date.now(),
|
||||
(exp) => {
|
||||
if (!Array.isArray(exp)) return exp;
|
||||
if (exp[0] === "PARAM") {
|
||||
const p = device[exp[1]];
|
||||
if (p?.value) return p.value[0];
|
||||
} else if (exp[0] === "FUNC") {
|
||||
if (exp[1] === "REMOTE_ADDRESS") {
|
||||
for (const root of ["InternetGatewayDevice", "Device"]) {
|
||||
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
|
||||
if (p?.value) return new URL(p.value[0]).hostname;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
) as number;
|
||||
|
||||
const socketTimeout: number = ctx.socket["timeout"];
|
||||
|
||||
// Disable socket timeout while waiting for postTasks()
|
||||
if (socketTimeout) ctx.socket.setTimeout(0);
|
||||
|
||||
const res = await apiFunctions.postTasks(
|
||||
ctx.params.id,
|
||||
ctx.request.body,
|
||||
onlineThreshold
|
||||
onlineThreshold,
|
||||
device
|
||||
);
|
||||
|
||||
log.tasks = res.tasks.map(t => t._id).join(",");
|
||||
// Restore socket timeout
|
||||
if (socketTimeout) ctx.socket.setTimeout(socketTimeout);
|
||||
|
||||
log.tasks = res.tasks.map((t) => t._id).join(",");
|
||||
|
||||
logger.accessInfo(log);
|
||||
|
||||
@@ -480,22 +610,26 @@ router.post("/devices/:id/tasks", async (ctx, next) => {
|
||||
ctx.body = res.tasks;
|
||||
});
|
||||
|
||||
router.post("/devices/:id/tags", async (ctx, next) => {
|
||||
router.post("/devices/:id/tags", async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const log = {
|
||||
message: "Update tags",
|
||||
context: ctx,
|
||||
deviceId: ctx.params.id,
|
||||
tags: ctx.request.body
|
||||
tags: ctx.request.body,
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
const filter = ["=", ["PARAM", "DeviceID.ID"], ctx.params.id];
|
||||
const filter = and(authorizer.getFilter("devices", 3), [
|
||||
"=",
|
||||
["PARAM", "DeviceID.ID"],
|
||||
ctx.params.id,
|
||||
]);
|
||||
if (!authorizer.hasAccess("devices", 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
const res = await db.query("devices", filter);
|
||||
if (!res.length) return void next();
|
||||
if (!res.length) return void (ctx.status = 404);
|
||||
|
||||
const validate = authorizer.getValidator("devices", res[0]);
|
||||
if (!validate("tags", ctx.request.body)) {
|
||||
@@ -517,30 +651,33 @@ router.post("/devices/:id/tags", async (ctx, next) => {
|
||||
ctx.body = "";
|
||||
});
|
||||
|
||||
router.get("/ping/:host", async ctx => {
|
||||
try {
|
||||
ctx.body = await apiFunctions.ping(ctx.params.host);
|
||||
} catch (err) {
|
||||
ctx.status = 404;
|
||||
ctx.response.headers["Cache-Control"] = "no-cache";
|
||||
ctx.body = `${err.name}: ${err.message}`;
|
||||
}
|
||||
router.get("/ping/:host", async (ctx) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
ping(ctx.params.host, (err, parsed) => {
|
||||
if (parsed) {
|
||||
ctx.body = parsed;
|
||||
} else {
|
||||
ctx.status = 500;
|
||||
ctx.body = err ? `${err.name}: ${err.message}` : "Unknown error";
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.put("/users/:id/password", async (ctx, next) => {
|
||||
router.put("/users/:id/password", async (ctx) => {
|
||||
const authorizer: Authorizer = ctx.state.authorizer;
|
||||
const username = ctx.params.id;
|
||||
const log = {
|
||||
message: "Change password",
|
||||
context: ctx,
|
||||
username: username
|
||||
username: username,
|
||||
};
|
||||
|
||||
const authorizer = ctx.state.authorizer;
|
||||
|
||||
if (!ctx.state.user) {
|
||||
// User not logged in
|
||||
if (
|
||||
!(await apiFunctions.authSimple(
|
||||
!(await apiFunctions.authLocal(
|
||||
ctx.state.configSnapshot,
|
||||
username,
|
||||
ctx.request.body.authPassword
|
||||
@@ -553,12 +690,16 @@ router.put("/users/:id/password", async (ctx, next) => {
|
||||
}
|
||||
} else if (!authorizer.hasAccess("users", 3)) {
|
||||
logUnauthorizedWarning(log);
|
||||
return void next();
|
||||
return void (ctx.status = 403);
|
||||
}
|
||||
|
||||
const filter = ["=", ["PARAM", RESOURCE_IDS.users], username];
|
||||
const filter = and(authorizer.getFilter("users", 3), [
|
||||
"=",
|
||||
["PARAM", RESOURCE_IDS.users],
|
||||
username,
|
||||
]);
|
||||
const res = await db.query("users", filter);
|
||||
if (!res.length) return void next();
|
||||
if (!res.length) return void (ctx.status = 404);
|
||||
|
||||
const newPassword = ctx.request.body.newPassword;
|
||||
if (ctx.state.user) {
|
||||
|
||||
+223
-266
@@ -17,89 +17,46 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { MongoClient, GridFSBucket } from "mongodb";
|
||||
import { Db, GridFSBucket, ObjectId } from "mongodb";
|
||||
import { Script } from "vm";
|
||||
import * as config from "../config";
|
||||
import { onConnect } from "../db";
|
||||
import * as mongodbFunctions from "../mongodb-functions";
|
||||
import * as expression from "../common/expression";
|
||||
import { QueryOptions } from "../types";
|
||||
|
||||
const CACHE_TTL = 300000;
|
||||
|
||||
let _clientPromise = null;
|
||||
import { QueryOptions, Expression } from "../types";
|
||||
import { Readable, Writable } from "stream";
|
||||
import { minimize } from "../common/boolean-expression";
|
||||
|
||||
const RESOURCE_COLLECTION = {
|
||||
files: "fs.files"
|
||||
files: "fs.files",
|
||||
};
|
||||
|
||||
function ensureIndexes(client): void {
|
||||
client
|
||||
.db()
|
||||
.collection("cache")
|
||||
.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
|
||||
}
|
||||
let db: Db;
|
||||
|
||||
function getClient(): Promise<MongoClient> {
|
||||
if (!_clientPromise) {
|
||||
_clientPromise = new Promise((resolve, reject) => {
|
||||
const CONNECTION_URL = "" + config.get("MONGODB_CONNECTION_URL");
|
||||
MongoClient.connect(
|
||||
CONNECTION_URL,
|
||||
{ useNewUrlParser: true },
|
||||
(err, client) => {
|
||||
if (err) return void reject(err);
|
||||
ensureIndexes(client);
|
||||
resolve(client);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return _clientPromise;
|
||||
}
|
||||
|
||||
export function cache<T>(key, valueGetter: () => Promise<T>, ttl): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getClient()
|
||||
.then(client => {
|
||||
const collection = client.db().collection("cache");
|
||||
collection.findOne({ _id: key }, (err, doc) => {
|
||||
if (err) return void reject(err);
|
||||
if (doc != null) return void resolve(JSON.parse(doc.value) as T);
|
||||
valueGetter()
|
||||
.then(res => {
|
||||
const expire = Date.now() + (ttl || CACHE_TTL);
|
||||
const cacheDoc = {
|
||||
_id: key,
|
||||
value: JSON.stringify(res),
|
||||
expire: new Date(expire)
|
||||
};
|
||||
collection.updateOne(
|
||||
{ _id: key },
|
||||
{ $set: cacheDoc },
|
||||
{ upsert: true },
|
||||
err => {
|
||||
if (err) reject(err);
|
||||
else resolve(res);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
onConnect(async (_db) => {
|
||||
db = _db;
|
||||
});
|
||||
|
||||
export function query(
|
||||
resource,
|
||||
filter,
|
||||
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(
|
||||
resource: string,
|
||||
filter: Expression,
|
||||
options?: QueryOptions,
|
||||
callback?: (doc: any) => void
|
||||
): Promise<any[]> {
|
||||
): Promise<void | any[]> {
|
||||
options = options || {};
|
||||
let q;
|
||||
filter = expression.evaluate(filter, null, Date.now());
|
||||
filter = minimize(filter, true);
|
||||
|
||||
if (Array.isArray(filter)) {
|
||||
if (resource === "devices") {
|
||||
@@ -116,89 +73,85 @@ export function query(
|
||||
}
|
||||
|
||||
q = mongodbFunctions.filterToMongoQuery(filter);
|
||||
} else if (filter != null && !filter) {
|
||||
} else if (!filter) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getClient().then(client => {
|
||||
const collection = client
|
||||
.db()
|
||||
.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||
const cursor = collection.find(q);
|
||||
if (options.projection) {
|
||||
let projection = options.projection;
|
||||
if (resource === "devices") {
|
||||
projection = mongodbFunctions.processDeviceProjection(
|
||||
options.projection
|
||||
);
|
||||
}
|
||||
|
||||
if (resource === "presets") projection.configurations = 1;
|
||||
cursor.project(projection);
|
||||
}
|
||||
|
||||
if (resource === "users") cursor.project({ password: 0, salt: 0 });
|
||||
|
||||
if (options.skip) cursor.skip(options.skip);
|
||||
if (options.limit) cursor.limit(options.limit);
|
||||
|
||||
if (options.sort) {
|
||||
let s = Object.entries(options.sort)
|
||||
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
|
||||
.reduce(
|
||||
(obj, [k, v]) =>
|
||||
Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }),
|
||||
{}
|
||||
);
|
||||
|
||||
if (resource === "devices") s = mongodbFunctions.processDeviceSort(s);
|
||||
cursor.sort(s);
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
cursor.toArray((err, docs) => {
|
||||
if (err) return reject(err);
|
||||
if (resource === "devices")
|
||||
docs = docs.map(d => mongodbFunctions.flattenDevice(d));
|
||||
else if (resource === "faults")
|
||||
docs = docs.map(d => mongodbFunctions.flattenFault(d));
|
||||
else if (resource === "tasks")
|
||||
docs = docs.map(d => mongodbFunctions.flattenTask(d));
|
||||
else if (resource === "presets")
|
||||
docs = docs.map(d => mongodbFunctions.flattenPreset(d));
|
||||
else if (resource === "files")
|
||||
docs = docs.map(d => mongodbFunctions.flattenFile(d));
|
||||
return resolve(docs);
|
||||
});
|
||||
} else {
|
||||
cursor.forEach(
|
||||
doc => {
|
||||
if (resource === "devices")
|
||||
doc = mongodbFunctions.flattenDevice(doc);
|
||||
else if (resource === "faults")
|
||||
doc = mongodbFunctions.flattenFault(doc);
|
||||
else if (resource === "tasks")
|
||||
doc = mongodbFunctions.flattenTask(doc);
|
||||
else if (resource === "presets")
|
||||
doc = mongodbFunctions.flattenPreset(doc);
|
||||
else if (resource === "files")
|
||||
doc = mongodbFunctions.flattenFile(doc);
|
||||
callback(doc);
|
||||
},
|
||||
err => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||
const cursor = collection.find(q);
|
||||
if (options.projection) {
|
||||
let projection = options.projection;
|
||||
if (resource === "devices") {
|
||||
projection = mongodbFunctions.processDeviceProjection(
|
||||
options.projection
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (resource === "presets") projection.configurations = 1;
|
||||
cursor.project(projection);
|
||||
}
|
||||
|
||||
if (resource === "users") cursor.project({ password: 0, salt: 0 });
|
||||
|
||||
if (options.skip) cursor.skip(options.skip);
|
||||
if (options.limit) cursor.limit(options.limit);
|
||||
|
||||
if (options.sort) {
|
||||
let s = Object.entries(options.sort)
|
||||
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
|
||||
.reduce(
|
||||
(obj, [k, v]) =>
|
||||
Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }),
|
||||
{}
|
||||
);
|
||||
|
||||
if (resource === "devices") s = mongodbFunctions.processDeviceSort(s);
|
||||
cursor.sort(s);
|
||||
}
|
||||
|
||||
if (!callback) {
|
||||
cursor.toArray((err, docs) => {
|
||||
if (err) return reject(err);
|
||||
if (resource === "devices")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenDevice(d));
|
||||
else if (resource === "faults")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenFault(d));
|
||||
else if (resource === "tasks")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenTask(d));
|
||||
else if (resource === "presets")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenPreset(d));
|
||||
else if (resource === "files")
|
||||
docs = docs.map((d) => mongodbFunctions.flattenFile(d));
|
||||
return resolve(docs);
|
||||
});
|
||||
} else {
|
||||
cursor.forEach(
|
||||
(doc) => {
|
||||
if (resource === "devices") doc = mongodbFunctions.flattenDevice(doc);
|
||||
else if (resource === "faults")
|
||||
doc = mongodbFunctions.flattenFault(doc);
|
||||
else if (resource === "tasks")
|
||||
doc = mongodbFunctions.flattenTask(doc);
|
||||
else if (resource === "presets")
|
||||
doc = mongodbFunctions.flattenPreset(doc);
|
||||
else if (resource === "files")
|
||||
doc = mongodbFunctions.flattenFile(doc);
|
||||
callback(doc);
|
||||
},
|
||||
(err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function count(resource, filter): Promise<number> {
|
||||
export function count(resource: string, filter: Expression): Promise<number> {
|
||||
let q;
|
||||
filter = expression.evaluate(filter, null, Date.now());
|
||||
filter = minimize(filter, true);
|
||||
|
||||
if (Array.isArray(filter)) {
|
||||
if (resource === "devices")
|
||||
@@ -208,207 +161,211 @@ export function count(resource, filter): Promise<number> {
|
||||
else if (resource === "faults")
|
||||
filter = mongodbFunctions.processFaultsFilter(filter);
|
||||
q = mongodbFunctions.filterToMongoQuery(filter);
|
||||
} else if (filter != null && !filter) {
|
||||
} else if (!filter) {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getClient().then(client => {
|
||||
const collection = client
|
||||
.db()
|
||||
.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||
collection.find(q).count((err, c) => {
|
||||
if (err) reject(err);
|
||||
else resolve(c);
|
||||
});
|
||||
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||
collection.find(q).count((err, c) => {
|
||||
if (err) reject(err);
|
||||
else resolve(c);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function updateDeviceTags(deviceId, tags): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const add = [];
|
||||
const pull = [];
|
||||
export async function updateDeviceTags(
|
||||
deviceId: string,
|
||||
tags: Record<string, boolean>
|
||||
): Promise<void> {
|
||||
const add = [];
|
||||
const pull = [];
|
||||
|
||||
const regex = /^[0-9a-zA-Z_]+$/;
|
||||
for (let [tag, onOff] of Object.entries(tags)) {
|
||||
tag = tag.trim();
|
||||
if (onOff) {
|
||||
if (!tag.match(regex))
|
||||
return void reject(new Error(`Invalid tag '${tag}'`));
|
||||
add.push(tag);
|
||||
} else {
|
||||
pull.push(tag);
|
||||
}
|
||||
}
|
||||
getClient()
|
||||
.then(client => {
|
||||
const collection = client.db().collection("devices");
|
||||
for (let [tag, onOff] of Object.entries(tags)) {
|
||||
tag = tag.trim();
|
||||
if (onOff) add.push(tag);
|
||||
else pull.push(tag);
|
||||
}
|
||||
|
||||
const object = {};
|
||||
if (add && add.length) object["$addToSet"] = { _tags: { $each: add } };
|
||||
if (pull && pull.length) object["$pullAll"] = { _tags: pull };
|
||||
collection.updateOne({ _id: deviceId }, object, err => {
|
||||
if (err) return void reject(err);
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
const collection = db.collection("devices");
|
||||
const object = {};
|
||||
|
||||
if (add?.length) object["$addToSet"] = { _tags: { $each: add } };
|
||||
if (pull?.length) object["$pullAll"] = { _tags: pull };
|
||||
|
||||
await collection.updateOne({ _id: deviceId }, object);
|
||||
}
|
||||
|
||||
function putResource(resource, id, object): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getClient()
|
||||
.then(client => {
|
||||
const collection = client
|
||||
.db()
|
||||
.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||
collection.replaceOne({ _id: id }, object, { upsert: true }, err => {
|
||||
if (err) return void reject(err);
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||
collection.replaceOne({ _id: id }, object, { upsert: true }, (err) => {
|
||||
if (err) return void reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteResource(resource, id): Promise<void> {
|
||||
function deleteResource(
|
||||
resource: string,
|
||||
id: string | ObjectId
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getClient()
|
||||
.then(client => {
|
||||
const collection = client
|
||||
.db()
|
||||
.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||
collection.deleteOne({ _id: id }, err => {
|
||||
if (err) return void reject(err);
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
const collection = db.collection(RESOURCE_COLLECTION[resource] || resource);
|
||||
collection.deleteOne({ _id: id }, (err) => {
|
||||
if (err) return void reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function putPreset(id, object): Promise<void> {
|
||||
export function putPreset(
|
||||
id: string,
|
||||
object: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
object = mongodbFunctions.preProcessPreset(object);
|
||||
return putResource("presets", id, object);
|
||||
}
|
||||
|
||||
export function deletePreset(id): Promise<void> {
|
||||
export function deletePreset(id: string): Promise<void> {
|
||||
return deleteResource("presets", id);
|
||||
}
|
||||
|
||||
export function putProvision(id, object): Promise<void> {
|
||||
export function putProvision(
|
||||
id: string,
|
||||
object: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
if (!object.script) object.script = "";
|
||||
try {
|
||||
new Script(`"use strict";(function(){\n${object.script}\n})();`);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
|
||||
filename: id,
|
||||
lineOffset: -1,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.stack?.startsWith(`${id}:`)) {
|
||||
return Promise.reject(
|
||||
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
|
||||
);
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return putResource("provisions", id, object);
|
||||
}
|
||||
|
||||
export function deleteProvision(id): Promise<void> {
|
||||
export function deleteProvision(id: string): Promise<void> {
|
||||
return deleteResource("provisions", id);
|
||||
}
|
||||
|
||||
export function putVirtualParameter(id, object): Promise<void> {
|
||||
export function putVirtualParameter(
|
||||
id: string,
|
||||
object: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
if (!object.script) object.script = "";
|
||||
try {
|
||||
new Script(`"use strict";(function(){\n${object.script}\n})();`);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
|
||||
filename: id,
|
||||
lineOffset: -1,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.stack?.startsWith(`${id}:`)) {
|
||||
return Promise.reject(
|
||||
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`)
|
||||
);
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return putResource("virtualParameters", id, object);
|
||||
}
|
||||
|
||||
export function deleteVirtualParameter(id): Promise<void> {
|
||||
export function deleteVirtualParameter(id: string): Promise<void> {
|
||||
return deleteResource("virtualParameters", id);
|
||||
}
|
||||
|
||||
export function putConfig(id, object): Promise<void> {
|
||||
export function putConfig(
|
||||
id: string,
|
||||
object: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
return putResource("config", id, object);
|
||||
}
|
||||
|
||||
export function deleteConfig(id): Promise<void> {
|
||||
export function deleteConfig(id: string): Promise<void> {
|
||||
return deleteResource("config", id);
|
||||
}
|
||||
|
||||
export function putPermission(id, object): Promise<void> {
|
||||
export function putPermission(
|
||||
id: string,
|
||||
object: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
return putResource("permissions", id, object);
|
||||
}
|
||||
|
||||
export function deletePermission(id): Promise<void> {
|
||||
export function deletePermission(id: string): Promise<void> {
|
||||
return deleteResource("permissions", id);
|
||||
}
|
||||
|
||||
export function putUser(id, object): Promise<void> {
|
||||
export function putUser(
|
||||
id: string,
|
||||
object: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getClient()
|
||||
.then(client => {
|
||||
const collection = client.db().collection("users");
|
||||
// update instead of replace to keep the password if not set by user
|
||||
collection.updateOne(
|
||||
{ _id: id },
|
||||
{ $set: object },
|
||||
{ upsert: true },
|
||||
err => {
|
||||
if (err) return void reject(err);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(reject);
|
||||
const collection = db.collection("users");
|
||||
// update instead of replace to keep the password if not set by user
|
||||
collection.updateOne(
|
||||
{ _id: id },
|
||||
{ $set: object },
|
||||
{ upsert: true },
|
||||
(err) => {
|
||||
if (err) return void reject(err);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteUser(id): Promise<void> {
|
||||
export function deleteUser(id: string): Promise<void> {
|
||||
return deleteResource("users", id);
|
||||
}
|
||||
|
||||
export function putFile(filename, metadata, contentStream): Promise<void> {
|
||||
export function downloadFile(filename: string): Readable {
|
||||
const bucket = new GridFSBucket(db);
|
||||
return bucket.openDownloadStreamByName(filename);
|
||||
}
|
||||
|
||||
export function putFile(
|
||||
filename: string,
|
||||
metadata: Record<string, string>,
|
||||
contentStream: Readable
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getClient()
|
||||
.then(client => {
|
||||
const bucket = new GridFSBucket(client.db());
|
||||
const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
|
||||
metadata: metadata
|
||||
});
|
||||
uploadStream.on("error", reject);
|
||||
contentStream.on("error", reject);
|
||||
uploadStream.on("finish", resolve);
|
||||
contentStream.pipe(uploadStream);
|
||||
})
|
||||
.catch(reject);
|
||||
const bucket = new GridFSBucket(db);
|
||||
const uploadStream = bucket.openUploadStreamWithId(
|
||||
filename as unknown as ObjectId,
|
||||
filename,
|
||||
{
|
||||
metadata: metadata,
|
||||
}
|
||||
);
|
||||
uploadStream.on("error", reject);
|
||||
contentStream.on("error", reject);
|
||||
uploadStream.on("finish", resolve);
|
||||
contentStream.pipe(uploadStream as Writable);
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteFile(filename): Promise<void> {
|
||||
export function deleteFile(filename: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getClient()
|
||||
.then(client => {
|
||||
const bucket = new GridFSBucket(client.db());
|
||||
bucket.delete(filename, err => {
|
||||
if (err) return void reject(err);
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
const bucket = new GridFSBucket(db);
|
||||
bucket.delete(filename as any, (err) => {
|
||||
if (err) return void reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteFault(id): Promise<void> {
|
||||
export function deleteFault(id: string): Promise<void> {
|
||||
return deleteResource("faults", id);
|
||||
}
|
||||
|
||||
export function deleteTask(id): Promise<void> {
|
||||
export function deleteTask(id: ObjectId): Promise<void> {
|
||||
return deleteResource("tasks", id);
|
||||
}
|
||||
|
||||
export function disconnect(): void {
|
||||
if (_clientPromise) {
|
||||
_clientPromise.then(client => {
|
||||
client.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,10 @@ interface Revisions<V> {
|
||||
}
|
||||
|
||||
export default class VersionedMap<K, V> {
|
||||
private _sizeDiff: number[];
|
||||
private _revision: number;
|
||||
private map: Map<K, (V | symbol)[]>;
|
||||
public dirty: number;
|
||||
private declare _sizeDiff: number[];
|
||||
private declare _revision: number;
|
||||
private declare map: Map<K, (V | symbol)[]>;
|
||||
public declare dirty: number;
|
||||
|
||||
public constructor() {
|
||||
this._sizeDiff = [0];
|
||||
@@ -46,7 +46,7 @@ export default class VersionedMap<K, V> {
|
||||
return this._revision;
|
||||
}
|
||||
|
||||
public set revision(rev) {
|
||||
public set revision(rev: number) {
|
||||
for (let i = this._sizeDiff.length; i <= rev; ++i)
|
||||
this._sizeDiff[i] = this._sizeDiff[i - 1];
|
||||
|
||||
@@ -193,7 +193,7 @@ export default class VersionedMap<K, V> {
|
||||
const last = v[l];
|
||||
v.splice(revision, l - revision);
|
||||
|
||||
if (last === NONEXISTENT && !v.some(vv => vv !== NONEXISTENT))
|
||||
if (last === NONEXISTENT && !v.some((vv) => vv !== NONEXISTENT))
|
||||
this.map.delete(k);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-11
@@ -53,12 +53,13 @@ export interface Element {
|
||||
}
|
||||
|
||||
export function parseXmlDeclaration(buffer: Buffer): Attribute[] {
|
||||
for (const enc of ["utf16le", "utf8", "latin1", "ascii"]) {
|
||||
const encodings: BufferEncoding[] = ["utf16le", "utf8", "latin1", "ascii"];
|
||||
for (const enc of encodings) {
|
||||
let str = buffer.toString(enc, 0, 150);
|
||||
if (str.startsWith("<?xml")) {
|
||||
str = str.split("\n")[0].trim();
|
||||
str = str.slice(0, str.indexOf("?>"));
|
||||
try {
|
||||
return parseAttrs(str.slice(5, -2));
|
||||
return parseAttrs(str.slice(5));
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
@@ -90,7 +91,7 @@ export function parseAttrs(string: string): Attribute[] {
|
||||
name: name,
|
||||
namespace: namespace,
|
||||
localName: localName,
|
||||
value: value
|
||||
value: value,
|
||||
};
|
||||
attrs.push(e);
|
||||
name = "";
|
||||
@@ -103,10 +104,12 @@ export function parseAttrs(string: string): Attribute[] {
|
||||
continue;
|
||||
|
||||
case CHAR_COLON:
|
||||
if (state) continue;
|
||||
if (idx >= colonIdx) colonIdx = i;
|
||||
continue;
|
||||
|
||||
case CHAR_EQUAL:
|
||||
if (state) continue;
|
||||
if (name) throw new Error(`Unexpected character at ${i}`);
|
||||
name = string.slice(idx, i).trim();
|
||||
// TODO validate name
|
||||
@@ -128,8 +131,8 @@ export function parseAttrs(string: string): Attribute[] {
|
||||
return attrs;
|
||||
}
|
||||
|
||||
export function decodeEntities(string): string {
|
||||
return string.replace(/&[0-9a-z#]+;/gi, match => {
|
||||
export function decodeEntities(string: string): string {
|
||||
return string.replace(/&[0-9a-z#]+;/gi, (match) => {
|
||||
switch (match) {
|
||||
case """:
|
||||
return '"';
|
||||
@@ -161,15 +164,15 @@ export function decodeEntities(string): string {
|
||||
});
|
||||
}
|
||||
|
||||
export function encodeEntities(string): string {
|
||||
export function encodeEntities(string: string): string {
|
||||
const entities = {
|
||||
"&": "&",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"<": "<",
|
||||
">": ">"
|
||||
">": ">",
|
||||
};
|
||||
return string.replace(/[&"'<>]/g, m => entities[m]);
|
||||
return string.replace(/[&"'<>]/g, (m) => entities[m]);
|
||||
}
|
||||
|
||||
export function parseXml(string: string): Element {
|
||||
@@ -186,7 +189,7 @@ export function parseXml(string: string): Element {
|
||||
attrs: "",
|
||||
text: "",
|
||||
bodyIndex: 0,
|
||||
children: []
|
||||
children: [],
|
||||
};
|
||||
|
||||
const stack: Element[] = [root];
|
||||
@@ -330,7 +333,7 @@ export function parseXml(string: string): Element {
|
||||
: "",
|
||||
text: "",
|
||||
bodyIndex: i + 1,
|
||||
children: []
|
||||
children: [],
|
||||
};
|
||||
parent.children.push(e);
|
||||
if (!selfClosing) stack.push(e);
|
||||
|
||||
Generated
+4958
-7325
File diff suppressed because it is too large.
Load diff
+50
-53
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "genieacs",
|
||||
"version": "1.2.0-beta.0",
|
||||
"version": "1.2.7",
|
||||
"description": "A TR-069 Auto Configuration Server (ACS)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -14,74 +14,71 @@
|
||||
],
|
||||
"author": "Zaid Abdulla <zaid@genieacs.com>",
|
||||
"license": "AGPL-3.0",
|
||||
"preferGlobal": true,
|
||||
"private": true,
|
||||
"directories": {
|
||||
"bin": "bin",
|
||||
"tools": "tools"
|
||||
"bin": "bin"
|
||||
},
|
||||
"dependencies": {
|
||||
"espresso-iisojs": "^1.0.4",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"koa": "^2.7.0",
|
||||
"koa-bodyparser": "^4.2.1",
|
||||
"koa-compress": "^3.0.0",
|
||||
"koa-jwt": "^3.6.0",
|
||||
"koa-router": "^7.4.0",
|
||||
"koa": "^2.13.1",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
"koa-compress": "^5.1.0",
|
||||
"koa-jwt": "^4.0.1",
|
||||
"koa-router": "^10.1.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"later": "^1.2.0",
|
||||
"mongodb": "^3.2.6",
|
||||
"parsimmon": "^1.13.0",
|
||||
"seedrandom": "^3.0.1",
|
||||
"yaml": "^1.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"libxmljs": "^0.19.5"
|
||||
"mongodb": "^4.1.2",
|
||||
"parsimmon": "^1.18.0",
|
||||
"seedrandom": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^8.3.2",
|
||||
"@types/koa": "^2.0.49",
|
||||
"@types/koa-compress": "^2.0.9",
|
||||
"@types/koa-router": "^7.0.42",
|
||||
"@types/koa-static": "^4.0.1",
|
||||
"@types/libxmljs": "^0.18.3",
|
||||
"@types/mithril": "^2.0.0",
|
||||
"@types/mongodb": "^3.1.31",
|
||||
"@types/parsimmon": "^1.10.0",
|
||||
"@types/seedrandom": "^2.4.28",
|
||||
"@types/svgo": "^1.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^1.13.0",
|
||||
"@typescript-eslint/parser": "^1.13.0",
|
||||
"ava": "^2.2.0",
|
||||
"codemirror": "^5.48.2",
|
||||
"cssnano": "^4.1.10",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
"@rollup/plugin-commonjs": "^20.0.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.4",
|
||||
"@rollup/plugin-typescript": "^8.2.5",
|
||||
"@types/codemirror": "^5.60.2",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@types/koa-compress": "^4.0.3",
|
||||
"@types/koa-router": "^7.4.4",
|
||||
"@types/koa-static": "^4.0.2",
|
||||
"@types/mithril": "^2.0.8",
|
||||
"@types/node": "^16.9.2",
|
||||
"@types/parsimmon": "^1.10.6",
|
||||
"@types/seedrandom": "^3.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.31.1",
|
||||
"@typescript-eslint/parser": "^4.31.1",
|
||||
"ava": "^3.15.0",
|
||||
"codemirror": "^5.62.3",
|
||||
"cssnano": "^5.0.8",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"esm": "^3.2.25",
|
||||
"mithril": "^2.0.3",
|
||||
"postcss": "^7.0.17",
|
||||
"postcss-import": "^12.0.1",
|
||||
"mithril": "^2.0.4",
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-import": "^14.0.2",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"prettier": "^1.18.2",
|
||||
"rollup": "^1.17.0",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"rollup-plugin-terser": "^5.1.1",
|
||||
"rollup-plugin-typescript": "^1.0.1",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"svgo": "^1.3.0",
|
||||
"ts-node": "^8.3.0",
|
||||
"typescript": "^3.5.3",
|
||||
"webpack": "^4.38.0"
|
||||
"prettier": "^2.4.1",
|
||||
"rollup": "^2.56.3",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"sql.js": "^1.6.1",
|
||||
"svgo": "^2.6.1",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.4.3",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.14.0"
|
||||
"node": ">=12.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "ava --verbose --color",
|
||||
"lint-ln": "find bin -type f -exec sh -c 'ln -fs \"$(sh -c pwd)/$1\" \"bin/.${1#bin/}.ts\"' _ {} \\;; find tools -type f -exec sh -c 'ln -fs \"$(sh -c pwd)/$1\" \"tools/.${1#tools/}.ts\"' _ {} \\;",
|
||||
"lint-eslint": "eslint --fix \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"",
|
||||
"lint-prettier": "prettier --parser typescript --write \"bin/**/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\" \"tools/**/*.ts\"",
|
||||
"lint-eslint": "eslint --fix \"bin/*.ts\" \"lib/**/*.ts\" \"ui/**/*.ts\" \"test/**/*.ts\" \"build/**/*.ts\"",
|
||||
"lint-prettier": "prettier --prose-wrap always --write .",
|
||||
"lint-tsc": "tsc --noEmit",
|
||||
"lint": "npm run lint-ln && 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"
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -2,7 +2,7 @@ import ava from "ava";
|
||||
import { randomBytes } from "crypto";
|
||||
import * as auth from "../lib/auth";
|
||||
|
||||
ava("digest", t => {
|
||||
ava("digest", (t) => {
|
||||
const username = "test";
|
||||
const password = "test";
|
||||
const uri = "/";
|
||||
@@ -14,7 +14,7 @@ ava("digest", t => {
|
||||
const challenges = [
|
||||
`Digest realm="${realm}",nonce="${nonce}"`,
|
||||
`Digest realm="${realm}",nonce="${nonce}",qop="auth"`,
|
||||
`Digest realm="${realm}",nonce="${nonce}",qop="auth-int"`
|
||||
`Digest realm="${realm}",nonce="${nonce}",qop="auth-int"`,
|
||||
];
|
||||
|
||||
t.plan(challenges.length);
|
||||
|
||||
@@ -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";
|
||||
|
||||
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));
|
||||
}
|
||||
});
|
||||
+11
-38
@@ -1,35 +1,7 @@
|
||||
import ava from "ava";
|
||||
import * as common from "../lib/common";
|
||||
|
||||
ava("typeOf", t => {
|
||||
const cases = [
|
||||
[undefined, common.UNDEFINED_TYPE],
|
||||
[null, common.NULL_TYPE],
|
||||
[true, common.BOOLEAN_TYPE],
|
||||
[false, common.BOOLEAN_TYPE],
|
||||
[0, common.NUMBER_TYPE],
|
||||
[1, common.NUMBER_TYPE],
|
||||
[-1, common.NUMBER_TYPE],
|
||||
[0.0, common.NUMBER_TYPE],
|
||||
[1.0, common.NUMBER_TYPE],
|
||||
[1.1, common.NUMBER_TYPE],
|
||||
[-1.0, common.NUMBER_TYPE],
|
||||
[-1.1, common.NUMBER_TYPE],
|
||||
["", common.STRING_TYPE],
|
||||
["TEST", common.STRING_TYPE],
|
||||
[[], common.ARRAY_TYPE],
|
||||
[[1, 2, 3], common.ARRAY_TYPE],
|
||||
[{}, common.OBJECT_TYPE],
|
||||
[{ test: 1 }, common.OBJECT_TYPE],
|
||||
[/[^A-Za-z0-9_]/g, common.REGEXP_TYPE],
|
||||
[new RegExp("//"), common.REGEXP_TYPE],
|
||||
[new Date(), common.DATE_TYPE]
|
||||
];
|
||||
t.plan(cases.length);
|
||||
for (const c of cases) t.is(common.typeOf(c[0]), c[1]);
|
||||
});
|
||||
|
||||
ava("generateDeviceId", t => {
|
||||
ava("generateDeviceId", (t) => {
|
||||
const space = [" ", "%20"];
|
||||
const special = [";", "%3B"];
|
||||
const cases = [
|
||||
@@ -37,30 +9,31 @@ ava("generateDeviceId", t => {
|
||||
{
|
||||
ProductClass: "TestProductClass",
|
||||
OUI: "TestOUI",
|
||||
SerialNumber: "TestSerialNumber"
|
||||
SerialNumber: "TestSerialNumber",
|
||||
},
|
||||
"TestOUI-TestProductClass-TestSerialNumber"
|
||||
"TestOUI-TestProductClass-TestSerialNumber",
|
||||
],
|
||||
[
|
||||
{
|
||||
OUI: "TestOUI",
|
||||
SerialNumber: "TestSerialNumber"
|
||||
SerialNumber: "TestSerialNumber",
|
||||
},
|
||||
"TestOUI-TestSerialNumber"
|
||||
"TestOUI-TestSerialNumber",
|
||||
],
|
||||
[
|
||||
{
|
||||
OUI: `TestOUIWith${space[0]}_${special[0]}2912`,
|
||||
SerialNumber: `TestSerialNumberWith${space[0]}_${special[0]}2912`
|
||||
SerialNumber: `TestSerialNumberWith${space[0]}_${special[0]}2912`,
|
||||
},
|
||||
`TestOUIWith${space[1]}_${special[1]}2912-TestSerialNumberWith${space[1]}_${special[1]}2912`
|
||||
]
|
||||
`TestOUIWith${space[1]}_${special[1]}2912-TestSerialNumberWith${space[1]}_${special[1]}2912`,
|
||||
],
|
||||
];
|
||||
t.plan(cases.length);
|
||||
for (const c of cases) t.is(common.generateDeviceId(c[0]), c[1]);
|
||||
for (const c of cases)
|
||||
t.is(common.generateDeviceId(c[0] as Record<string, string>), c[1]);
|
||||
});
|
||||
|
||||
ava("escapeRegExp", t => {
|
||||
ava("escapeRegExp", (t) => {
|
||||
t.is(
|
||||
common.escapeRegExp("\\ ^ $ * + ? . ( ) | { } [ ]"),
|
||||
"\\\\ \\^ \\$ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]"
|
||||
|
||||
+19
-19
@@ -5,7 +5,7 @@ import PathSet from "../lib/common/path-set";
|
||||
import VersionedMap from "../lib/versioned-map";
|
||||
import { Attributes } from "../lib/types";
|
||||
|
||||
ava("getAliasDeclarations", t => {
|
||||
ava("getAliasDeclarations", (t) => {
|
||||
const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b");
|
||||
const decs = device.getAliasDeclarations(path, 99);
|
||||
|
||||
@@ -22,64 +22,64 @@ ava("getAliasDeclarations", t => {
|
||||
}
|
||||
});
|
||||
|
||||
ava("unpack", t => {
|
||||
ava("unpack", (t) => {
|
||||
const now = Date.now();
|
||||
const deviceData = {
|
||||
paths: new PathSet(),
|
||||
timestamps: new VersionedMap<Path, number>(),
|
||||
attributes: new VersionedMap<Path, Attributes>(),
|
||||
trackers: new Map<Path, { [name: string]: number }>(),
|
||||
changes: new Set<string>()
|
||||
changes: new Set<string>(),
|
||||
};
|
||||
|
||||
device.set(deviceData, Path.parse("a.1.b"), now, {
|
||||
value: [now, ["b", "xsd:string"]]
|
||||
value: [now, ["b", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.c"), now, {
|
||||
value: [now, ["c", "xsd:string"]]
|
||||
value: [now, ["c", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.1.a"), now, {
|
||||
value: [now, ["", "xsd:string"]]
|
||||
value: [now, ["", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.1.b"), now, {
|
||||
value: [now, ["b1", "xsd:string"]]
|
||||
value: [now, ["b1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.1.c"), now, {
|
||||
value: [now, ["c1", "xsd:string"]]
|
||||
value: [now, ["c1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.2.a"), now, {
|
||||
value: [now, ["", "xsd:string"]]
|
||||
value: [now, ["", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.2.b"), now, {
|
||||
value: [now, ["b2", "xsd:string"]]
|
||||
value: [now, ["b2", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.1.a.2.c"), now, {
|
||||
value: [now, ["c2", "xsd:string"]]
|
||||
value: [now, ["c2", "xsd:string"]],
|
||||
});
|
||||
|
||||
device.set(deviceData, Path.parse("a.2.b"), now, {
|
||||
value: [now, ["b", "xsd:string"]]
|
||||
value: [now, ["b", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.c"), now, {
|
||||
value: [now, ["c", "xsd:string"]]
|
||||
value: [now, ["c", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.1.a"), now, {
|
||||
value: [now, ["", "xsd:string"]]
|
||||
value: [now, ["", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.1.b"), now, {
|
||||
value: [now, ["b1", "xsd:string"]]
|
||||
value: [now, ["b1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.1.c"), now, {
|
||||
value: [now, ["c1", "xsd:string"]]
|
||||
value: [now, ["c1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.2.a"), now, {
|
||||
value: [now, ["", "xsd:string"]]
|
||||
value: [now, ["", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.2.b"), now, {
|
||||
value: [now, ["c1", "xsd:string"]]
|
||||
value: [now, ["c1", "xsd:string"]],
|
||||
});
|
||||
device.set(deviceData, Path.parse("a.2.a.2.c"), now, {
|
||||
value: [now, ["b1", "xsd:string"]]
|
||||
value: [now, ["b1", "xsd:string"]],
|
||||
});
|
||||
|
||||
let unpacked: Path[];
|
||||
|
||||
+94
-12
@@ -1,13 +1,13 @@
|
||||
import ava from "ava";
|
||||
import * as mongodbFunctions from "../lib/mongodb-functions";
|
||||
import { stringify } from "../lib/common/expression-parser";
|
||||
import { stringify, parse } from "../lib/common/expression-parser";
|
||||
|
||||
ava("mongoQueryToFilter", t => {
|
||||
ava("convertOldPrecondition", (t) => {
|
||||
const tests = [
|
||||
[{}, "true"],
|
||||
[{ test: "test" }, 'test = "test"'],
|
||||
[{ test: { $eq: "test" } }, 'test = "test"'],
|
||||
[{ test: { $ne: "test" } }, 'test <> "test"'],
|
||||
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'],
|
||||
[{ test: { $gte: "test" } }, 'test >= "test"'],
|
||||
[{ "Tags.test": true }, "Tags.test IS NOT NULL"],
|
||||
[{ "Tags.test": false }, "Tags.test IS NULL"],
|
||||
@@ -22,17 +22,21 @@ ava("mongoQueryToFilter", t => {
|
||||
[{ _tags: { $eq: "test" } }, "Tags.test IS NOT NULL"],
|
||||
[
|
||||
{ $and: [{ test: "test" }, { test: { $ne: "test" } }] },
|
||||
'test = "test" AND test <> "test"'
|
||||
'test = "test" AND (test <> "test" OR test IS NULL)',
|
||||
],
|
||||
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
|
||||
[
|
||||
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
|
||||
'test = "test" OR test <> "test"'
|
||||
'test = "test" OR (test <> "test" OR test IS NULL)',
|
||||
],
|
||||
[
|
||||
{ test: { $gte: "test1", $ne: "test2" } },
|
||||
'test >= "test1" AND test <> "test2"'
|
||||
]
|
||||
'test >= "test1" AND (test <> "test2" OR test IS NULL)',
|
||||
],
|
||||
[
|
||||
{ test: "test", test2: { $ne: "test2" } },
|
||||
'test = "test" AND (test2 <> "test2" OR test2 IS NULL)',
|
||||
],
|
||||
];
|
||||
|
||||
const shouldFailTests = [
|
||||
@@ -41,18 +45,96 @@ ava("mongoQueryToFilter", t => {
|
||||
[{ "Tags.test": { $gt: true } }, "Invalid tag query"],
|
||||
[{ _tags: [] }, "Invalid type"],
|
||||
[{ _tags: { $gt: "test" } }, "Invalid tag query"],
|
||||
[{ $nor: [] }, "Operator $nor not supported"]
|
||||
[{ $nor: [] }, "Operator $nor not supported"],
|
||||
];
|
||||
|
||||
t.plan(tests.length + 2 * shouldFailTests.length);
|
||||
for (const test of tests)
|
||||
t.is(stringify(mongodbFunctions.mongoQueryToFilter(test[0])), test[1]);
|
||||
for (const test of tests) {
|
||||
t.is(
|
||||
stringify(
|
||||
mongodbFunctions.convertOldPrecondition(
|
||||
test[0] as Record<string, unknown>
|
||||
)
|
||||
),
|
||||
test[1]
|
||||
);
|
||||
}
|
||||
|
||||
for (const test of shouldFailTests) {
|
||||
const func = (): void => {
|
||||
mongodbFunctions.mongoQueryToFilter(test[0]);
|
||||
mongodbFunctions.convertOldPrecondition(
|
||||
test[0] as Record<string, unknown>
|
||||
);
|
||||
};
|
||||
const error = t.throws(func, Error);
|
||||
const error = t.throws(func, { instanceOf: Error });
|
||||
t.is(error.message, test[1]);
|
||||
}
|
||||
});
|
||||
|
||||
ava("filterToMongoQuery", async (t) => {
|
||||
const queries: [string, Record<string, unknown>][] = [
|
||||
["true", {}],
|
||||
["Tags.tag1 = true", { _tags: "tag1" }],
|
||||
["Tags.tag1 <> false", { _tags: "tag1" }],
|
||||
["Tags.tag1 IS NULL", { _tags: { $ne: "tag1" } }],
|
||||
["Tags.tag1 = 123", { "Tags.tag1": 123 }],
|
||||
["Param1 = 'value1'", { "Param1._value": "value1" }],
|
||||
["Param1 <> 'value1'", { "Param1._value": { $nin: ["value1", null] } }],
|
||||
["Param1 > 'value'", { "Param1._value": { $gt: "value" } }],
|
||||
["Param1 IS NOT NULL", { "Param1._value": { $ne: null } }],
|
||||
["Param1 LIKE 'value'", { "Param1._value": /^value$/ }],
|
||||
["LOWER(Param1) LIKE 'value'", { "Param1._value": /^value$/i }],
|
||||
[
|
||||
"Param1 <> 'value2' OR NOT (Param2 = 'value1' OR Param1 < 'value2')",
|
||||
{
|
||||
$or: [
|
||||
{ "Param1._value": { $nin: ["value2", null] } },
|
||||
{
|
||||
$and: [
|
||||
{ "Param2._value": { $nin: ["value1", null] } },
|
||||
{ "Param1._value": { $gte: "value2" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
"Param1 <> 'value2' OR Param1 IS NULL",
|
||||
{
|
||||
$or: [
|
||||
{ "Param1._value": { $nin: ["value2", null] } },
|
||||
{ "Param1._value": null },
|
||||
],
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
for (const [expStr, expect] of queries) {
|
||||
const exp = parse(expStr);
|
||||
const query = mongodbFunctions.filterToMongoQuery(
|
||||
mongodbFunctions.processDeviceFilter(exp)
|
||||
);
|
||||
t.deepEqual(query, expect);
|
||||
}
|
||||
|
||||
const failQueries: [any, string][] = [
|
||||
["Param1 = Param2", "Invalid RHS operand of = clause"],
|
||||
["Param1 LIKE Param2", "Invalid RHS operand of LIKE clause"],
|
||||
["1 LIKE '1'", "Invalid LHS operand of LIKE clause"],
|
||||
["1 = 2", "Invalid LHS operand of = clause"],
|
||||
["1 IS NULL", "Invalid LHS operand of IS NULL clause"],
|
||||
["false", "Primitives are not valid queries"],
|
||||
["UPPER(Param1) LIKE 'value'", "Invalid RHS operand of LIKE clause"],
|
||||
];
|
||||
|
||||
for (const [expStr, err] of failQueries) {
|
||||
const exp = parse(expStr);
|
||||
t.throws(
|
||||
() =>
|
||||
mongodbFunctions.filterToMongoQuery(
|
||||
mongodbFunctions.processDeviceFilter(exp)
|
||||
),
|
||||
{ message: err }
|
||||
);
|
||||
}
|
||||
});
|
||||
+10
-10
@@ -2,7 +2,7 @@ import ava from "ava";
|
||||
import Path from "../lib/common/path";
|
||||
import PathSet from "../lib/common/path-set";
|
||||
|
||||
ava("depth", t => {
|
||||
ava("depth", (t) => {
|
||||
const pathSet = new PathSet();
|
||||
t.is(pathSet.depth, 0);
|
||||
pathSet.add(Path.parse(""));
|
||||
@@ -11,14 +11,14 @@ ava("depth", t => {
|
||||
t.is(pathSet.depth, 2);
|
||||
});
|
||||
|
||||
ava("add", t => {
|
||||
ava("add", (t) => {
|
||||
const pathSet = new PathSet();
|
||||
pathSet.add(Path.parse("a"));
|
||||
pathSet.add(Path.parse("a"));
|
||||
t.is(pathSet.find(Path.parse("a"), true, true, 99).length, 1);
|
||||
});
|
||||
|
||||
ava("get", t => {
|
||||
ava("get", (t) => {
|
||||
const pathSet = new PathSet();
|
||||
pathSet.add(Path.parse("a.*"));
|
||||
pathSet.add(Path.parse("a.a"));
|
||||
@@ -28,7 +28,7 @@ ava("get", t => {
|
||||
t.is(pathSet.get(Path.parse("*.a")), null);
|
||||
});
|
||||
|
||||
ava("find", t => {
|
||||
ava("find", (t) => {
|
||||
const pathSet = new PathSet();
|
||||
pathSet.add(Path.parse("a"));
|
||||
pathSet.add(Path.parse("a.*"));
|
||||
@@ -37,32 +37,32 @@ ava("find", t => {
|
||||
pathSet.add(Path.parse("*.*"));
|
||||
|
||||
t.deepEqual(
|
||||
pathSet.find(Path.parse(""), true, true, 1).map(p => p.toString()),
|
||||
pathSet.find(Path.parse(""), true, true, 1).map((p) => p.toString()),
|
||||
["a"]
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
pathSet.find(Path.parse(""), false, false, 2).map(p => p.toString()),
|
||||
pathSet.find(Path.parse(""), false, false, 2).map((p) => p.toString()),
|
||||
["a", "a.*", "a.a", "*.a", "*.*"]
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
pathSet.find(Path.parse("a.*"), false, false).map(p => p.toString()),
|
||||
pathSet.find(Path.parse("a.*"), false, false).map((p) => p.toString()),
|
||||
["a.*"]
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
pathSet.find(Path.parse("a.*"), false, true).map(p => p.toString()),
|
||||
pathSet.find(Path.parse("a.*"), false, true).map((p) => p.toString()),
|
||||
["a.*", "a.a"]
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
pathSet.find(Path.parse("a.*"), true, false).map(p => p.toString()),
|
||||
pathSet.find(Path.parse("a.*"), true, false).map((p) => p.toString()),
|
||||
["a.*", "*.*"]
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
pathSet.find(Path.parse("a.*"), true, true).map(p => p.toString()),
|
||||
pathSet.find(Path.parse("a.*"), true, true).map((p) => p.toString()),
|
||||
["a.*", "a.a", "*.a", "*.*"]
|
||||
);
|
||||
});
|
||||
+22
-3
@@ -1,13 +1,32 @@
|
||||
import ava from "ava";
|
||||
import Path from "../lib/common/path";
|
||||
|
||||
ava("toString", t => {
|
||||
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) => {
|
||||
const path1 = Path.parse('abc.[ abc : 123 , 123 : abc , 123: " abc "].123');
|
||||
const path2 = Path.parse('abc.[123: " abc ",abc:123,123:abc].123');
|
||||
t.is(path1.toString(), path2.toString());
|
||||
});
|
||||
|
||||
ava("slice", t => {
|
||||
ava("slice", (t) => {
|
||||
const path = Path.parse("a.*.b.[x:y].c");
|
||||
const sliced = path.slice(1, -1);
|
||||
t.is(sliced.toString(), '*.b.[x:"y"]');
|
||||
@@ -15,7 +34,7 @@ ava("slice", t => {
|
||||
t.is(sliced.wildcard, 0b1);
|
||||
});
|
||||
|
||||
ava("concat", t => {
|
||||
ava("concat", (t) => {
|
||||
const path1 = Path.parse("a");
|
||||
const path2 = Path.parse("*.[a:b]");
|
||||
const concat = path1.concat(path2);
|
||||
|
||||
+13
-13
@@ -2,10 +2,10 @@ import ava from "ava";
|
||||
import {
|
||||
parseXmlDeclaration,
|
||||
decodeEntities,
|
||||
parseXml
|
||||
parseXml,
|
||||
} from "../lib/xml-parser";
|
||||
|
||||
ava("parseXmlDeclaration", t => {
|
||||
ava("parseXmlDeclaration", (t) => {
|
||||
const buf = Buffer.from(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n<soap-env:Envelope />'
|
||||
);
|
||||
@@ -15,25 +15,25 @@ ava("parseXmlDeclaration", t => {
|
||||
name: "version",
|
||||
namespace: "",
|
||||
localName: "version",
|
||||
value: "1.0"
|
||||
value: "1.0",
|
||||
},
|
||||
{
|
||||
name: "encoding",
|
||||
namespace: "",
|
||||
localName: "encoding",
|
||||
value: "UTF-8"
|
||||
}
|
||||
value: "UTF-8",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
ava("decodeEntities", t => {
|
||||
ava("decodeEntities", (t) => {
|
||||
t.is(
|
||||
decodeEntities("&&<>"'>§��;"),
|
||||
"&&<>\"'>§𠮷;"
|
||||
);
|
||||
});
|
||||
|
||||
ava("parse", t => {
|
||||
ava("parse", (t) => {
|
||||
const xml =
|
||||
'<?xml version="1.0"?>\n<a-b:c><d f="1<g>"/><!-- comment --><h >i</h></a-b:c>';
|
||||
const parsed = parseXml(xml);
|
||||
@@ -60,7 +60,7 @@ ava("parse", t => {
|
||||
attrs: 'f="1<g>"',
|
||||
text: "",
|
||||
bodyIndex: 42,
|
||||
children: []
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "h",
|
||||
@@ -69,10 +69,10 @@ ava("parse", t => {
|
||||
attrs: "",
|
||||
text: "i",
|
||||
bodyIndex: 62,
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -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,84 +0,0 @@
|
||||
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
|
||||
|
||||
/**
|
||||
* Copyright 2013-2019 GenieACS Inc.
|
||||
*
|
||||
* This file is part of GenieACS.
|
||||
*
|
||||
* GenieACS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* GenieACS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { connect, devicesCollection } from "../lib/db";
|
||||
|
||||
const outputFilename = path.resolve(`data_model_${process.argv[2]}.json`);
|
||||
process.chdir(path.resolve(path.dirname(fs.realpathSync(__filename)), ".."));
|
||||
|
||||
const dataModel = {};
|
||||
|
||||
function recursive(object, p = ""): void {
|
||||
for (const key in object) {
|
||||
if (key[0] === "_" || !object[key]) continue;
|
||||
|
||||
const isObject = object[key]["_object"];
|
||||
const isValue = "_value" in object[key];
|
||||
|
||||
if ((isObject && isValue) || (!isObject && !isValue))
|
||||
throw new Error("Unexpected object");
|
||||
|
||||
if (isValue) {
|
||||
let value = object[key]["_value"];
|
||||
if (typeof value === "boolean") value = +value;
|
||||
|
||||
const values = [object[key]["_writable"] || false, value.toString()];
|
||||
if (object[key]["_type"]) values.push(object[key]["_type"]);
|
||||
|
||||
dataModel[`${p}${key}`] = values;
|
||||
}
|
||||
|
||||
if (isObject) {
|
||||
const values = [object[key]["_writable"] || false];
|
||||
dataModel[`${p}${key}.`] = values;
|
||||
recursive(object[key], `${p}${key}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connect()
|
||||
.then(() => {
|
||||
devicesCollection.findOne(
|
||||
{
|
||||
_id: process.argv[2]
|
||||
},
|
||||
{},
|
||||
(err, device) => {
|
||||
if (err) throw err;
|
||||
|
||||
if (!device) throw new Error("Device not found");
|
||||
|
||||
recursive(device);
|
||||
|
||||
fs.writeFile(outputFilename, JSON.stringify(dataModel, null, 2), () => {
|
||||
if (err) throw err;
|
||||
|
||||
process.stdout.write(`${outputFilename} saved\n`);
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
throw err;
|
||||
});
|
||||
+8
-11
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"esnext", "dom"
|
||||
],
|
||||
"module": "esnext",
|
||||
"lib": ["ES2019", "dom"],
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"target": "esnext",
|
||||
"target": "ES2019",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"alwaysStrict": true,
|
||||
@@ -14,11 +12,10 @@
|
||||
"allowJs": true
|
||||
},
|
||||
"include": [
|
||||
"./bin/.*.ts",
|
||||
"./lib/**/*",
|
||||
"./ui/**/*",
|
||||
"./test/**/*",
|
||||
"./build/**/*",
|
||||
"./tools/.*.ts"
|
||||
"./bin/*.ts",
|
||||
"./lib/**/*",
|
||||
"./ui/**/*",
|
||||
"./test/**/*",
|
||||
"./build/**/*"
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -22,7 +22,7 @@ import { ClosureComponent, Component } from "mithril";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: vnode => {
|
||||
view: (vnode) => {
|
||||
const active = { [vnode.attrs["page"]]: "active" };
|
||||
const tabs = [];
|
||||
|
||||
@@ -97,7 +97,7 @@ const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
return m("nav#side-menu", m("ul", tabs));
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -37,13 +37,14 @@ import * as usersPage from "./users-page";
|
||||
import Authorizer from "../lib//common/authorizer";
|
||||
import * as notifications from "./notifications";
|
||||
import { contextifyComponent } from "./components";
|
||||
import { PermissionSet, UiConfig } from "../lib/types";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
authorizer: Authorizer;
|
||||
permissionSets: any;
|
||||
permissionSets: PermissionSet[];
|
||||
username: string;
|
||||
clientConfig: any;
|
||||
clientConfig: { ui: UiConfig };
|
||||
configSnapshot: string;
|
||||
genieacsVersion: string;
|
||||
}
|
||||
@@ -58,57 +59,44 @@ const adminPages = [
|
||||
"files",
|
||||
"config",
|
||||
"users",
|
||||
"permissions"
|
||||
"permissions",
|
||||
];
|
||||
|
||||
let state;
|
||||
|
||||
let lastRenderTimestamp = 0;
|
||||
let pageVisitTimestamp = 0;
|
||||
let fulfillTimeout = null;
|
||||
|
||||
function fulfill(): void {
|
||||
clearTimeout(fulfillTimeout);
|
||||
fulfillTimeout = setTimeout(() => {
|
||||
store.fulfill(lastRenderTimestamp, pageVisitTimestamp).then(updated => {
|
||||
if (updated) m.redraw();
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function pagify(pageName, page): RouteResolver {
|
||||
const component: RouteResolver = {
|
||||
render: () => {
|
||||
lastRenderTimestamp = Date.now();
|
||||
const lastRenderTimestamp = Date.now();
|
||||
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);
|
||||
fulfill();
|
||||
const attrs = {};
|
||||
attrs["page"] = pageName;
|
||||
attrs["oncreate"] = attrs["onupdate"] = () => {
|
||||
store.fulfill(lastRenderTimestamp);
|
||||
};
|
||||
return m(layout, attrs, p);
|
||||
},
|
||||
onmatch: null
|
||||
onmatch: null,
|
||||
};
|
||||
|
||||
component.onmatch = (args, requestedPath) => {
|
||||
pageVisitTimestamp = Date.now();
|
||||
store.setTimestamp(Date.now());
|
||||
if (!page.init) {
|
||||
state = null;
|
||||
fulfill();
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
return new Promise<void>((resolve) => {
|
||||
page
|
||||
.init(args)
|
||||
.then(st => {
|
||||
.then((st) => {
|
||||
if (!st) return void m.route.set("/");
|
||||
state = st;
|
||||
resolve();
|
||||
fulfill();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
if (!window.username && err.message.indexOf("authorized") >= 0) {
|
||||
notifications.push("error", err.message);
|
||||
m.route.set("/login", { continue: requestedPath });
|
||||
@@ -132,7 +120,7 @@ function redirectAdminPage(): RouteResolver {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
return component;
|
||||
}
|
||||
@@ -154,5 +142,5 @@ m.route(document.body, "/overview", {
|
||||
"/admin/files": pagify("files", filesPage),
|
||||
"/admin/config": pagify("config", configPage),
|
||||
"/admin/users": pagify("users", usersPage),
|
||||
"/admin/permissions": pagify("permissions", permissionsPage)
|
||||
"/admin/permissions": pagify("permissions", permissionsPage),
|
||||
});
|
||||
+35
-21
@@ -17,19 +17,21 @@
|
||||
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export default class Autocomplete {
|
||||
private callback: (
|
||||
value: string,
|
||||
callback: (suggestions: { value: string; tip?: string }[]) => void
|
||||
) => void;
|
||||
private element: HTMLInputElement;
|
||||
private hideTimeout: NodeJS.Timeout;
|
||||
private visible: boolean;
|
||||
private default: string;
|
||||
private selection: number;
|
||||
private container: HTMLElement;
|
||||
type AutocompleteCallback = (
|
||||
value: string,
|
||||
callback: (suggestions: { value: string; tip?: string }[]) => void
|
||||
) => void;
|
||||
|
||||
public constructor(className, callback) {
|
||||
export default class Autocomplete {
|
||||
private declare callback: AutocompleteCallback;
|
||||
private declare element: HTMLInputElement;
|
||||
private declare hideTimeout: NodeJS.Timeout;
|
||||
private declare visible: boolean;
|
||||
private declare default: string;
|
||||
private declare selection: number;
|
||||
private declare container: HTMLElement;
|
||||
|
||||
public constructor(className: string, callback: AutocompleteCallback) {
|
||||
this.callback = callback;
|
||||
this.element = null;
|
||||
this.hideTimeout = null;
|
||||
@@ -44,16 +46,13 @@ export default class Autocomplete {
|
||||
this.container.className = className;
|
||||
}
|
||||
|
||||
public attach(el): void {
|
||||
public attach(el: HTMLInputElement): void {
|
||||
el.setAttribute("autocomplete", "off");
|
||||
|
||||
el.addEventListener("focus", () => {
|
||||
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.reposition();
|
||||
});
|
||||
|
||||
el.addEventListener("blur", () => {
|
||||
@@ -62,14 +61,15 @@ export default class Autocomplete {
|
||||
this.hide();
|
||||
});
|
||||
|
||||
el.addEventListener("keydown", e => {
|
||||
el.addEventListener("keydown", (e) => {
|
||||
if (this.element !== el) return;
|
||||
if (e.key === "Escape") {
|
||||
if (this.visible) this.hide();
|
||||
} else if (e.key === "Enter") {
|
||||
if (this.default != null) {
|
||||
el.value = this.default;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
el.dispatchEvent(new InputEvent("input"));
|
||||
this.update();
|
||||
}
|
||||
} else if (e.key === "ArrowDown") {
|
||||
@@ -91,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.pageXOffset}px`;
|
||||
this.container.style.width = `${domRect.width}px`;
|
||||
this.container.style.top = `${domRect.bottom + window.pageYOffset}px`;
|
||||
}
|
||||
|
||||
private hide(): void {
|
||||
this.container.style.opacity = "0";
|
||||
this.visible = false;
|
||||
@@ -108,7 +121,7 @@ export default class Autocomplete {
|
||||
private update(): void {
|
||||
const el = this.element;
|
||||
|
||||
this.callback(el.value, suggestions => {
|
||||
this.callback(el.value, (suggestions) => {
|
||||
if (this.element !== el) return;
|
||||
this.default = null;
|
||||
|
||||
@@ -154,9 +167,10 @@ export default class Autocomplete {
|
||||
|
||||
const t = document.createTextNode(suggestion.value);
|
||||
e.appendChild(t);
|
||||
e.addEventListener("mousedown", ev => {
|
||||
e.addEventListener("mousedown", (ev) => {
|
||||
ev.preventDefault();
|
||||
el.value = suggestion.value;
|
||||
el.dispatchEvent(new InputEvent("input"));
|
||||
if (this.element === el) this.update();
|
||||
});
|
||||
this.container.appendChild(e);
|
||||
|
||||
@@ -17,17 +17,23 @@
|
||||
* 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 { changePassword } from "./store";
|
||||
import { m } from "./components";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
interface Attrs {
|
||||
noAuth?: boolean;
|
||||
username?: string;
|
||||
onPasswordChange: () => void;
|
||||
}
|
||||
|
||||
const component: ClosureComponent<Attrs> = () => {
|
||||
return {
|
||||
view: vnode => {
|
||||
const onPasswordChange = vnode.attrs["onPasswordChange"];
|
||||
const enforceAuth = !vnode.attrs["noAuth"];
|
||||
const username = vnode.attrs["username"];
|
||||
view: (vnode) => {
|
||||
const onPasswordChange = vnode.attrs.onPasswordChange;
|
||||
const enforceAuth = !vnode.attrs.noAuth;
|
||||
const username = vnode.attrs.username;
|
||||
|
||||
if (username) vnode.state["username"] = username;
|
||||
|
||||
@@ -40,19 +46,19 @@ const component: ClosureComponent = (): Component => {
|
||||
type: "text",
|
||||
value: vnode.state["username"],
|
||||
disabled: !!username,
|
||||
oninput: e => {
|
||||
oninput: (e) => {
|
||||
vnode.state["username"] = e.target.value;
|
||||
},
|
||||
oncreate: _vnode => {
|
||||
oncreate: (_vnode) => {
|
||||
(_vnode.dom as HTMLSelectElement).focus();
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
let fields = {
|
||||
newPassword: "New password",
|
||||
confirmPassword: "Confirm password"
|
||||
confirmPassword: "Confirm password",
|
||||
};
|
||||
if (enforceAuth)
|
||||
fields = Object.assign({ authPassword: "Your password" }, fields);
|
||||
@@ -66,9 +72,9 @@ const component: ClosureComponent = (): Component => {
|
||||
name: f,
|
||||
type: "password",
|
||||
value: vnode.state[f],
|
||||
oninput: e => {
|
||||
oninput: (e) => {
|
||||
vnode.state[f] = e.target.value;
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -77,7 +83,7 @@ const component: ClosureComponent = (): Component => {
|
||||
const submit = m(
|
||||
"button.primary",
|
||||
{
|
||||
type: "submit"
|
||||
type: "submit",
|
||||
},
|
||||
"Change password"
|
||||
) as VnodeDOM;
|
||||
@@ -89,7 +95,7 @@ const component: ClosureComponent = (): Component => {
|
||||
m(
|
||||
"form",
|
||||
{
|
||||
onsubmit: e => {
|
||||
onsubmit: (e) => {
|
||||
e.redraw = false;
|
||||
e.preventDefault();
|
||||
if (
|
||||
@@ -120,19 +126,19 @@ const component: ClosureComponent = (): Component => {
|
||||
if (onPasswordChange) onPasswordChange();
|
||||
(submit.dom as HTMLFormElement).disabled = false;
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
notifications.push("error", err.message);
|
||||
(submit.dom as HTMLFormElement).disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
form
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
return m("div.put-form", children);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+43
-30
@@ -17,44 +17,57 @@
|
||||
* 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 { codeMirror } from "./dynamic-loader";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: vnode => {
|
||||
return m("textarea", {
|
||||
name: vnode.attrs["id"],
|
||||
value: vnode.attrs["value"],
|
||||
oncreate: _vnode => {
|
||||
const editor = codeMirror.fromTextArea(_vnode.dom, {
|
||||
mode: vnode.attrs["mode"],
|
||||
lineNumbers: true,
|
||||
readOnly: vnode.attrs["readOnly"],
|
||||
extraKeys: {
|
||||
"Ctrl-Enter": () => {
|
||||
if (vnode.attrs["onSubmit"])
|
||||
vnode.attrs["onSubmit"](_vnode.dom);
|
||||
},
|
||||
"Cmd-Enter": () => {
|
||||
if (vnode.attrs["onSubmit"])
|
||||
vnode.attrs["onSubmit"](_vnode.dom);
|
||||
}
|
||||
}
|
||||
});
|
||||
interface Attrs {
|
||||
id: string;
|
||||
value: string;
|
||||
mode: string;
|
||||
readOnly?: boolean;
|
||||
focus?: boolean;
|
||||
onSubmit?: (dom: Element) => void;
|
||||
onChange?: (value: string) => void;
|
||||
onReady?: (editor: Editor) => void;
|
||||
}
|
||||
|
||||
if (vnode.attrs["onChange"]) {
|
||||
editor.on("change", e => {
|
||||
vnode.attrs["onChange"](e.getValue());
|
||||
const component: ClosureComponent<Attrs> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return m("textarea", {
|
||||
name: vnode.attrs.id,
|
||||
value: vnode.attrs.value,
|
||||
oncreate: (_vnode) => {
|
||||
const editor = codeMirror.fromTextArea(
|
||||
_vnode.dom as HTMLTextAreaElement,
|
||||
{
|
||||
mode: vnode.attrs.mode,
|
||||
lineNumbers: true,
|
||||
readOnly: vnode.attrs.readOnly,
|
||||
extraKeys: {
|
||||
"Ctrl-Enter": () => {
|
||||
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
|
||||
},
|
||||
"Cmd-Enter": () => {
|
||||
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (vnode.attrs.onChange) {
|
||||
editor.on("change", (e) => {
|
||||
vnode.attrs.onChange(e.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
if (vnode.attrs["focus"]) editor.focus();
|
||||
if (vnode.attrs["onReady"]) vnode.attrs["onReady"](editor);
|
||||
}
|
||||
if (vnode.attrs.focus) editor.focus();
|
||||
if (vnode.attrs.onReady) vnode.attrs.onReady(editor);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+26
-13
@@ -22,9 +22,9 @@ import m, {
|
||||
Attributes,
|
||||
Children,
|
||||
ComponentTypes,
|
||||
Lifecycle,
|
||||
CommonAttributes,
|
||||
ClosureComponent,
|
||||
Vnode
|
||||
Vnode,
|
||||
} from "mithril";
|
||||
import parameter from "./components/parameter";
|
||||
import parameterList from "./components/parameter-list";
|
||||
@@ -39,6 +39,7 @@ import tags from "./components/tags";
|
||||
import ping from "./components/ping";
|
||||
import deviceLink from "./components/device-link";
|
||||
import longTextComponent from "./long-text-component";
|
||||
import loading from "./components/loading";
|
||||
|
||||
const comps = {
|
||||
parameter,
|
||||
@@ -53,7 +54,8 @@ const comps = {
|
||||
tags,
|
||||
ping,
|
||||
"device-link": deviceLink,
|
||||
"long-text": longTextComponent
|
||||
"long-text": longTextComponent,
|
||||
loading: loading,
|
||||
};
|
||||
|
||||
const contextifiedComponents = new WeakMap<ComponentTypes, ComponentTypes>();
|
||||
@@ -79,7 +81,7 @@ interface MC extends Static {
|
||||
<Attrs, State>(
|
||||
ctx: Attributes,
|
||||
component: ComponentTypes<Attrs, State>,
|
||||
attributes: Attrs & Lifecycle<Attrs, State> & { key?: string | number },
|
||||
attributes: Attrs & CommonAttributes<Attrs, State>,
|
||||
...args: Children[]
|
||||
): Vnode<Attrs, State>;
|
||||
};
|
||||
@@ -96,7 +98,7 @@ const M = new Proxy(m, {
|
||||
get: (target, prop) => {
|
||||
if (prop === "context") return contextFn;
|
||||
else return Reflect.get(target, prop);
|
||||
}
|
||||
},
|
||||
}) as MC;
|
||||
|
||||
function contextFn(context, ...argumentsList): Vnode {
|
||||
@@ -114,7 +116,7 @@ function applyContext(vnode, parentContext): void {
|
||||
vnodeContext.set(vnode, vc);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -125,21 +127,32 @@ export function contextifyComponent(component: ComponentTypes): ComponentTypes {
|
||||
if (typeof component !== "function") {
|
||||
c = Object.assign({}, component);
|
||||
const view = component.view;
|
||||
c.view = function(vnode) {
|
||||
c.view = function (vnode) {
|
||||
const context = vnodeContext.get(vnode) || {};
|
||||
const res = Reflect.apply(view, this, [vnode]);
|
||||
applyContext(res, context);
|
||||
return res;
|
||||
};
|
||||
} else if (!component.prototype || !component.prototype.view) {
|
||||
c = initialNode => {
|
||||
} else if (!component.prototype?.view) {
|
||||
c = (initialNode) => {
|
||||
const state = (component as ClosureComponent)(initialNode);
|
||||
const view = state.view;
|
||||
state.view = function(vnode) {
|
||||
state.view = function (vnode) {
|
||||
const context = vnodeContext.get(vnode) || {};
|
||||
const res = Reflect.apply(view, this, [vnode]);
|
||||
applyContext(res, context);
|
||||
return res;
|
||||
try {
|
||||
const res = Reflect.apply(view, this, [vnode]);
|
||||
applyContext(res, context);
|
||||
return res;
|
||||
} catch (err) {
|
||||
return m(
|
||||
"p.error",
|
||||
{
|
||||
title: "Click to print stack trace to console",
|
||||
onclick: () => console.error(err),
|
||||
},
|
||||
"Error!"
|
||||
);
|
||||
}
|
||||
};
|
||||
return state;
|
||||
};
|
||||
|
||||
+137
-89
@@ -30,126 +30,174 @@ function escapeRegExp(str): string {
|
||||
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
||||
}
|
||||
|
||||
const keysByDepth: WeakMap<Record<string, unknown>, string[][]> = new WeakMap();
|
||||
|
||||
function orderKeysByDepth(device: Record<string, unknown>): string[][] {
|
||||
if (keysByDepth.has(device)) return keysByDepth.get(device);
|
||||
const res: string[][] = [];
|
||||
for (const key of Object.keys(device)) {
|
||||
let count = 0;
|
||||
for (
|
||||
let i = key.lastIndexOf(".", key.length - 2);
|
||||
i >= 0;
|
||||
i = key.lastIndexOf(".", i - 1)
|
||||
)
|
||||
++count;
|
||||
while (res.length <= count) res.push([]);
|
||||
res[count].push(key);
|
||||
}
|
||||
keysByDepth.set(device, res);
|
||||
return res;
|
||||
}
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: vnode => {
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs["device"];
|
||||
|
||||
const limit = vnode.attrs["limit"] > 0 ? vnode.attrs["limit"] : 100;
|
||||
|
||||
const search = m("input", {
|
||||
type: "text",
|
||||
placeholder: "Search parameters",
|
||||
oninput: e => {
|
||||
oninput: (e) => {
|
||||
vnode.state["searchString"] = e.target.value;
|
||||
e.redraw = false;
|
||||
clearTimeout(vnode.state["timeout"]);
|
||||
vnode.state["timeout"] = setTimeout(m.redraw, 500);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const instanceRegex = /\.[0-9]+$/;
|
||||
let re;
|
||||
if (vnode.state["searchString"]) {
|
||||
const keywords = vnode.state["searchString"].split(" ").filter(s => s);
|
||||
const keywords = vnode.state["searchString"]
|
||||
.split(" ")
|
||||
.filter((s) => s);
|
||||
if (keywords.length)
|
||||
re = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i");
|
||||
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
|
||||
}
|
||||
|
||||
const rows = Object.keys(device)
|
||||
.sort()
|
||||
.map(k => {
|
||||
const filteredKeys: string[] = [];
|
||||
const allKeys = orderKeysByDepth(device);
|
||||
let count = 0;
|
||||
for (const keys of allKeys) {
|
||||
let c = 0;
|
||||
for (const k of keys) {
|
||||
const p = device[k];
|
||||
const val = [];
|
||||
const attrs = {};
|
||||
if (re) {
|
||||
const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k;
|
||||
if (!re.test(str)) attrs["style"] = "display: none;";
|
||||
}
|
||||
const str = p.value?.[0] ? `${k} ${p.value[0]}` : k;
|
||||
if (re && !re.test(str)) continue;
|
||||
++c;
|
||||
if (count < limit) filteredKeys.push(k);
|
||||
}
|
||||
count += c;
|
||||
}
|
||||
|
||||
if (p.object === false) {
|
||||
val.push(
|
||||
m(
|
||||
"parameter",
|
||||
Object.assign({ device: device, parameter: memoizedParse(k) })
|
||||
)
|
||||
);
|
||||
} else if (p.object && p.writable) {
|
||||
if (instanceRegex.test(k)) {
|
||||
val.push(
|
||||
m(
|
||||
"button",
|
||||
{
|
||||
title: "Delete this instance",
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "deleteObject",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
objectName: k
|
||||
});
|
||||
}
|
||||
},
|
||||
getIcon("delete-instance")
|
||||
)
|
||||
);
|
||||
} else {
|
||||
val.push(
|
||||
m(
|
||||
"button",
|
||||
{
|
||||
title: "Create a new instance",
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "addObject",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
objectName: k
|
||||
});
|
||||
}
|
||||
},
|
||||
getIcon("add-instance")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
filteredKeys.sort();
|
||||
|
||||
const rows = filteredKeys.map((k) => {
|
||||
const p = device[k];
|
||||
const val = [];
|
||||
const attrs = { key: k };
|
||||
|
||||
if (p.object === false) {
|
||||
val.push(
|
||||
m(
|
||||
"button",
|
||||
{
|
||||
title: "Refresh tree",
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "getParameterValues",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
parameterNames: [k]
|
||||
});
|
||||
}
|
||||
},
|
||||
getIcon("refresh")
|
||||
"parameter",
|
||||
Object.assign({ device: device, parameter: memoizedParse(k) })
|
||||
)
|
||||
);
|
||||
} else if (p.object && p.writable) {
|
||||
if (instanceRegex.test(k)) {
|
||||
val.push(
|
||||
m(
|
||||
"button",
|
||||
{
|
||||
title: "Delete this instance",
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "deleteObject",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
objectName: k,
|
||||
});
|
||||
},
|
||||
},
|
||||
getIcon("delete-instance")
|
||||
)
|
||||
);
|
||||
} else {
|
||||
val.push(
|
||||
m(
|
||||
"button",
|
||||
{
|
||||
title: "Create a new instance",
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "addObject",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
objectName: k,
|
||||
});
|
||||
},
|
||||
},
|
||||
getIcon("add-instance")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return m(
|
||||
"tr",
|
||||
attrs,
|
||||
m("td.left", m("long-text", { text: k })),
|
||||
m("td.right", val)
|
||||
);
|
||||
});
|
||||
val.push(
|
||||
m(
|
||||
"button",
|
||||
{
|
||||
title: "Refresh tree",
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "getParameterValues",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
parameterNames: [k],
|
||||
});
|
||||
},
|
||||
},
|
||||
getIcon("refresh")
|
||||
)
|
||||
);
|
||||
|
||||
return m(
|
||||
"tr",
|
||||
attrs,
|
||||
m("td.left", m("long-text", { text: k })),
|
||||
m("td.right", val)
|
||||
);
|
||||
});
|
||||
|
||||
return m(
|
||||
".all-parameters",
|
||||
"loading",
|
||||
{ queries: [vnode.attrs["deviceQuery"]] },
|
||||
m(
|
||||
"a.download-csv",
|
||||
{
|
||||
href: `/api/devices/${device["DeviceID.ID"].value[0]}.csv`,
|
||||
download: "",
|
||||
style: "float: right;"
|
||||
},
|
||||
"Download"
|
||||
),
|
||||
search,
|
||||
m(".parameter-list", m("table", m("tbody", rows)))
|
||||
".all-parameters",
|
||||
m(
|
||||
"a.download-csv",
|
||||
{
|
||||
href: `api/devices/${encodeURIComponent(
|
||||
device["DeviceID.ID"].value[0]
|
||||
)}.csv`,
|
||||
download: "",
|
||||
style: "float: right;",
|
||||
},
|
||||
"Download"
|
||||
),
|
||||
search,
|
||||
m(
|
||||
".parameter-list",
|
||||
m("table", m("tbody", rows)),
|
||||
m(
|
||||
"m",
|
||||
`Displaying ${filteredKeys.length} out of ${count} parameters.`
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import * as store from "../store";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: vnode => {
|
||||
view: (vnode) => {
|
||||
if (vnode.attrs["filter"]) {
|
||||
if (
|
||||
!store.evaluateExpression(
|
||||
@@ -34,13 +34,13 @@ const component: ClosureComponent = (): Component => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const children = Object.values(vnode.attrs["components"]).map(c => {
|
||||
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
||||
if (typeof c !== "object") return `${c}`;
|
||||
return m(c["type"], c);
|
||||
});
|
||||
if (vnode.attrs["element"]) return m(vnode.attrs["element"], children);
|
||||
else return children;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import * as store from "../store";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: vnode => {
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs["device"];
|
||||
|
||||
const buttons = [];
|
||||
@@ -38,9 +38,9 @@ const component: ClosureComponent = (): Component => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "reboot",
|
||||
device: device["DeviceID.ID"].value[0]
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
"Reboot"
|
||||
)
|
||||
@@ -54,9 +54,9 @@ const component: ClosureComponent = (): Component => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "factoryReset",
|
||||
device: device["DeviceID.ID"].value[0]
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
"Reset"
|
||||
)
|
||||
@@ -70,9 +70,9 @@ const component: ClosureComponent = (): Component => {
|
||||
onclick: () => {
|
||||
taskQueue.stageDownload({
|
||||
name: "download",
|
||||
device: device["DeviceID.ID"].value[0]
|
||||
devices: [device["DeviceID.ID"].value[0]],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
"Push file"
|
||||
)
|
||||
@@ -93,17 +93,17 @@ const component: ClosureComponent = (): Component => {
|
||||
notifications.push("success", `${deviceId}: Device deleted`);
|
||||
m.route.set("/devices");
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
notifications.push("error", err.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
"Delete"
|
||||
)
|
||||
);
|
||||
|
||||
return m(".actions-bar", buttons);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -22,25 +22,27 @@ import { m } from "../components";
|
||||
import * as store from "../store";
|
||||
import * as notifications from "../notifications";
|
||||
import { getIcon } from "../icons";
|
||||
import { stringify } from "../../lib/common/yaml";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: vnode => {
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs["device"];
|
||||
const deviceId = device["DeviceID.ID"].value[0];
|
||||
const faults = store.fetch("faults", [
|
||||
"AND",
|
||||
[">", ["PARAM", "_id"], `${deviceId}:`],
|
||||
["<", ["PARAM", "_id"], `${deviceId}:zzzz`]
|
||||
["<", ["PARAM", "_id"], `${deviceId}:zzzz`],
|
||||
]);
|
||||
|
||||
const headers = [
|
||||
"Channel",
|
||||
"Code",
|
||||
"Message",
|
||||
"Detail",
|
||||
"Retries",
|
||||
"Timestamp"
|
||||
].map(l => m("th", l));
|
||||
"Timestamp",
|
||||
].map((l) => m("th", l));
|
||||
const thead = m("thead", m("tr", headers));
|
||||
|
||||
const rows = [];
|
||||
@@ -48,7 +50,9 @@ const component: ClosureComponent = (): Component => {
|
||||
rows.push([
|
||||
m("td", f["channel"]),
|
||||
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", new Date(f["timestamp"]).toLocaleString()),
|
||||
m(
|
||||
@@ -57,30 +61,33 @@ const component: ClosureComponent = (): Component => {
|
||||
"button",
|
||||
{
|
||||
title: "Delete fault",
|
||||
onclick: e => {
|
||||
onclick: (e) => {
|
||||
e.redraw = false;
|
||||
store
|
||||
.deleteResource("faults", f["_id"])
|
||||
.then(() => {
|
||||
notifications.push("success", "Fault deleted");
|
||||
store.fulfill(Date.now(), Date.now());
|
||||
store.setTimestamp(Date.now());
|
||||
m.redraw();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
notifications.push("error", err.message);
|
||||
store.fulfill(Date.now(), Date.now());
|
||||
store.setTimestamp(Date.now());
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
getIcon("remove")
|
||||
)
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
let tbody;
|
||||
if (rows.length) {
|
||||
tbody = m("tbody", rows.map(r => m("tr", r)));
|
||||
tbody = m(
|
||||
"tbody",
|
||||
rows.map((r) => m("tr", r))
|
||||
);
|
||||
} else {
|
||||
tbody = m(
|
||||
"tbody",
|
||||
@@ -88,8 +95,12 @@ const component: ClosureComponent = (): Component => {
|
||||
);
|
||||
}
|
||||
|
||||
return m("table.table", thead, tbody);
|
||||
}
|
||||
return m(
|
||||
"loading",
|
||||
{ queries: [faults] },
|
||||
m("table.table", thead, tbody)
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ import { m } from "../components";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: vnode => {
|
||||
view: (vnode) => {
|
||||
let deviceId;
|
||||
if (vnode.attrs["device"])
|
||||
deviceId = vnode.attrs["device"]["DeviceID.ID"].value[0];
|
||||
|
||||
const children = Object.values(vnode.attrs["components"]).map(c => {
|
||||
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
||||
if (typeof c !== "object") return `${c}`;
|
||||
const attrs = Object.assign({}, vnode.attrs, c);
|
||||
return m(attrs["type"], attrs);
|
||||
@@ -41,7 +41,7 @@ const component: ClosureComponent = (): Component => {
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user