Compare commits

..
298 Commits
Author SHA1 Message Date
Zaid Humaimidi c6fe696300 1.2.10 2024-03-18 19:32:26 -07:00
Zaid Humaimidi 8d7d4de386 Changelog for v1.2.10 2024-03-18 19:29:55 -07:00
Zaid Humaimidi 3701c73b00 Update dependencies 2024-03-18 19:17:47 -07:00
Zaid Humaimidi 6e501990e0 Update author in package.json 2024-03-18 18:56:09 -07:00
Zaid Humaimidi 450fc6fe53 Fix lock expired errors 2024-03-11 11:04:49 -07:00
Divya Jagadish 6962cedba9 Fix exception caused by invalid con req URL 2024-03-09 13:14:38 -08:00
Zaid Humaimidi 227a005400 Fix exception when URL has two trailing slashes 2024-03-07 13:27:06 -08:00
Divya Jagadish 24cd3f5670 Support cert and key as string env variable 2024-03-03 13:48:00 -08:00
Divya Jagadish 052706877a Fix bug in Change Password 2024-03-02 11:31:47 -08:00
Zaid Humaimidi 867e7e768d Fix crash with certain validator expressions 2024-03-01 09:47:12 -08:00
Zaid Humaimidi 1c61504a68 Fix regression in f3068c5 2024-03-01 09:43:52 -08:00
Zaid Humaimidi effd574f4e XMPP connection request 2024-02-27 11:41:03 -08:00
Zaid Humaimidi f3068c5355 Improve pagination/sorting 2024-02-24 04:26:40 -08:00
Zaid Humaimidi f8e6e93602 Better version metadata string 2023-12-19 13:11:59 -08:00
Zaid Humaimidi e6c8d4f3f2 Build script improvements 2023-12-18 23:57:44 -08:00
Zaid Humaimidi 0dee9a5f00 Remove license headers and update imports 2023-12-18 19:13:59 -08:00
Zaid Humaimidi bfbbdb12ca Fix label issue in some components 2023-12-18 11:33:49 -08:00
Zaid Humaimidi ddb240d542 Migrate from Ava test framework to node:test 2023-12-17 22:26:52 -08:00
Zaid Humaimidi 0c9c027a77 Update linting 2023-12-16 19:08:13 -08:00
Zaid Humaimidi 4f2dfb4f48 Drop Rollup in favor of esbuild 2023-12-12 22:27:58 -08:00
Zaid Humaimidi 342210faf1 Utilize locks to prevent certain edge case issues 2023-12-06 11:10:23 -08:00
Zaid Humaimidi f4596db9f7 Remove caching of tasks, faults and operations
No longer seems to provide significant performance benefit to justify the
drawback of the added code complexity and the additional DB write query.
2023-11-23 10:34:47 -08:00
Zaid Humaimidi d122b224b0 Resolve issues from DB lock collection migration 2023-11-16 15:58:26 -08:00
Zaid Humaimidi 7a142a4c08 Close log streams on process shutdown 2023-11-14 01:41:25 -08:00
Zaid Humaimidi 4cb143cc0a Encode event codes containing illegal chars 2023-11-13 15:18:24 -08:00
Zaid Humaimidi 8d11e1cdd9 Support conditional requests in genieacs-fs 2023-11-13 13:25:29 -08:00
Zaid Humaimidi f7e75caf2c Support Range header in genieacs-fs 2023-11-10 15:15:24 -08:00
Divya Jagadish 02665faa9f Move locks to their own DB collection 2023-10-08 12:14:06 -07:00
Zaid Humaimidi c3954c277f Refactor db and local-cache code
Separated cwmp and ui code into distinct files to enhance clarity and
maintainability with varied internal data representations.
2023-09-29 14:55:44 -07:00
Clément Hampaï 493f1d2f37 Support HEAD query on fs endpoint
Some CPEs query the FS endpoint with a `HEAD` request first before downloading
the image using a `GET`.
2023-08-30 18:14:59 -07:00
Zaid Humaimidi 39bb937f9d Fix oversight in commit 58488eb 2023-08-29 12:20:53 -07:00
Zaid Humaimidi 35b193f058 Fix bug in handling of 9005 faults 2023-08-28 18:21:19 -07:00
Zaid Humaimidi 4b0d1786dd Strip extraneous properties from task objects 2023-08-28 18:19:23 -07:00
Zaid Humaimidi ea956fbc9c 404 when updating tags of non-existing devices 2023-08-28 18:17:22 -07:00
Zaid Humaimidi a0ad6ec3f1 Fix crash with certain validator expressions 2023-08-28 18:16:40 -07:00
Zaid Humaimidi 32dda9f194 Add indexable smart query types 2023-08-14 23:45:37 -07:00
Divya Jagadish c7022a2735 Add show option in permissions page 2023-08-11 15:37:43 -07:00
Zaid Humaimidi a54b2c8cd6 Handle unhandledRejection events
Because in Nodejs 12-14 unhandled rejections don't trigger uncaughtException
event by default so the process doesn't exit in case of errors.
2023-08-09 19:51:40 -07:00
Zaid Humaimidi 7b8a49a7ba Refactor request listeners to use async functions 2023-08-09 19:51:40 -07:00
Zaid Humaimidi 58488ebc3d Refactor DB code to use promises 2023-08-09 19:51:38 -07:00
Zaid Humaimidi 3169e829e7 Refactor expression to Mongo query translation
The previous approach was simplistic and could result in inefficient queries
in certain edge cases, notably causing MongoDB to not utilize existing indexes
when querying by last inform timestamps given upper and lower bounds. The new
implementation leverages our existing expression minimizer code and can
therefore generate more efficient queries or at least queries that can take
advantage of existing indexes.
2023-08-09 19:48:49 -07:00
Zaid Humaimidi 947f96299d Prevent premature close error in small files 2023-04-19 12:01:53 -07:00
Dan Morphis ee8dc3ae12 Update installation-guide.rst
Remove the JWT line from the env file and add a line to automatically generate and append a secure JWT secret.
2023-01-22 17:41:55 -08:00
Zaid Abdulla d1c01e0848 Fix FS drops connection if download is too slow
Only happens in HTTPS because seemingly somewhere in the stack up to
50mb of data is being buffered potentially triggering a timeout at a
higher level if not drained fast enough.

This commit also includes other streams related improvements.
2022-11-01 09:13:36 -07:00
Oliver Kraitschy d53e10f62d Fix port in FS URL example 2022-10-30 10:21:35 -07:00
Zaid Abdulla 444183da0a 1.2.9 2022-08-22 09:51:55 -07:00
Zaid Abdulla c02f667623 Changelog for v1.2.9 2022-08-22 09:51:25 -07:00
Zaid Abdulla 25ea0c8b64 Update dependencies 2022-08-22 09:44:25 -07:00
Zaid Abdulla 2789961596 Skip invalid timestamps in declare() 2022-08-12 16:30:00 -07:00
Zaid Abdulla d91be02599 Fix exception matching IPv6 against IPv4 CIDR 2022-08-12 11:32:15 -07:00
Zaid Abdulla 5806fe2df6 Better locking in local cache
Fixes occassional lock expired errors after updating presets, etc.
2022-08-12 01:11:04 -07:00
Zaid Abdulla eb5caa6b5a Skip root GPN workaround 2022-08-01 11:32:05 -07:00
Zaid Abdulla ffbcf2bbc0 Fix Mongo query bug when using <> operator 2022-07-15 11:36:15 -07:00
Zaid Abdulla 6f29267266 Fix HTML Quirks mode issues 2022-06-29 11:07:42 -07:00
Zaid Abdulla b0d50b07e7 Fix synthesized expressions being non-minimal
Let's try this again. The implementation of canRaise callback was
overly complicated and was causing the minimizer to ocassionally
get stuck in a local optima.
2022-06-14 11:23:33 -07:00
Zaid Abdulla c06538f9cc Fix browser quirks mode warning 2022-04-08 13:14:27 -07:00
Divya Jagadish 3d0ac9b415 Uses debounce function for parameter search 2022-04-08 13:09:04 -07:00
Divya Jagadish dabf1b2f43 Improve sorting behavior by adding delay 2022-04-08 13:08:41 -07:00
Divya Jagadish 7941d929d2 Fix ping bug on certain platforms 2022-04-08 13:01:05 -07:00
Divya Jagadish 7ccdbace70 Authenticate ping API 2022-04-08 12:59:32 -07:00
Zaid Abdulla 344f0ec5e0 Fix DB update calls containing unchanged params 2022-03-11 11:43:49 -08:00
Zaid Abdulla 4b533d0aad Fix issues caused by invalid xsd:dateTime values
Invalid dateTime strings were saved in the DB as NaN causing uncaught
exception in CSV download.
2022-03-11 11:42:10 -08:00
Zaid Abdulla 2b37f11e2e Clean up MongoDB query projection
This prevents potential 'path collision' errors from MongoDB.
2022-02-17 18:14:53 -08:00
Zaid Abdulla adaf801d6e Stream query results as they become available 2021-11-29 23:36:27 -08:00
Zaid Abdulla 94756e463f Pass BROTLI_OPERATION_FLUSH to koa-compress 2021-11-29 23:11:20 -08:00
Zaid Abdulla 3c55306e22 Fix false warning message 2021-11-15 15:42:33 -08:00
Zaid Abdulla aa12b7c724 Fix query unpacking breaking $regex operator
If the regex string can be parsed as a date or a number, the unpacked
query will be rejected by MongoDB because $regex operator only accepts
a string.
2021-11-08 14:21:14 -08:00
Zaid Abdulla 8f827997cd Fix tags set via script saved in encoded form 2021-11-08 13:55:30 -08:00
Zaid Abdulla c9a4be0475 Deubg log for HTTP and HTTPS client errors 2021-11-08 12:21:02 -08:00
Zaid Abdulla fa95d7a310 1.2.8 2021-10-27 13:07:03 -07:00
Zaid Abdulla d299ef3a59 Changelog for v1.2.8 2021-10-27 13:06:37 -07:00
Zaid Abdulla 2c606b268e Update dependencies 2021-10-27 13:04:34 -07:00
Zaid Abdulla 1b86f41ad7 Replace later dependency with @breejs/later 2021-10-26 17:40:17 -07:00
Zaid Abdulla c4754a9b55 Strip dev deps from new lock file format 2021-10-26 10:33:33 -07:00
Zaid Abdulla f24111ea9f Support modulo operator in expressions 2021-10-25 16:51:46 -07:00
Zaid Abdulla 8f662fb152 Fix expr parser treating - and / as commutative 2021-10-25 13:40:48 -07:00
Zaid Abdulla dd722ba6e0 Improve sorting toggle behavior
Last sorted column now takes precedence over any previously sorted
columns. This makes for a more intuitive user experience.
2021-10-25 13:36:23 -07:00
Zaid Abdulla 88a84788a5 Evaluate component attrs before use 2021-10-24 18:17:03 -07:00
Zaid Abdulla 73baffc3b9 Improve container component
Allow passing custom attribute to the HTML element. Implement function
encodeUriComponent for attribute expressions.
2021-10-24 18:14:36 -07:00
Zaid Abdulla 0188ac860b Handle unsupported ACS methods gracefully 2021-10-15 16:21:08 -07:00
Zaid Abdulla 7f295beeec Validate host arg passed to ping
Fixes remote code execution vulnerability reported by Alex Hordijk.
2021-10-15 16:21:08 -07:00
Zaid Abdulla 2ac536bf8f Fix ui.pageSize regression 2021-09-22 09:27:11 -07:00
Zaid Abdulla f63e357b29 Fix synthesized expressions being non-minimal 2021-09-20 12:41:09 -07:00
Zaid Abdulla ceec3f2cc8 1.2.7 2021-09-18 14:36:19 -07:00
Zaid Abdulla 2b60feb2f4 Changelog for v1.2.7 2021-09-18 14:35:34 -07:00
Zaid Abdulla 9203417686 Fix a couple of regressions in 1.2.6 2021-09-18 14:34:11 -07:00
Zaid Abdulla 8477cd1cfa 1.2.6 2021-09-16 14:25:41 -07:00
Zaid Abdulla 12c958dc03 Changelog for v1.2.6 2021-09-16 14:25:20 -07:00
Zaid Abdulla fdb81b7a43 Update dependencies 2021-09-16 14:17:39 -07:00
Zaid Abdulla 529386a9b6 Prevent invalid YAML config from bricking the UI 2021-09-16 13:27:37 -07:00
Zaid Abdulla 98698b775c Update copyright notice in README.md 2021-09-16 13:27:37 -07:00
Zaid Abdulla 9706026ea5 Update Prettier 2021-09-16 13:27:37 -07:00
Zaid Abdulla 9e1d7eca9b Rename WLAN hosts to LAN hosts in default config 2021-09-16 13:27:37 -07:00
Zaid Abdulla d48deaaf47 Catch JSON parse exceptions in NBI 2021-09-16 13:27:37 -07:00
Zaid Abdulla ec770b4e1b Fix redundant SPV triggered for unknown types
Fix issue where declarations against parameters with non-standard value
type trigger an SPV call when the value being declared is a boolean or
a number.
2021-09-16 13:27:37 -07:00
Zaid Abdulla c3c7ab72b2 Utilize ?. operator and set TypeScript target 2021-09-16 13:27:37 -07:00
Zaid Abdulla ca83704958 Ignore session cookie on Inform
Some devices preserve cookies set in a previous session and pass them
on to the next session. The presence of a session cookie in the Inform
request causes an unnecessary DB query and, in some edge cases, an
Invalid Session error.
2021-09-16 13:27:37 -07:00
Zaid Abdulla bc2d258f9c Refactor bool exp synthesizer
It's now more intelligent about generating IS NULL clauses. For example,
instead of 'LOWER(param) IS NULL' it now generates 'param IS NULL'.
2021-09-16 13:27:37 -07:00
Zaid Abdulla 7ae3bcdc0f Fix escape char not interpreted in expressions 2021-09-16 00:17:21 -07:00
Zaid Abdulla 53bb4d3552 Fix filters resetting user input on redraw 2021-09-16 00:17:21 -07:00
Zaid Abdulla 595f0eddf1 Fix deprecations 2021-09-16 00:17:21 -07:00
Zaid Abdulla 0861b2db40 Update TypeScript 2021-09-16 00:17:21 -07:00
Zaid Abdulla f4692faed6 Update MongoDB driver 2021-09-16 00:17:19 -07:00
Zaid Abdulla cc885e7d04 Update mithril types 2021-08-13 14:36:59 -07:00
Michael Ducharme cd8d264f03 Fix typo in default provision 2021-08-11 14:14:09 -07:00
JonasGhost 1e10d394bb Default to dual stack interface binding 2021-08-11 13:24:22 -07:00
Zaid Abdulla 359eca7bbc Fix upload metadata missing when behind nginx 2021-08-11 13:08:27 -07:00
Zaid Abdulla 088e25cbb0 New config option cwmp.downloadSuccessOnTimeout
Workaround for CPEs that neglect to send TransferComplete message after
a successful download.
2021-08-11 12:27:51 -07:00
Zaid Abdulla 1d02893692 Fix repeating downloads if timestamp > now 2021-08-05 01:15:27 -07:00
Zaid Abdulla da01793ac5 Fix inability to close unsupported preset notif 2021-08-02 14:32:32 -07:00
Zaid Abdulla cce85e48ed Fix crash in NBI when device not found 2021-08-02 14:04:06 -07:00
Zaid Abdulla be90214bf9 Update min Node version in installation guide 2021-08-02 13:14:04 -07:00
Zaid Abdulla 6ce91df127 Display progress bar when uploading file 2021-07-27 14:18:35 -07:00
Zaid Abdulla 1a53a320da Detect and correct for client side clock skew 2021-07-25 12:48:42 -07:00
Zaid Abdulla cfe279b511 Tolerate responses with invalid/missing params 2021-07-24 23:18:51 -07:00
Zaid Abdulla 08cfe4171c Make parsing param paths strict 2021-07-16 13:20:07 -07:00
Zaid Abdulla 6c37e88ab1 Fix crash when CPE sends invalid auth header 2021-07-13 18:45:23 -07:00
Zaid Abdulla 8b27794cc7 Fix crash when passing invalid attrs to declare 2021-07-13 17:00:59 -07:00
Zaid Abdulla 9cf18503ee Fix exceptions from ext showing as timeout error 2021-07-13 15:34:38 -07:00
Zaid Abdulla 3b36cefbca Fix UDP conn request promise rejection warning 2021-06-21 18:17:08 -07:00
Zaid Abdulla 02e09dfdab Add dot to list of encoded chars in tags 2021-06-14 14:06:05 -07:00
Zaid Abdulla 63a75cc765 Fix bug causing lock expired exceptions 2021-06-09 16:32:12 -07:00
Zaid Abdulla 630697a0cb Ignore UDP connection request errors 2021-04-18 11:24:27 -07:00
Zaid Abdulla f6aa444a23 Ignore invalid UDPConnectionRequestAddress 2021-04-17 23:37:40 -07:00
Zaid Abdulla 139dc5410e Don't crash when ping result can't be parsed 2021-04-01 18:54:44 -07:00
Zaid Abdulla e4582695b7 1.2.5 2021-03-12 15:36:19 -08:00
Zaid Abdulla 0064c2b951 Changelog for v1.2.5 2021-03-12 15:36:05 -08:00
Zaid Abdulla 47c2d6ee6f Update dependencies 2021-03-12 15:30:21 -08:00
Zaid Abdulla f290ce696f Support custom types for uploaded files 2021-03-12 15:30:13 -08:00
Zaid Abdulla 556424d4d6 Fix missing padding at bottom of page in Chrome 2021-03-12 13:08:45 -08:00
Zaid Abdulla 7e507125c4 Remove --unsafe-perm as it's no longer necessary 2021-03-10 16:56:45 -08:00
Zaid Abdulla 2ca0caa258 Reimplement foldString without regex
Lookbehind in regex is not supported in Safari browsers. This implementation
should be more efficient so why not.
2021-02-26 13:22:58 -08:00
Zaid Abdulla 478a0af781 1.2.4 2021-02-24 22:46:45 -08:00
Zaid Abdulla 9eab36cd73 Changelog for v1.2.4 2021-02-24 22:45:26 -08:00
Zaid Abdulla 8f69b74a89 Update dependencies 2021-02-24 22:37:13 -08:00
Zaid Abdulla fccda8d726 Fix bug in NBI where querying files gives 404 2021-02-23 16:50:28 -08:00
Zaid Abdulla 111cb106d0 Validate task name in API 2021-02-23 13:31:32 -08:00
Zaid Abdulla 5b17d1b2ca Shell script to spell check docs 2021-02-23 13:18:40 -08:00
Zaid Abdulla 02dfe4b554 Show warning when task name not recognized 2021-02-17 17:54:21 -08:00
Zaid Abdulla 688811317c Display spinner animation during loading 2021-02-16 00:09:24 -08:00
Zaid Abdulla 4871613c67 Add missing authentication failure logging 2021-02-09 10:37:51 -08:00
Zaid Abdulla 540898de21 Add file download link in files page 2021-02-08 15:02:40 -08:00
Zaid Abdulla e9a4676f5c New function ROUND() in expressions 2021-02-07 11:29:44 -08:00
Zaid Abdulla 12197f3b2a Fault when GPVR doesn't contain expected params 2021-02-05 17:15:21 -08:00
Zaid Abdulla bdad2f9a32 Implicit fetch of 'writable' attribute
The writable attribute may not already be present when attempting to
modify parameters included in the inform message. For other parameters
the writable attrbiute is guarenteed to have already been fetched
during discovery.
2021-02-04 22:54:30 -08:00
Zaid Abdulla 2e32b5d2a6 Serialize fault detail property in CSV to YAML 2021-02-02 16:53:22 -08:00
Zaid Abdulla 51e160560c Fix handling of 'undefined' in YAML 2021-02-02 16:50:13 -08:00
Zaid Abdulla a60a212a10 Tweak cache thresholds 2021-02-02 15:32:15 -08:00
Zaid Abdulla a692dddd50 Remove dump-data-model tool
The simulator now accepts CSV files downloaded from the UI.
2021-02-01 14:09:09 -08:00
Zaid Abdulla f62a42ada4 Fix edge case bug preventing GPN on root param 2021-02-01 13:05:55 -08:00
Zaid Abdulla 97ed9797c5 Display timestamps in CSV as ISO string 2021-02-01 13:02:42 -08:00
Zaid Abdulla 405f9d6a0f Fix debug log exception on invalid session 2021-01-31 14:04:13 -08:00
Zaid Abdulla 995a232387 Use --enable-source-maps in installation guide 2021-01-31 13:22:24 -08:00
Zaid Abdulla 1e6ab38ede Display xsd:dateTime types as ISO string 2021-01-31 11:39:02 -08:00
Zaid Abdulla ca36bf1b64 Handle UI component errors gracefully 2021-01-25 16:30:47 -08:00
Zaid Abdulla 79cb0b02d0 Fix jarring outline color 2021-01-25 15:15:32 -08:00
Zaid Abdulla e3acaf925d Remove JSBI
No longer needed as most browsers have BigInt support now.
2021-01-25 14:44:58 -08:00
Zaid Abdulla 44748e5769 Remove webpack
We no longer need webpack to polyfill dynamic imports because the vast
majority of browser now support ES modules and dynamic imports.
2021-01-25 13:16:56 -08:00
Zaid Abdulla 6cca59c666 Better syntax error message when saving script 2021-01-24 14:17:31 -08:00
Zaid Abdulla 7594883404 Cache files to improve genieacs-fs performance
This also fixes a memory leak issue when the client disconnects before
completing the download. I suspect the leak had something to do with
using pipe() but I'm not sure if it's a bug in MongoDB driver or that
we were lacking proper error handling. Anyway, the pipe is no longer
leaking :)
2021-01-22 16:20:09 -08:00
Zaid Abdulla cc89fd92d7 New simple implementation for YAML serialization
Tiny in size so we don't need to fetch the 80KB yaml package for simple
YAML serializaiton needs in various pages. It's also 8x faster than
yaml package so that's a big plus for debug logging.
2021-01-21 13:03:52 -08:00
Zaid Abdulla 134ca46dde Display version number underneath the logo 2021-01-13 14:27:33 -08:00
Zaid Abdulla 03c3c456eb Specify compression level for Brotli compression
Otherwise the default max quality will be used which is extremely slow.
2021-01-13 14:21:07 -08:00
Zaid Abdulla edcee33780 Apply Prettier to config and markdown files 2021-01-07 01:29:22 -08:00
Zaid Abdulla 5571ce96b4 Fix exception in ping component
The exception happens when ConnectionRequestURL is not a valid URL.
2021-01-06 05:09:09 -08:00
Zaid Abdulla a40e4b863b Pipe stdout/stderr from ext to process log 2021-01-05 12:09:12 -08:00
Malco Dantas 80269ec6bd Fix _id in API-Reference 2021-01-02 20:35:20 -08:00
JonasGhost 5f44f6597c Fix bug IPv6 ping 2021-01-02 20:29:29 -08:00
Zaid Abdulla 13ff0724e7 Access logging for genieacs-nbi 2020-12-16 10:29:33 -08:00
Zaid Abdulla f613ba25c2 Prevent accidentally publishing source to npm 2020-12-11 14:17:33 -08:00
Zaid Abdulla c950c73e76 Add option to set limit in all-parameters comp. 2020-12-09 18:08:56 -08:00
LVoogd 8d8e122633 Documentation for CPE to ACS authentication 2020-12-09 17:47:35 -08:00
Zaid Abdulla d29448e3a7 Fix parsing XML attrs with colon, equal chars 2020-12-08 02:07:34 -08:00
Zaid Abdulla a91dbe5021 Handle multi-line XML declaration 2020-12-08 02:06:55 -08:00
Zaid Abdulla 63cd8dd2de Save fault when session terminates unsuccessfully 2020-12-08 00:27:29 -08:00
Zaid Abdulla bded263adf Refactor HTTP connection requests 2020-12-04 15:33:35 -08:00
Zaid Abdulla fa1e40e370 Read session lock to detect task completion 2020-11-30 18:11:51 -08:00
Zaid Abdulla 30bea8ac57 Save CPE changes in case of session error/timeout 2020-11-19 14:18:05 -08:00
Zaid Abdulla d787f66e3f Reimplement locking w/o relying on hostInfo cmd
Because the hostInfo MongoDB command may not be allowed in some
environments. Also, in sharded deployments the clock skew may differ
between different shards so using hostInfo is not enough. This new
implementation simply assumes the clocks are in sync within a certain
range and throws an error if clocks are found to be out of sync.
2020-11-18 15:06:49 -08:00
Zaid Abdulla a9b0e3a1f7 Use a single MongoDB client 2020-11-18 00:01:41 -08:00
Zaid Abdulla bd55d6f512 Better support for LIKE op in query minimization 2020-11-16 15:23:38 -08:00
Zaid Abdulla e64001cddc Accept primitive expressions in filter component 2020-11-16 15:20:48 -08:00
Zaid Abdulla 60d8dcb682 1.2.3 2020-10-26 23:40:41 -07:00
Zaid Abdulla e0d648ea3e Changelog for v1.2.3 2020-10-26 23:33:36 -07:00
Zaid Abdulla 6ba67c3fb6 Update dependencies 2020-10-26 23:18:37 -07:00
Zaid Abdulla 04a7c08a20 Fix typos 2020-10-26 15:29:03 -07:00
Zaid Abdulla 92091b52a2 More deprecation fixes 2020-10-26 15:28:22 -07:00
Zaid Abdulla a43bba2405 Fix deprecations 2020-10-25 23:34:05 -07:00
Zaid Abdulla c8a6eb4c72 Show confirm close alert for unsaved changes 2020-10-25 13:37:35 -07:00
Zaid Abdulla 36798cfb5f Fix confusing 404 for unauthorized requests 2020-10-24 10:24:19 -07:00
Zaid Abdulla c52ff6f6ec Support Unicode characters in tags 2020-10-20 15:44:11 -07:00
Zaid Abdulla dfab724515 Add new config cwmp.skipWritableCheck
Some CPEs incorrectly report writable parameters as non-writable. This
config option lets us bypass the check and attempt to modify the
parameter even if it reported as read-only.
2020-10-20 13:35:34 -07:00
Zaid Abdulla 4fd919ccfa Add more XML validations 2020-10-19 13:34:08 -07:00
Zaid Abdulla 62849e8081 Fix connection timeout being set incorrectly 2020-10-18 12:52:01 -07:00
Zaid Abdulla 9f4b421b59 Add Node types to dev dependencies 2020-10-17 20:09:32 -07:00
Zaid Abdulla 44a2fe11ee Fix invalid expression after minimization
In rare edge cases where IS NULL is used the minimization process can
generate an expression that's not equivilant to the original one.
2020-10-16 17:33:29 -07:00
Zaid Abdulla 91d7152ca7 1.2.2 2020-10-03 00:37:35 -07:00
Zaid Abdulla 3fccc1d8a4 Changelog for v1.2.2 2020-10-03 00:36:59 -07:00
Zaid Abdulla 5bb6f60c4d Update dependencies and fix new lint errors 2020-10-02 23:21:07 -07:00
Zaid Abdulla c2f002b95a Better ordering of config YAML 2020-10-02 14:50:17 -07:00
Zaid Abdulla c55a256d52 Show proper login failure message 2020-09-30 15:43:55 -07:00
Zaid Abdulla 3fc0b1740d Limit max page size 2020-09-28 12:47:56 -07:00
Zaid Abdulla 1956441d52 Limit number of tasks in queue 2020-09-28 12:47:15 -07:00
Zaid Abdulla 01e01dae69 Set max height for task queue 2020-09-28 12:40:55 -07:00
Zaid Abdulla f14e6ceb97 Bulk push file from device list page 2020-09-27 11:20:30 -07:00
Zaid Abdulla 82124d471e Fix invalid arguments fault in refresh preset
Happens when upgrading from v1.1 where genieacs-gui creates refresh
configuration with 'age' passed as a string.
2020-09-23 23:15:00 -07:00
Zaid Abdulla 9286bd6aa0 Fix unhandled promise rejections 2020-09-17 17:00:41 -07:00
Zaid Abdulla 328bfa03e0 Allow refreshObject task on root param 2020-09-15 12:50:02 -07:00
Zaid Abdulla de65ca0921 Fix crash when deserializing operation objects 2020-09-14 15:58:47 -07:00
Zaid Abdulla 50e9731920 1.2.1 2020-09-08 21:40:11 -07:00
Zaid Abdulla 97c817f199 Changelog for v1.2.1 2020-09-08 21:39:34 -07:00
Zaid Abdulla 58bb8fdb14 Update MongoDB driver
Fixing this vulnerability https://npmjs.com/advisories/1555
2020-09-08 21:35:56 -07:00
Zaid Abdulla a6f9e3d0de Renaming yebool to espresso-iisojs 2020-09-08 20:11:00 -07:00
Zaid Abdulla 0b60406b43 Add lint rule for unhandled promise rejections 2020-09-08 17:33:48 -07:00
Zaid Abdulla ad682d0b69 Fix deleting items not reflecting in local store 2020-09-08 11:20:02 -07:00
Zaid Abdulla c6f4f9480c Better handling of negation in filterToMongoQuery
Similar to SQL, negating a null should still be null.
2020-09-08 00:13:48 -07:00
Zaid Abdulla dd9cd01681 Fix querying faults in UI showing no results 2020-09-08 00:11:14 -07:00
Zaid Abdulla c17d2eae2f 1.2.0 2020-09-01 07:59:30 -07:00
Zaid Abdulla a8e770fea5 Changelog for v1.2.0 2020-09-01 07:59:15 -07:00
Zaid Abdulla cfa989d346 Update README.md 2020-09-01 06:55:17 -07:00
Zaid Abdulla b02c1df3b2 Update CONTRIBUTING.md 2020-09-01 06:45:09 -07:00
Zaid Abdulla dfc4ab81aa Increase width of filter input 2020-09-01 04:15:02 -07:00
Zaid Abdulla e48829a04c Update remaining dependencies 2020-09-01 04:14:14 -07:00
Zaid Abdulla e532f99625 Update build dependencies 2020-09-01 03:12:17 -07:00
Zaid Abdulla 2780e3970e Update ESLint, Prettier, TypeScript, and Ava 2020-09-01 02:59:27 -07:00
Zaid Abdulla 04b19eb05f Update Koa and related dependencies 2020-09-01 02:43:06 -07:00
Zaid Abdulla 1760207217 Update Parsimmon 2020-09-01 02:30:23 -07:00
Zaid Abdulla 495e7b0264 Update types for MongoDB driver 2020-09-01 01:58:06 -07:00
Zaid Abdulla 2411db6b36 Better handling of invalid CPE messages 2020-08-28 00:49:05 -07:00
Zaid Abdulla c9eb5d1327 Types refactoring 2020-08-25 01:02:11 -07:00
Zaid Abdulla cce525cb07 Display subset of params in all-parameters comp.
For better performance as rendering thousands of parameters can be
quite heavy.
2020-08-20 14:38:23 -07:00
Zaid Abdulla 49daf5adf2 Improve 'parameter' comp. for complex expressions 2020-08-19 21:51:12 -07:00
Zaid Abdulla 19c5bb03ea Fix exception when passing null as PARAM arg 2020-08-19 20:04:36 -07:00
Zaid Abdulla ec44ccfc07 Fix bug in authorizer 2020-08-18 12:39:46 -07:00
Zaid Abdulla bf9517c652 Implement support for Get/SetParameterAttributes 2020-08-17 19:21:13 -07:00
Zaid Abdulla 6385f54888 Allow expressions in provision arguments 2020-08-17 16:51:10 -07:00
Zaid Abdulla fef4ddfbba Fix canRaise bug wrecking expression synthesis 2020-08-17 16:34:57 -07:00
Zaid Abdulla 36046a6fac Docs fix 2020-08-17 10:10:52 -07:00
Zaid Abdulla 2a85d768a5 Implement COALESCE function 2020-08-17 10:01:45 -07:00
Zaid Abdulla 6cbd59c6ec Implement CASE operator
This also modifies the behavior of AND/OR operators to strictly return
true/false/null whereas previously they could return the value of the
first truthy/false operand. The purpose of this change is to maintain
expression equivalency before and after minimization.
2020-08-15 14:07:45 -07:00
Zaid Abdulla e3ef177842 Update MongoDB driver 2020-08-11 23:33:59 -07:00
Zaid Abdulla 00ccf99be6 Minimize store queries 2020-07-29 02:31:31 -07:00
Zaid Abdulla 03840ea7ea Update rollup and webpack 2020-07-29 01:56:27 -07:00
Zaid Abdulla ac980116bd Update ESLint and Prettier 2020-07-27 20:30:13 -07:00
Zaid Abdulla 5a9e03db2c Use relative URLs throughout the UI
To allow serving from a subdirectory using a reverse proxy.
2020-07-22 11:07:32 -07:00
Zaid Abdulla c2c24f5c24 Respect SPV related config in alias declarations 2020-06-30 11:09:58 -07:00
Zaid Abdulla e516d5338c Reject param paths containing empty segments 2020-03-13 18:30:53 -07:00
Zaid Abdulla 41f3987db4 Delete code that shouldn't have been checked in 2020-02-10 13:04:19 -08:00
Zaid Abdulla 65bf6d7d6e Fix bug if fault occurs in precondition stage 2020-02-09 21:39:45 -08:00
Zaid Abdulla 603f5c79ac Support Forwarded header 2019-12-29 14:50:08 -08:00
Zaid Abdulla e80109de93 Process inform message before authentication
This allows the authentication expression to read events and other
parameters.
2019-12-24 10:47:37 -08:00
Zaid Abdulla ce955e34c1 Allow PUT to overwrite existing files 2019-12-16 16:05:43 -08:00
Zaid Abdulla 1e6bb5f450 Add parse() and UTC() to SandboxDate 2019-12-12 10:28:45 -08:00
Zaid Abdulla f301d70339 Use session timestamp when evaluating preconditions 2019-12-06 12:54:35 -08:00
Zaid Abdulla feb3d7f244 Fix regression from a previous commit 2019-12-03 20:02:00 -08:00
Zaid Abdulla dd907a67b0 Add types in local-cache.ts 2019-12-03 14:20:34 -08:00
Zaid Abdulla 37d8222479 Memoize authorizer object 2019-12-03 14:11:07 -08:00
Zaid Abdulla a915b581cc Use configContextCallback for preconditions 2019-12-03 14:03:46 -08:00
Zaid Abdulla add4c84132 Expose all device params to config expressions 2019-11-27 17:00:03 -08:00
Zaid Abdulla 75ff56f3ae Fix vparam examples in docs 2019-11-26 15:51:45 -08:00
Zaid Abdulla f786be7493 Fix deferred declarations in vparams 2019-11-24 12:59:12 -08:00
Zaid Abdulla 07a5c96e2a Don't throw error if ping fails 2019-11-23 12:36:12 -08:00
Zaid Abdulla 612563f171 Fix regression in timeoutOperations 2019-11-19 13:13:25 -08:00
Zaid Abdulla 27fb235d20 IS NULL should check on value attribute 2019-11-13 19:07:42 -08:00
Zaid Abdulla c66739c420 Send Connection: close header on shut down 2019-11-13 19:06:37 -08:00
Zaid Abdulla 140acaa491 Remove libxmljs dependency 2019-11-07 22:45:32 -08:00
Zaid Abdulla 765f0be7bc Fix download task object validation 2019-10-31 16:10:47 -07:00
Zaid Abdulla ab27ebfaef Fix bug in mongoQueryToFilter 2019-10-29 13:01:47 -07:00
Zaid Abdulla f135be403c Tag object should not be writable 2019-10-28 19:10:43 -07:00
Zaid Abdulla c07f57af1f Missed something in previous commit 2019-10-28 19:08:50 -07:00
Zaid Abdulla df5e4cf454 Check against invalid args in default provisions 2019-10-28 18:07:33 -07:00
Zaid Abdulla 85b5e12c94 Use alphanumeric characters for con req password 2019-10-24 17:45:39 -07:00
Zaid Abdulla 8533ee7f2e UI authentication improvements 2019-10-24 17:43:24 -07:00
Zaid Abdulla 77563fc85e Move ping code to own file 2019-10-21 22:58:22 -07:00
Mirek Laus 0e02b7c7ea Fix ping on FreeBSD 2019-10-21 22:58:12 -07:00
Shweelan Samson f966194e22 Update ping component via DOM manipulation 2019-10-19 19:10:27 -07:00
Zaid Abdulla 306015f022 Fix permission filter not being applied 2019-10-11 12:41:35 -07:00
Zaid Abdulla 20857549d0 Fix download URL when SSL is enabled 2019-10-11 11:58:20 -07:00
Zaid Abdulla e31d74bd45 Docs fixes 2019-10-11 00:40:55 -07:00
gothosting 5c6d85025c Missed "/" from line 47 for genieacs-ext path
I omitted the "/" on the initial commit for the SPAWN error on extensions
2019-10-09 14:30:17 -07:00
Zaid Abdulla 209c0f7620 Fix remoteAddress workaround
Tree shaking removed the remoteAddress workaround because property access
should have no side effect. This change prevents that.
2019-09-23 22:12:32 -07:00
Zaid Abdulla 559aa0fd36 Fix filter by tag 2019-09-23 22:02:20 -07:00
Zaid Abdulla ed0aa8ae7a Fix process shutdown issues 2019-09-23 16:03:08 -07:00
Zaid Abdulla 4bcee572f1 Fix URL-encoding of download params hyperlink 2019-09-16 14:17:27 -07:00
Zaid Abdulla 3d38de74d7 Remove label from nested parameter-table 2019-09-14 15:53:44 -07:00
Zaid Abdulla 5b5346f858 Add missing event code in bootstrap preset 2019-09-12 12:34:01 -07:00
Zaid Abdulla f57bcae70c Fix regression in GPN heuristic 2019-09-12 11:16:42 -07:00
Zaid Abdulla 1c9aa493c6 Fix occasional exceptions on SIGINT and SIGTERM 2019-09-09 02:14:12 -07:00
Zaid Abdulla 6f0e1d66ce Remove CWMP_KEEP_ALIVE_TIMEOUT
This turned out not to be necessary. Will use SESSION_TIMEOUT for
connection timeout.
2019-09-05 17:18:04 -07:00
Zaid Abdulla b5efe6c578 Fix session.serialize bug 2019-09-05 17:16:21 -07:00
Shweelan Samson fd34dd8839 Return null in case of expired cache 2019-08-19 15:28:46 -07:00
Zaid Abdulla 17889cf41c Fix typo in config option name 2019-08-15 17:41:28 -07:00
Zaid Abdulla c2e8585801 Docs updates 2019-08-15 17:40:25 -07:00
Zaid Abdulla 46e009afab Docs updates 2019-08-13 23:55:50 -07:00
Gareth Bow 78eec1b5ca Fix spawn bin/genieacs-ext 2019-08-13 09:21:05 -07:00
Zaid Abdulla 59cd930b6c Support more charsets using iconv-lite 2019-08-04 12:40:02 -07:00
Zaid Abdulla db3736e60b Fix exception when logging XML parse errors 2019-08-04 12:33:48 -07:00
Zaid Abdulla d10fa2e073 Few installation guide fixes 2019-07-30 12:13:18 -07:00
167 changed files with 25653 additions and 21872 deletions

No files matched your search

-2
View File
@@ -1,2 +0,0 @@
!/bin/.*.ts
!/tools/.*.ts
+18 -9
View File
@@ -1,6 +1,6 @@
{
"env": {
"es6": true,
"es2022": true,
"node": true
},
"parser": "@typescript-eslint/parser",
@@ -8,20 +8,29 @@
"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"}],
"no-return-await": "error",
"prefer-const": ["error", { "destructuring": "all" }],
"eqeqeq": ["error", "always", { "null": "ignore" }],
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-use-before-define": ["error", { "functions": false }],
"@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }],
"require-atomic-updates": 0,
"@typescript-eslint/no-use-before-define": [
"error",
{ "functions": false }
],
"@typescript-eslint/explicit-function-return-type": [
"error",
{ "allowExpressions": true }
],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"no-prototype-builtins": 0
}
}
-2
View File
@@ -1,6 +1,4 @@
*~
node_modules
dist
bin/.*.ts
tools/.*.ts
docs/_build
-25
View File
@@ -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/.*
+2
View File
@@ -0,0 +1,2 @@
dist
npm-shrinkwrap.json
+260 -52
View File
@@ -1,48 +1,254 @@
# Change Log
## 1.2.10 (2024-03-18)
- Add support for XMPP connection requests. Use the environment variables
`XMPP_JID` and `XMPP_PASSWORD` to configure the XMPP connection for the ACS.
- The environment variables `CWMP_SSL_CERT` and `CWMP_SSL_KEY`, as well as their
counterparts for UI, NBI, and FS, now accept PEM-encoded certificates and keys
in addition to file paths.
- The UI no longer requires users to refresh the page after modifying presets,
provisions, or virtual parameters. Refreshing is now only necessary for
changes to users, permissions, or UI configurations.
- Improved conversion of GenieACS expressions into MongoDB queries for more
optimized queries and better index utilization.
- Refactor UI pagination and sorting to fix issues from the previous approach,
especially with sorting by rapidly changing parameters such as 'last inform'
time.
- The file server now supports HEAD requests, the Range header, and conditional
requests.
- Addressed an issue causing file server disconnections for slow clients over
HTTPS.
- Fix bug preventing users from changing their own passwords.
- Introduced a new 'locks' collection for database locking, replacing the
previous use of the 'cache' collection for this purpose.
- Various other minor fixes and enhancements.
## 1.2.9 (2022-08-22)
- New config option `cwmp.skipRootGpn` to enable a workaround for some
problematic CPEs that reject GPN requests on data model root.
- Stream query results and CSV downloads as data becomes available instead of
buffering the entire response.
- Log HTTP/HTTPS client errors in debug log.
- Fix occasional lock expired errors after updating presets, etc.
- Fix bug where queries containing `<>` operator may return incorrect results.
- Fix a performance hit caused by DB calls containing the entire CPE data model
rather then just the updated parameters.
- Fix bug where tags containing special characters are saved in their encoded
form when set via a Provision script.
- Fix issue causing invalid `xsd:dateTime` values to be saved in DB as `NaN`
rather than maintain the original string value.
- Fix bug using `$regex` operator with a numeric or a datetime string.
- Fix ping not working on certain platforms.
- Ping requests are now authenticated.
- Fix error when the `FORWARDED_HEADER` environment variable contains IPv4 CIDR
while the listening interface is IPv6 and vice versa.
- Fix false warning "unexpected parameter in response" showing in
`genieacs-cwmp` logs.
- Other minor fixes and stability improvements.
## 1.2.8 (2021-10-27)
- Fix a remote code execution security vulnerability in genieacs-ui.
- All UI components can now be configured using fully dynamic expressions.
Previously some component types only accept fixed string values as properties.
- The `container` UI component can now be configured with an `element` property
that's either a string or a nested pair of `tag` and `attributes` properties.
The various attributes under the `attributes` property can now make use of a
new function `ENCODEURICOMPONENT()` to facilitate creating custom hyperlinks
in the UI.
- Improve sorting buttons' behavior in the device listing page and other pages.
Sorting by multiple columns should now feel more intuitive.
- Support the modulo (%) operator in expressions.
- Fix a regression in the previous release where the config option 'ui.pageSize'
no longer works.
- Fix process crash when a CPE sends an unsupported ACS method.
## 1.2.7 (2021-09-18)
- Fix regression causing frequent invalid session errors.
- Fix regression breaking digest authentication in connection requests.
## 1.2.6 (2021-09-16)
- New config option `cwmp.downloadSuccessOnTimeout` to enable a workaround for
CPEs that neglect to send a TransferComplete message after a successful
download.
- Display a progress bar when uploading new files.
- Default to dual stack interface binding (i.e. `::` instead of `0.0.0.0`).
Unless the binding interface is explicitly set, this will cause IPv4 addresses
in the logs to be displayed as IPv4-mapped IPv6 addresses (e.g.
`::ffff:127.0.0.1` instead of `127.0.0.1`).
- Detect and correct for client side (browser) clock skew that would otherwise
alter the numbers in the pie charts.
- Various improvements relating to dealing with buggy TR-069 client
implementations.
- Fix a bug causing "lock expired" exceptions when a CWMP session remains open
for a very long time due to slow clients.
- Fix metadata of uploaded files going missing due to nginx stripping away what
it considers to be invalid headers. The nginx directive
`ignore_invalid_headers` is no longer required.
- Fix crash when a CPE is assigned a tag containing a dot.
- Fix bug preventing the user from closing the preset pop-up after being
presented with unsupported preset message.
- Fix exceptions raised from ext scripts manifesting as timeout faults.
- Fix download getting triggered repeatedly when the value passed to `Download`
parameter is greater than the current timestamp.
- Fix crash when passing invalid attributes to `declare`.
- Fix crash in NBI when pushing tasks to non-existing devices.
- Fix crash in NBI when passing invalid JSON to various API endpoints.
- Fix crash when the output from `ping` command cannot be parsed in some rare
edge cases.
- A number of other fixes and stability improvements.
## 1.2.5 (2021-03-12)
- Support specifying custom types when uploading files.
- Fix JS compatibility issue with Safari browser.
## 1.2.4 (2021-02-24)
- The data model state of a CPE is no longer forgotten after unsuccessful
session termination (e.g. timeout). This addresses a number of undesired side
effects that arise when a CPE does not terminate the session properly.
- Executing tasks that take a long time to complete (e.g. refreshing the entire
data model) no longer shows a timeout error while the task is still being
processed.
- New function `ROUND()` available to expressions. It works similar to the
function by the same name in SQLite and PostgreSQL.
- Log access events for `genieacs-nbi` service.
- Pipe stdout/stderr from extension scripts to the `genieacs-cwmp` process log.
- Parameter values of type `xsd:dateTime` are now displayed in the UI and CSV
downloads as a date string rather than a numeric value.
- Add file download link in the files listing page.
- Display spinner loading animation throughout the UI.
- Display GenieACS version and build number underneath the logo.
- New option to specify how many parameters are displayed at a time in the
all-parameters component. Simply set `limit` property in the component config.
- Reduce overly strong Brotli compression level which was causing significant
page load slowdown when Brotli is used.
- Retire dump-data-model tool. `genieacs-sim` can now use a CSV file as its data
model.
- Reduce the number of concurrent database connections from each process.
- Remove dependency on 'hostInfo' MongoDB command which is a privileged action.
It is now possible to use shared MongoDB instances with limited privileged.
- Fix bug in NBI where querying files returns 404 error.
- Fix ping not working for devices with an IPv6 address.
- Fix an elusive memory leak in `genieacs-fs` that slowly eats up memory and can
go unnoticed for long periods of time.
- Fix a rare edge case where a `declare()` call to set a parameter value may not
work as intended if the parameter was originally received as part of the
Inform message.
- A number of other fixes and stability improvements.
## 1.2.3 (2020-10-26)
- New config option 'cwmp.skipWritableCheck' for when some CPEs incorrectly
report writable parameters as non-writable. When set to true, the scripts will
no longer respect the 'writable' attribute of the CPE parameters and will send
a SetParamteerValues, AddObject, or DeleteObject request anyway.
- Tags no longer restrict what characters are allowed. Any character other than
alphanumeric characters, hyphen, or underscore is now encoded in the data
model (i.e. Tags.\<tag>) using its hex value preceded by "0x".
- Ask for a confirmation before closing a pop-up dialog with unsaved changes.
- Better XML validation to avoid crashes caused by invalid CPE requests.
- Fix confusing 404 error message when the user attempts to modify a resource
when they don't have the necessary permissions.
- Fix a rare issue where genieacs-cwmp stops accepting new connections after
running for a few weeks.
- Fix exception when IS NULL operator is used in certain situations.
## 1.2.2 (2020-10-03)
- Added button to push files to selected devices from device listing page.
- A few minor UI improvements.
- Fix exception that can happen and persist after a Download request.
- Fix validation bug preventing running refreshObject task on data model root.
- Fix invalid arguments fault in refresh preset configuration when upgrading
from v1.1.
## 1.2.1 (2020-09-08)
- Fix bug causing faults to not be displayed in the UI.
- Fix bug where deleting objects does not get reflected immediately in the UI.
- Improve conversion between filters written in the expression format and
MongoDB queries. There should now be fewer edge cases where the two are not
equisatisfiable.
## 1.2.0 (2020-09-01)
- Support GetParameterAttributes and SetParameterAttributes TR-069 methods.
- Support CASE statement and COALESCE function in expressions.
- Provision arguments can now be a list of expressions that are dynamically
evaluated.
- Support Forwarded HTTP header to display in the logs the correct IP of CPEs
behind a reverse proxy. Must be configured using FORWARDED_HEADER option.
- Config expressions can now access all available device parameters, not only
serial number, product class, and OUI.
- Use relative URLs throughout the UI to allow serving from a subdirectory using
a reverse proxy.
- Make Date.parse() and Date.UTC() available to provision scripts.
- libxmljs has been entirely removed in favor of our bespoke XML parser.
- Removed the config option CWMP_KEEP_ALIVE_TIMEOUT. SESSION_TIMEOUT is now used
to determine the TCP connection timeout.
- The all-parameters component now limits the number of parameters displayed for
better performance.
- The process genieacs-cwmp is now much less likely to throw exceptions as a
result of invalid requests from CPE.
- A large number of bug fixes and stability improvements.
## 1.2.0-beta.0 (2019-07-30)
- 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 +258,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 +328,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
View File
@@ -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
+24 -34
View File
@@ -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).
-6
View File
@@ -1,6 +0,0 @@
export default {
"compileEnhancements": false,
"extensions": ["ts"],
"require": ["esm", "ts-node/register/transpile-only"]
};
-129
View File
@@ -1,129 +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 config from "../lib/config";
import * as logger from "../lib/logger";
import * as cluster from "../lib/cluster";
import * as server from "../lib/server";
import * as cwmp from "../lib/cwmp";
import * as db from "../lib/db";
import * as extensions from "../lib/extensions";
import * as cache from "../lib/cache";
import { version as VERSION } from "../package.json";
logger.init("cwmp", VERSION);
const SERVICE_ADDRESS = config.get("CWMP_INTERFACE");
const SERVICE_PORT = config.get("CWMP_PORT");
function exitWorkerGracefully(): void {
db.disconnect();
cache.disconnect();
extensions.killAll();
cluster.worker.disconnect();
}
function exitWorkerUngracefully(): void {
extensions.killAll().then(() => {
process.exit(1);
});
}
if (!cluster.worker) {
const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES");
logger.info({
message: `genieacs-cwmp starting`,
pid: process.pid,
version: VERSION
});
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
process.on("SIGINT", () => {
logger.info({
message: "Received signal SIGINT, exiting",
pid: process.pid
});
cluster.stop();
});
process.on("SIGTERM", () => {
logger.info({
message: "Received signal SIGTERM, exiting",
pid: process.pid
});
cluster.stop();
});
} else {
const ssl = {
key: config.get("CWMP_SSL_KEY"),
cert: config.get("CWMP_SSL_CERT")
};
const keepAliveTimeout = config.get("CWMP_KEEP_ALIVE_TIMEOUT") as number;
process.on("uncaughtException", err => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({
message: "Uncaught exception",
exception: err,
pid: process.pid
});
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
process.on("SIGINT", () => {
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
process.on("SIGTERM", () => {
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
Promise.all([db.connect(), cache.connect()])
.then(() => {
server.start(
SERVICE_PORT,
SERVICE_ADDRESS,
ssl,
cwmp.listener,
cwmp.onConnection,
keepAliveTimeout
);
})
.catch(err => {
setTimeout(() => {
throw err;
});
});
}
+118
View File
@@ -0,0 +1,118 @@
import * as config from "../lib/config.ts";
import * as logger from "../lib/logger.ts";
import * as cluster from "../lib/cluster.ts";
import * as server from "../lib/server.ts";
import * as cwmp from "../lib/cwmp.ts";
import * as db from "../lib/db/db.ts";
import * as extensions from "../lib/extensions.ts";
import { version as VERSION } from "../package.json";
logger.init("cwmp", VERSION);
const SERVICE_ADDRESS = config.get("CWMP_INTERFACE") as string;
const SERVICE_PORT = config.get("CWMP_PORT") as number;
function exitWorkerGracefully(): void {
setTimeout(exitWorkerUngracefully, 5000).unref();
Promise.all([
db.disconnect(),
extensions.killAll(),
cluster.worker.disconnect(),
])
.then(logger.close)
.catch(exitWorkerUngracefully);
}
function exitWorkerUngracefully(): void {
void extensions.killAll().finally(() => {
process.exit(1);
});
}
if (!cluster.worker) {
const WORKER_COUNT = config.get("CWMP_WORKER_PROCESSES") as number;
logger.info({
message: `genieacs-cwmp starting`,
pid: process.pid,
version: VERSION,
});
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
process.on("SIGINT", () => {
logger.info({
message: "Received signal SIGINT, exiting",
pid: process.pid,
});
cluster.stop();
});
process.on("SIGTERM", () => {
logger.info({
message: "Received signal SIGTERM, exiting",
pid: process.pid,
});
cluster.stop();
});
} else {
const key = config.get("CWMP_SSL_KEY") as string;
const cert = config.get("CWMP_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
onConnection: cwmp.onConnection,
onClientError: cwmp.onClientError,
timeout: 30000,
keepAliveTimeout: 0,
requestTimeout: cwmp.REQUEST_TIMEOUT,
};
// Need this for Node < 15
process.on("unhandledRejection", (err) => {
throw err;
});
process.on("uncaughtException", (err) => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({
message: "Uncaught exception",
exception: err,
pid: process.pid,
});
server.stop(false).then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
const initPromise = db
.connect()
.then(() => {
server.start(options, cwmp.listener);
})
.catch((err) => {
setTimeout(() => {
throw err;
});
});
process.on("SIGINT", () => {
void initPromise.finally(() => {
server
.stop(false)
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
});
process.on("SIGTERM", () => {
void initPromise.finally(() => {
server
.stop(false)
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
});
}
+17 -31
View File
@@ -1,25 +1,4 @@
#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { Fault } from "../lib/types";
import { Fault } from "../lib/types.ts";
const jobs = new Set();
const fileName = process.argv[2];
@@ -35,20 +14,20 @@ function errorToFault(err: Error): Fault {
message: err.message,
detail: {
name: err.name,
message: err.message
}
message: err.message,
},
};
if (err.stack) {
fault.detail["stack"] = err.stack;
// Trim the stack trace
const stackTrimIndex = fault.detail["stack"].match(
/\s+at\s[^\s]+\s\(.*genieacs-ext:.+\)/
/\s+at\s[^\s]+\s\(.*genieacs-ext:.+\)/,
);
if (stackTrimIndex) {
fault.detail["stack"] = fault.detail["stack"].slice(
0,
stackTrimIndex.index
stackTrimIndex.index,
);
}
}
@@ -56,16 +35,21 @@ function errorToFault(err: Error): Fault {
return fault;
}
process.on("uncaughtException", err => {
// Need this for Node < 15
process.on("unhandledRejection", (err) => {
throw 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 +63,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 +77,6 @@ process.on("message", message => {
});
// Ignore SIGINT
process.on("SIGINT", () => {});
process.on("SIGINT", () => {
// Ignore
});
-116
View File
@@ -1,116 +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 config from "../lib/config";
import * as logger from "../lib/logger";
import * as cluster from "../lib/cluster";
import * as server from "../lib/server";
import { listener } from "../lib/fs";
import * as db from "../lib/db";
import * as cache from "../lib/cache";
import { version as VERSION } from "../package.json";
logger.init("fs", VERSION);
const SERVICE_ADDRESS = config.get("FS_INTERFACE");
const SERVICE_PORT = config.get("FS_PORT");
function exitWorkerGracefully(): void {
db.disconnect();
cache.disconnect();
cluster.worker.disconnect();
}
function exitWorkerUngracefully(): void {
process.exit(1);
}
if (!cluster.worker) {
const WORKER_COUNT = config.get("FS_WORKER_PROCESSES");
logger.info({
message: `genieacs-fs starting`,
pid: process.pid,
version: VERSION
});
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
process.on("SIGINT", () => {
logger.info({
message: "Received signal SIGINT, exiting",
pid: process.pid
});
cluster.stop();
});
process.on("SIGTERM", () => {
logger.info({
message: "Received signal SIGTERM, exiting",
pid: process.pid
});
cluster.stop();
});
} else {
const ssl = {
key: config.get("FS_SSL_KEY"),
cert: config.get("FS_SSL_CERT")
};
process.on("uncaughtException", err => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({
message: "Uncaught exception",
exception: err,
pid: process.pid
});
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
process.on("SIGINT", () => {
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
process.on("SIGTERM", () => {
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
Promise.all([db.connect(), cache.connect()])
.then(() => {
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
})
.catch(err => {
setTimeout(() => {
throw err;
});
});
}
+100
View File
@@ -0,0 +1,100 @@
import * as config from "../lib/config.ts";
import * as logger from "../lib/logger.ts";
import * as cluster from "../lib/cluster.ts";
import * as server from "../lib/server.ts";
import { listener } from "../lib/fs.ts";
import * as db from "../lib/db/db.ts";
import { version as VERSION } from "../package.json";
logger.init("fs", VERSION);
const SERVICE_ADDRESS = config.get("FS_INTERFACE") as string;
const SERVICE_PORT = config.get("FS_PORT") as number;
function exitWorkerGracefully(): void {
setTimeout(exitWorkerUngracefully, 5000).unref();
Promise.all([db.disconnect(), cluster.worker.disconnect()])
.then(logger.close)
.catch(exitWorkerUngracefully);
}
function exitWorkerUngracefully(): void {
process.exit(1);
}
if (!cluster.worker) {
const WORKER_COUNT = config.get("FS_WORKER_PROCESSES") as number;
logger.info({
message: `genieacs-fs starting`,
pid: process.pid,
version: VERSION,
});
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
process.on("SIGINT", () => {
logger.info({
message: "Received signal SIGINT, exiting",
pid: process.pid,
});
cluster.stop();
});
process.on("SIGTERM", () => {
logger.info({
message: "Received signal SIGTERM, exiting",
pid: process.pid,
});
cluster.stop();
});
} else {
const key = config.get("FS_SSL_KEY") as string;
const cert = config.get("FS_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
};
// Need this for Node < 15
process.on("unhandledRejection", (err) => {
throw err;
});
process.on("uncaughtException", (err) => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({
message: "Uncaught exception",
exception: err,
pid: process.pid,
});
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
const initPromise = db
.connect()
.then(() => {
server.start(options, listener);
})
.catch((err) => {
setTimeout(() => {
throw err;
});
});
process.on("SIGINT", () => {
void initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
});
process.on("SIGTERM", () => {
void initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
});
}
-120
View File
@@ -1,120 +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 config from "../lib/config";
import * as logger from "../lib/logger";
import * as cluster from "../lib/cluster";
import * as server from "../lib/server";
import { listener } from "../lib/nbi";
import * as db from "../lib/db";
import * as extensions from "../lib/extensions";
import * as cache from "../lib/cache";
import { version as VERSION } from "../package.json";
logger.init("nbi", VERSION);
const SERVICE_ADDRESS = config.get("NBI_INTERFACE");
const SERVICE_PORT = config.get("NBI_PORT");
function exitWorkerGracefully(): void {
db.disconnect();
cache.disconnect();
extensions.killAll();
cluster.worker.disconnect();
}
function exitWorkerUngracefully(): void {
extensions.killAll().then(() => {
process.exit(1);
});
}
if (!cluster.worker) {
const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES");
logger.info({
message: `genieacs-nbi starting`,
pid: process.pid,
version: VERSION
});
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
process.on("SIGINT", () => {
logger.info({
message: "Received signal SIGINT, exiting",
pid: process.pid
});
cluster.stop();
});
process.on("SIGTERM", () => {
logger.info({
message: "Received signal SIGTERM, exiting",
pid: process.pid
});
cluster.stop();
});
} else {
const ssl = {
key: config.get("NBI_SSL_KEY"),
cert: config.get("NBI_SSL_CERT")
};
process.on("uncaughtException", err => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({
message: "Uncaught exception",
exception: err,
pid: process.pid
});
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
process.on("SIGINT", () => {
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
process.on("SIGTERM", () => {
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
Promise.all([db.connect(), cache.connect()])
.then(() => {
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, listener);
})
.catch(err => {
setTimeout(() => {
throw err;
});
});
}
+107
View File
@@ -0,0 +1,107 @@
import * as config from "../lib/config.ts";
import * as logger from "../lib/logger.ts";
import * as cluster from "../lib/cluster.ts";
import * as server from "../lib/server.ts";
import { listener } from "../lib/nbi.ts";
import * as db from "../lib/db/db.ts";
import * as extensions from "../lib/extensions.ts";
import { version as VERSION } from "../package.json";
logger.init("nbi", VERSION);
const SERVICE_ADDRESS = config.get("NBI_INTERFACE") as string;
const SERVICE_PORT = config.get("NBI_PORT") as number;
function exitWorkerGracefully(): void {
setTimeout(exitWorkerUngracefully, 5000).unref();
Promise.all([
db.disconnect(),
extensions.killAll(),
cluster.worker.disconnect(),
])
.then(logger.close)
.catch(exitWorkerUngracefully);
}
function exitWorkerUngracefully(): void {
void extensions.killAll().finally(() => {
process.exit(1);
});
}
if (!cluster.worker) {
const WORKER_COUNT = config.get("NBI_WORKER_PROCESSES") as number;
logger.info({
message: `genieacs-nbi starting`,
pid: process.pid,
version: VERSION,
});
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
process.on("SIGINT", () => {
logger.info({
message: "Received signal SIGINT, exiting",
pid: process.pid,
});
cluster.stop();
});
process.on("SIGTERM", () => {
logger.info({
message: "Received signal SIGTERM, exiting",
pid: process.pid,
});
cluster.stop();
});
} else {
const key = config.get("NBI_SSL_KEY") as string;
const cert = config.get("NBI_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
};
// Need this for Node < 15
process.on("unhandledRejection", (err) => {
throw err;
});
process.on("uncaughtException", (err) => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({
message: "Uncaught exception",
exception: err,
pid: process.pid,
});
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
const initPromise = db
.connect()
.then(() => {
server.start(options, listener);
})
.catch((err) => {
setTimeout(() => {
throw err;
});
});
process.on("SIGINT", () => {
void initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
});
process.on("SIGTERM", () => {
void initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
});
}
-132
View File
@@ -1,132 +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 config from "../lib/config";
import * as logger from "../lib/logger";
import * as cluster from "../lib/cluster";
import * as server from "../lib/server";
import { listener } from "../lib/ui";
import * as extensions from "../lib/extensions";
import * as db from "../lib/ui/db";
import * as db2 from "../lib/db";
import * as cache from "../lib/cache";
import { version as VERSION } from "../package.json";
logger.init("ui", VERSION);
const SERVICE_ADDRESS = config.get("UI_INTERFACE");
const SERVICE_PORT = config.get("UI_PORT");
function exitWorkerGracefully(): void {
db.disconnect();
db2.disconnect();
cache.disconnect();
extensions.killAll();
cluster.worker.disconnect();
}
function exitWorkerUngracefully(): void {
extensions.killAll().then(() => {
process.exit(1);
});
}
if (!cluster.worker) {
const WORKER_COUNT = config.get("UI_WORKER_PROCESSES");
logger.info({
message: `genieacs-ui starting`,
pid: process.pid,
version: VERSION
});
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
process.on("SIGINT", () => {
logger.info({
message: "Received signal SIGINT, exiting",
pid: process.pid
});
cluster.stop();
});
process.on("SIGTERM", () => {
logger.info({
message: "Received signal SIGTERM, exiting",
pid: process.pid
});
cluster.stop();
});
} else {
const ssl = {
key: config.get("UI_SSL_KEY"),
cert: config.get("UI_SSL_CERT")
};
let stopping = false;
process.on("uncaughtException", err => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({
message: "Uncaught exception",
exception: err,
pid: process.pid
});
stopping = true;
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
process.on("SIGINT", () => {
stopping = true;
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
process.on("SIGTERM", () => {
stopping = true;
server
.stop()
.then(exitWorkerGracefully)
.catch(exitWorkerUngracefully);
});
const _listener = (req, res): void => {
if (stopping) res.setHeader("Connection", "close");
listener(req, res);
};
Promise.all([db2.connect(), cache.connect()])
.then(() => {
server.start(SERVICE_PORT, SERVICE_ADDRESS, ssl, _listener);
})
.catch(err => {
setTimeout(() => {
throw err;
});
});
}
+107
View File
@@ -0,0 +1,107 @@
import * as config from "../lib/config.ts";
import * as logger from "../lib/logger.ts";
import * as cluster from "../lib/cluster.ts";
import * as server from "../lib/server.ts";
import { listener } from "../lib/ui.ts";
import * as extensions from "../lib/extensions.ts";
import * as db from "../lib/db/db.ts";
import { version as VERSION } from "../package.json";
logger.init("ui", VERSION);
const SERVICE_ADDRESS = config.get("UI_INTERFACE") as string;
const SERVICE_PORT = config.get("UI_PORT") as number;
function exitWorkerGracefully(): void {
setTimeout(exitWorkerUngracefully, 5000).unref();
Promise.all([
db.disconnect(),
extensions.killAll(),
cluster.worker.disconnect(),
])
.then(logger.close)
.catch(exitWorkerUngracefully);
}
function exitWorkerUngracefully(): void {
void extensions.killAll().finally(() => {
process.exit(1);
});
}
if (!cluster.worker) {
const WORKER_COUNT = config.get("UI_WORKER_PROCESSES") as number;
logger.info({
message: `genieacs-ui starting`,
pid: process.pid,
version: VERSION,
});
cluster.start(WORKER_COUNT, SERVICE_PORT, SERVICE_ADDRESS);
process.on("SIGINT", () => {
logger.info({
message: "Received signal SIGINT, exiting",
pid: process.pid,
});
cluster.stop();
});
process.on("SIGTERM", () => {
logger.info({
message: "Received signal SIGTERM, exiting",
pid: process.pid,
});
cluster.stop();
});
} else {
const key = config.get("UI_SSL_KEY") as string;
const cert = config.get("UI_SSL_CERT") as string;
const options = {
port: SERVICE_PORT,
host: SERVICE_ADDRESS,
ssl: key && cert ? { key, cert } : null,
timeout: 30000,
};
// Need this for Node < 15
process.on("unhandledRejection", (err) => {
throw err;
});
process.on("uncaughtException", (err) => {
if ((err as NodeJS.ErrnoException).code === "ERR_IPC_DISCONNECTED") return;
logger.error({
message: "Uncaught exception",
exception: err,
pid: process.pid,
});
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
const initPromise = db
.connect()
.then(() => {
server.start(options, async (req, res) => listener(req, res));
})
.catch((err) => {
setTimeout(() => {
throw err;
});
});
process.on("SIGINT", () => {
void initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
});
process.on("SIGTERM", () => {
void initPromise.finally(() => {
server.stop().then(exitWorkerGracefully).catch(exitWorkerUngracefully);
});
});
}
+5
View File
@@ -0,0 +1,5 @@
export const APP_JS = "app.js";
export const APP_CSS = "app.css";
export const ICONS_SVG = "icons.svg";
export const LOGO_SVG = "logo.svg";
export const FAVICON_PNG = "favicon.png";
+263 -294
View File
@@ -1,97 +1,46 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "node:path";
import fs from "node:fs";
import { createHash } from "node:crypto";
import { promisify } from "node:util";
import { exec } from "node:child_process";
import * as esbuild from "esbuild";
import { optimize } from "svgo";
import * as xmlParser from "../lib/xml-parser.ts";
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 { terser } from "rollup-plugin-terser";
import webpack from "webpack";
import postcss from "postcss";
import postcssImport from "postcss-import";
import postcssPresetEnv from "postcss-preset-env";
import cssnano from "cssnano";
import SVGO from "svgo";
import * as xmlParser from "../lib/xml-parser";
const fsAsync = {
readdir: promisify(fs.readdir),
readFile: promisify(fs.readFile),
writeFile: promisify(fs.writeFile),
copyFile: promisify(fs.copyFile),
rename: promisify(fs.rename),
chmod: promisify(fs.chmod),
lstat: promisify(fs.lstat),
exists: promisify(fs.exists),
rmdir: promisify(fs.rmdir),
unlink: promisify(fs.unlink),
mkdir: promisify(fs.mkdir),
};
const execAsync = promisify(exec);
const MODE = process.env["NODE_ENV"] || "production";
const BUILD_METADATA = new Date()
.toISOString()
.split(".")[0]
.replace(/[^0-9]/g, "");
const INPUT_DIR = process.cwd();
const OUTPUT_DIR = path.join(INPUT_DIR, "dist");
const INPUT_DIR = path.resolve(__dirname, "..");
const OUTPUT_DIR = path.resolve(__dirname, "../dist");
const externals = [
"path",
"fs",
"cluster",
"os",
"tls",
"http",
"https",
"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",
"stream",
"mithril",
"parsimmon",
"yaml",
"codemirror",
"codemirror/mode/javascript/javascript",
"codemirror/mode/yaml/yaml"
];
function rmDirSync(dirPath): void {
if (!fs.existsSync(dirPath)) return;
const files = fs.readdirSync(dirPath);
async function rmDir(dirPath: string): Promise<void> {
if (!(await fsAsync.exists(dirPath))) return;
const files = await fsAsync.readdir(dirPath);
for (const file of files) {
const filePath = `${dirPath}/${file}`;
if (fs.statSync(filePath).isFile()) fs.unlinkSync(filePath);
else rmDirSync(filePath);
const filePath = path.join(dirPath, file);
if ((await fsAsync.lstat(filePath)).isDirectory()) await rmDir(filePath);
else await fsAsync.unlink(filePath);
}
fs.rmdirSync(dirPath);
await fsAsync.rmdir(dirPath);
}
// For lockfileVersion = 1
function stripDevDeps(deps): void {
if (!deps["dependencies"]) return;
for (const [k, v] of Object.entries(deps["dependencies"])) {
@@ -101,6 +50,15 @@ function stripDevDeps(deps): void {
if (!Object.keys(deps["dependencies"]).length) delete deps["dependencies"];
}
// For lockfileVersion = 2
function stripDevDeps2(deps): void {
if (!deps["packages"]) return;
for (const [k, v] of Object.entries(deps["packages"])) {
delete v["devDependencies"];
if (v["dev"]) delete deps["packages"][k];
}
}
function xmlTostring(xml): string {
const children = [];
for (const c of xml.children || []) children.push(xmlTostring(c));
@@ -110,6 +68,59 @@ function xmlTostring(xml): string {
: `<${xml.name} ${xml.attrs}>${children.join("")}</${xml.name}>`;
}
function assetHash(buffer: Buffer | string): string {
return createHash("md5").update(buffer).digest("hex").slice(0, 8);
}
const ASSETS = {} as {
APP_JS?: string;
APP_CSS?: string;
ICONS_SVG?: string;
LOGO_SVG?: string;
FAVICON_PNG?: string;
};
const assetsPlugin = {
name: "assets",
setup(build) {
build.onLoad({ filter: /\/build\/assets.ts$/ }, () => {
const lines = Object.entries(ASSETS).map(
([k, v]) => `export const ${k} = ${JSON.stringify(v)};`,
);
return { contents: lines.join("\n") };
});
},
} as esbuild.Plugin;
const packageDotJsonPlugin = {
name: "packageDotJson",
setup(build) {
const sourcePath = path.join(INPUT_DIR, "package.json");
build.onResolve({ filter: /\/package.json$/ }, (args) => {
const p = path.join(args.resolveDir, args.path);
if (p !== sourcePath) return undefined;
return { path: path.join(OUTPUT_DIR, "package.json") };
});
},
} as esbuild.Plugin;
const inlineDepsPlugin = {
name: "inlineDeps",
setup(build) {
const deps = [
"parsimmon",
"espresso-iisojs",
"codemirror",
"mithril",
"yaml",
];
build.onResolve({ filter: /^[^.]/ }, async (args) => {
if (deps.some((d) => args.path.startsWith(d))) return undefined;
return { sideEffects: false, external: true };
});
},
} as esbuild.Plugin;
function generateSymbol(id: string, svgStr: string): string {
const xml = xmlParser.parseXml(svgStr);
const svg = xml.children[0];
@@ -122,48 +133,65 @@ 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 getBuildMetadata(): Promise<string> {
const date = new Date().toISOString().slice(2, 10).replaceAll("-", "");
const [commit, diff, newFiles] = await Promise.all([
execAsync("git rev-parse HEAD"),
execAsync("git diff HEAD"),
execAsync("git ls-files --others --exclude-standard"),
]).then((res) => res.map((r) => r.stdout.trim()));
if (!diff && !newFiles) return date + commit.slice(0, 4);
const hash = createHash("md5");
hash.update(commit).update(diff).update(newFiles);
for (const file of newFiles.split("\n").filter((f) => f))
hash.update(await fsAsync.readFile(file));
return date + hash.digest("hex").slice(0, 4);
}
async function init(): Promise<void> {
// Delete any old output directory
rmDirSync(OUTPUT_DIR);
// Create output directory layout
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()
const [buildMetadata, packageJsonFile, npmShrinkwrapFile] = await Promise.all(
[
getBuildMetadata(),
fsAsync.readFile(path.join(INPUT_DIR, "package.json")),
fsAsync.readFile(path.join(INPUT_DIR, "npm-shrinkwrap.json")),
],
);
const packageJson = JSON.parse(packageJsonFile.toString());
delete packageJson["devDependencies"];
packageJson["scripts"] = {
install: packageJson["scripts"].install,
configure: packageJson["scripts"].configure
};
packageJson["version"] = `${packageJson["version"]}+${BUILD_METADATA}`;
fs.writeFileSync(
path.resolve(OUTPUT_DIR, "package.json"),
JSON.stringify(packageJson, null, 2)
);
delete packageJson["private"];
delete packageJson["scripts"];
packageJson["version"] = `${packageJson["version"]}+${buildMetadata}`;
// Create npm-shrinkwrap.json
const npmShrinkwrapJson = JSON.parse(
fs.readFileSync(path.resolve(INPUT_DIR, "npm-shrinkwrap.json")).toString()
);
npmShrinkwrapJson["version"] = packageJson["version"];
stripDevDeps(npmShrinkwrapJson);
fs.writeFileSync(
path.resolve(OUTPUT_DIR, "npm-shrinkwrap.json"),
JSON.stringify(npmShrinkwrapJson, null, 2)
);
const npmShrinkwrap = JSON.parse(npmShrinkwrapFile.toString());
npmShrinkwrap["version"] = packageJson["version"];
stripDevDeps(npmShrinkwrap);
stripDevDeps2(npmShrinkwrap);
await rmDir(OUTPUT_DIR);
await fsAsync.mkdir(OUTPUT_DIR);
await Promise.all([
fsAsync.mkdir(path.join(OUTPUT_DIR, "bin")),
fsAsync.mkdir(path.join(OUTPUT_DIR, "public")),
fsAsync.writeFile(
path.join(OUTPUT_DIR, "package.json"),
JSON.stringify(packageJson, null, 2),
),
fsAsync.writeFile(
path.join(OUTPUT_DIR, "npm-shrinkwrap.json"),
JSON.stringify(npmShrinkwrap, null, 2),
),
]);
}
async function copyStatic(): Promise<void> {
@@ -172,219 +200,160 @@ async function copyStatic(): Promise<void> {
"README.md",
"CHANGELOG.md",
"public/logo.svg",
"public/favicon.png"
"public/favicon.png",
];
for (const file of files) {
fs.copyFileSync(
path.resolve(INPUT_DIR, file),
path.resolve(OUTPUT_DIR, file)
);
}
const [logo, favicon] = await Promise.all([
fsAsync.readFile(path.join(INPUT_DIR, "public/logo.svg")),
fsAsync.readFile(path.join(INPUT_DIR, "public/favicon.png")),
]);
ASSETS.LOGO_SVG = `logo-${assetHash(logo)}.svg`;
ASSETS.FAVICON_PNG = `favicon-${assetHash(favicon)}.png`;
const filenames = {} as Record<string, string>;
filenames["public/logo.svg"] = path.join("public", ASSETS.LOGO_SVG);
filenames["public/favicon.png"] = path.join("public", ASSETS.FAVICON_PNG);
await Promise.all(
files.map((f) =>
fsAsync.copyFile(
path.join(INPUT_DIR, f),
path.join(OUTPUT_DIR, filenames[f] || f),
),
),
);
}
async function generateCss(): Promise<void> {
const cssInPath = path.resolve(INPUT_DIR, "ui/css/app.css");
const cssOutPath = path.resolve(OUTPUT_DIR, "public/app.css");
const cssIn = fs.readFileSync(cssInPath);
const cssOut = await postcss([
postcssImport,
postcssPresetEnv({
stage: 3,
features: {
"nesting-rules": true,
"color-mod-function": true
}
}),
cssnano
]).process(cssIn, { from: cssInPath, to: cssOutPath });
fs.writeFileSync(cssOutPath, cssOut.css);
}
const res = await esbuild.build({
bundle: true,
absWorkingDir: INPUT_DIR,
minify: MODE === "production",
sourcemap: "linked",
sourcesContent: false,
entryPoints: ["ui/css/app.css"],
entryNames: "[dir]/[name]-[hash]",
outfile: path.join(OUTPUT_DIR, "public/app.css"),
target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"],
metafile: true,
});
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);
for (const [k, v] of Object.entries(res.metafile.outputs)) {
if (v.entryPoint === "ui/css/app.css") {
ASSETS.APP_CSS = path.relative(
path.join(OUTPUT_DIR, "public"),
path.join(INPUT_DIR, k),
);
break;
}
}
}
async function generateBackendJs(): Promise<void> {
for (const bin of [
const services = [
"genieacs-cwmp",
"genieacs-ext",
"genieacs-nbi",
"genieacs-fs",
"genieacs-ui"
]) {
const inputFile = path.resolve(INPUT_DIR, `bin/${bin}`);
const outputFile = path.resolve(OUTPUT_DIR, `bin/${bin}`);
const bundle = await rollup({
input: inputFile,
external: externals,
acorn: {
allowHashBang: true
},
treeshake: {
propertyReadSideEffects: false,
pureExternalModules: true
},
plugins: [
rollupReplace({
delimiters: ["", ""],
"#!/usr/bin/env -S node -r esm -r ts-node/register/transpile-only": ""
}),
rollupJson({ preferConst: true }),
{
resolveId: (importee, importer) => {
if (importee.endsWith("/package.json")) {
const p = path.resolve(path.dirname(importer), importee);
if (p === path.resolve(INPUT_DIR, "package.json"))
return path.resolve(OUTPUT_DIR, "package.json");
}
return null;
}
},
typescript({
tsconfig: "./tsconfig.json",
include: [`bin/${bin}`, "lib/**/*.ts"]
}),
MODE === "production" ? terser() : null
]
});
"genieacs-ui",
];
await bundle.write({
format: "cjs",
preferConst: true,
sourcemap: "inline",
sourcemapExcludeSources: true,
banner: "#!/usr/bin/env node",
file: outputFile
});
await esbuild.build({
bundle: true,
absWorkingDir: INPUT_DIR,
minify: MODE === "production",
sourcemap: "inline",
sourcesContent: false,
platform: "node",
target: "node12.13.0",
packages: "external",
banner: { js: "#!/usr/bin/env node" },
entryPoints: services.map((s) => `bin/${s}.ts`),
outdir: path.join(OUTPUT_DIR, "bin"),
plugins: [packageDotJsonPlugin, assetsPlugin],
});
for (const bin of services) {
const p = path.join(OUTPUT_DIR, "bin", bin);
await fsAsync.rename(`${p}.js`, p);
// Mark as executable
const mode = fs.statSync(outputFile).mode;
fs.chmodSync(outputFile, mode | 73);
const mode = (await fsAsync.lstat(p)).mode;
await fsAsync.chmod(p, mode | 73);
}
}
async function generateFrontendJs(): Promise<void> {
const inputFile = path.resolve(INPUT_DIR, "ui/app.ts");
const outputFile = path.resolve(OUTPUT_DIR, "public/app.js");
const bundle = await rollup({
input: inputFile,
external: externals,
plugins: [
rollupJson({ preferConst: true }),
typescript({ tsconfig: "./tsconfig.json" })
],
inlineDynamicImports: true,
treeshake: {
propertyReadSideEffects: false,
pureExternalModules: true
},
onwarn: ((warning, warn) => {
// Ignore circular dependency warnings
if (warning.code !== "CIRCULAR_DEPENDENCY") warn(warning);
}) as WarningHandler
});
await bundle.write({
preferConst: true,
const res = await esbuild.build({
bundle: true,
absWorkingDir: INPUT_DIR,
splitting: true,
minify: MODE === "production",
sourcemap: "linked",
sourcesContent: false,
platform: "browser",
format: "esm",
sourcemap: "inline",
sourcemapExcludeSources: true,
file: outputFile
target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"],
entryPoints: ["ui/app.ts"],
entryNames: "[dir]/[name]-[hash]",
outdir: path.join(OUTPUT_DIR, "public"),
plugins: [packageDotJsonPlugin, inlineDepsPlugin, assetsPlugin],
metafile: true,
});
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"
}
};
for (const [k, v] of Object.entries(res.metafile.outputs)) {
for (const imp of v.imports)
if (imp.external) throw new Error(`External import found: ${imp.path}`);
const stats = await promisify(webpack)(webpackConf);
process.stdout.write(stats.toString({ colors: true }) + "\n");
if (v.entryPoint === "ui/app.ts") {
ASSETS.APP_JS = path.relative(
path.join(OUTPUT_DIR, "public"),
path.join(INPUT_DIR, k),
);
}
}
}
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 symbols = [] as string[];
const iconsDir = path.join(INPUT_DIR, "ui/icons");
for (const file of await fsAsync.readdir(iconsDir)) {
const id = path.parse(file).name;
const filePath = path.join(iconsDir, file);
const { data } = await svgo.optimize(fs.readFileSync(filePath).toString());
const src = (await fsAsync.readFile(filePath)).toString();
const { data } = await optimize(src, {
plugins: [
{
name: "preset-default",
params: {
overrides: {
removeViewBox: false,
},
},
},
],
});
symbols.push(generateSymbol(id, data));
}
fs.writeFileSync(
path.resolve(OUTPUT_DIR, "public/icons.svg"),
`<svg xmlns="http://www.w3.org/2000/svg">${symbols.join("")}</svg>`
const data = `<svg xmlns="http://www.w3.org/2000/svg">${symbols.join(
"",
)}</svg>`;
ASSETS.ICONS_SVG = `icons-${assetHash(data)}.svg`;
await fsAsync.writeFile(
path.join(OUTPUT_DIR, "public", ASSETS.ICONS_SVG),
data,
);
}
init()
.then(() => {
.then(() =>
Promise.all([
copyStatic(),
Promise.all([generateIconsSprite(), copyStatic()]).then(
generateFrontendJs,
),
generateCss(),
generateIconsSprite(),
generateToolsJs(),
generateBackendJs(),
generateFrontendJs()
])
.then(() => {})
.catch(err => {
process.stderr.write(err.stack + "\n");
});
})
.catch(err => {
]).then(generateBackendJs),
)
.catch((err) => {
process.stderr.write(err.stack + "\n");
});
+55
View File
@@ -0,0 +1,55 @@
import { exec } from "node:child_process";
import { promisify } from "node:util";
const execPromise = promisify(exec);
async function runEslint(): Promise<string> {
const CMD =
"eslint 'bin/*.ts' 'lib/**/*.ts' 'ui/**/*.ts' 'test/**/*.ts' 'build/**/*.ts'";
const env = {
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
...process.env,
};
try {
const { stdout, stderr } = await execPromise(CMD, { env });
if (stderr) throw new Error(stderr);
return stdout;
} catch (err) {
if (err.killed || err.signal || err.stderr || err.code !== 1) throw err;
return err.stdout;
}
}
async function runTsc(): Promise<string> {
const CMD = "tsc --noEmit";
const env = {
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
...process.env,
};
const { stdout, stderr } = await execPromise(CMD, { env });
if (stderr) throw new Error(stderr);
return stdout;
}
async function runPrettier(): Promise<string> {
const CMD = "prettier --prose-wrap always --write .";
const env = {
...(process.stdout.isTTY && { FORCE_COLOR: "1" }),
...process.env,
};
const { stdout, stderr } = await execPromise(CMD, { env });
if (stderr) throw new Error(stderr);
return stdout;
}
async function runAll(): Promise<void> {
const prom1 = runPrettier();
const prom2 = runEslint();
const prom3 = runTsc();
console.log(await prom1);
console.log(await prom2);
console.log(await prom3);
}
runAll().catch(console.error);
+178
View File
@@ -0,0 +1,178 @@
personal_ws-1.1 en 0
genieacs
GenieACS
javascript
sudo
config
cwmp
CPE
systemctl
nbi
CWMP
NBI
ACS
SSL
xsd
args
DeviceID
GENIEACS
env
param
arg
AUTH
igd
TCP
toctree
auth
EnvironmentFile
JSON
json
parameterValues
setParameterValues
stderr
stdout
attr
cfg
CPEs
ExecStart
faq
FileType
GenieACS's
JWT
maxdepth
npm
objectName
refreshObject
SerialNumber
systemd
usr
WantedBy
yaml
chown
Config
getParameterValues
https
InternetGatewayDevice
mkdir
OUI
sql
TODO
UDP
addObject
AGPLv
const
cpe
cron
deleteObject
FactoryReset
factoryReset
FileName
fileType
Github
hostname
HTTPS
jwt
latlong
NPM
oui
parameterNames
ProductClass
productClass
repo
SSID
sublicense
TLS
WANIPConnection
WPA
YourProvisionName
abc
Abdulla
accessors
AddObject
AddressingType
api
APIs
chmod
cwmp's
dateext
DATETIME
dateTime
datetime
declaratively
delaycompress
DeleteObject
deviceId
DHCP
dir
distro
equisatisfiable
ExternalIPAddress
failover
favor
func
GetParameterAttributes
getPassword
gte
gui
hexBinary
Hils
HOSTNAME
init
journalctl
journald
LastFileName
LastFileType
lastInform
LIBXMLJS
libxmljs
literalinclude
lockfile
logrotate
lte
mipsbe
normalizers
pre
quickstart
README
redis
reimplemented
RequestDownload
RESTful
rollout
sandboxed
scalable
SetParameterAttributes
SetParamteerValues
SIGINT
SIGTERM
skipWritableCheck
stringify
subdirectory
TargetFileName
useradd
VirtualParameters
vparam
xml
YAML
Zaid
cacheExpire
iss
http
statusCode
rawData
pos
Brotli
CSV
hostInfo
IPv
downloadSuccessOnTimeout
TransferComplete
nginx
ENCODEURICOMPONENT
pageSize
skipRootGpn
GPN
NaN
CIDR
XMPP
PEM
+12
View File
@@ -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
+64
View File
@@ -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
View File
@@ -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``
~~~~~~~~~~
+66
View File
@@ -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
+212 -1
View File
@@ -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:7567/') 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
View File
@@ -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;
+1
View File
@@ -30,6 +30,7 @@ Welcome to GenieACS's documentation!
provisions
virtual-parameters
administration-faq
.. raw:: latex
+26 -18
View File
@@ -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,12 +68,19 @@ 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
GENIEACS_UI_JWT_SECRET=secret
NODE_OPTIONS=--enable-source-maps
GENIEACS_EXT_DIR=/opt/genieacs/ext
Generate a secure JWT secret and append to :file:`/opt/genieacs/genieacs.env`:
.. code:: bash
node -e "console.log(\"GENIEACS_UI_JWT_SECRET=\" + require('crypto').randomBytes(128).toString('hex'))" >> /opt/genieacs/genieacs.env
Set file ownership and permissions:
@@ -100,11 +107,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 +129,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 +138,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 +151,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 +160,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 +173,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 +182,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 +195,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
+8
View File
@@ -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
+7 -7
View File
@@ -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});
}
+362 -142
View File
@@ -1,112 +1,160 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { parse } from "url";
import * as db from "./db";
import * as common from "./common";
import * as cache from "./cache";
import { ObjectId } from "mongodb";
import { collections } from "./db/db.ts";
import {
getCurrentSnapshot,
deleteConfig,
deleteFault as dbDeleteFault,
deleteFile,
deletePermission,
deletePreset,
deleteProvision,
deleteTask,
deleteUser,
deleteVirtualParameter,
putConfig,
putPermission,
putPreset,
putProvision,
putUser,
putVirtualParameter,
} from "./ui/db.ts";
import * as common from "./util.ts";
import * as cache from "./cache.ts";
import { acquireLock, getToken, releaseLock } from "./lock.ts";
import {
getRevision,
getConfig,
getConfigExpression
} from "./local-cache";
getConfigExpression,
getUsers,
} from "./ui/local-cache.ts";
import {
httpConnectionRequest,
udpConnectionRequest
} from "./connection-request";
import { Expression, Task } from "./types";
udpConnectionRequest,
xmppConnectionRequest,
} from "./connection-request.ts";
import { Expression, Task } from "./types.ts";
import { evaluate } from "./common/expression/util.ts";
import { hashPassword } from "./auth.ts";
import { flattenDevice } from "./ui/db.ts";
import { ResourceLockedError } from "./common/errors.ts";
import * as config from "../lib/config.ts";
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 XMPP_CONFIGURED = !!config.get("XMPP_JID");
const device = await db.devicesCollection.findOne({ _id: deviceId }, options);
if (!device) throw new Error("No such device");
export async function connectionRequest(
deviceId: string,
device?: Record<string, { value?: [boolean | number | string, string] }>,
): Promise<string> {
if (!device) {
const res = await collections.devices.findOne({ _id: deviceId });
if (!res) throw new Error("No such device");
device = flattenDevice(res);
}
let managementServer,
connectionRequestUrl,
let connectionRequestUrl,
udpConnectionRequestAddress,
stunEnable,
connReqJabberId,
username,
password;
if (device.Device)
// TR-181 data model
managementServer = device.Device.ManagementServer;
// TR-098 data model
else managementServer = device.InternetGatewayDevice.ManagementServer;
if (managementServer.ConnectionRequestURL)
connectionRequestUrl = managementServer.ConnectionRequestURL._value;
if (managementServer.UDPConnectionRequestAddress) {
udpConnectionRequestAddress =
managementServer.UDPConnectionRequestAddress._value;
if (device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]) {
connectionRequestUrl = (device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
device[
"InternetGatewayDevice.ManagementServer.UDPConnectionRequestAddress"
] || {}
).value || [""])[0];
stunEnable = ((
device["InternetGatewayDevice.ManagementServer.STUNEnable"] || {}
).value || [""])[0];
connReqJabberId = ((
device["InternetGatewayDevice.ManagementServer.ConnReqJabberID"] || {}
).value || [""])[0];
username = ((
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestUsername"
] || {}
).value || [""])[0];
password = ((
device[
"InternetGatewayDevice.ManagementServer.ConnectionRequestPassword"
] || {}
).value || [""])[0];
} else {
connectionRequestUrl = (device[
"Device.ManagementServer.ConnectionRequestURL"
].value || [""])[0];
udpConnectionRequestAddress = ((
device["Device.ManagementServer.UDPConnectionRequestAddress"] || {}
).value || [""])[0];
stunEnable = ((device["Device.ManagementServer.STUNEnable"] || {})
.value || [""])[0];
connReqJabberId = ((device["Device.ManagementServer.ConnReqJabberID"] || {})
.value || [""])[0];
username = ((
device["Device.ManagementServer.ConnectionRequestUsername"] || {}
).value || [""])[0];
password = ((
device["Device.ManagementServer.ConnectionRequestPassword"] || {}
).value || [""])[0];
}
let remoteAddress;
try {
remoteAddress = new URL(connectionRequestUrl).hostname;
} catch (err) {
return "Invalid connection request URL";
}
if (managementServer.ConnectionRequestUsername)
username = managementServer.ConnectionRequestUsername._value;
if (managementServer.ConnectionRequestPassword)
password = managementServer.ConnectionRequestPassword._value;
const context = {
id: device["_id"],
serialNumber: device["_deviceId"]["SerialNumber"],
productClass: device["_deviceId"]["ProductClass"],
oui: device["_deviceId"]["OUI"],
remoteAddress: connectionRequestUrl
? parse(connectionRequestUrl).host
: null,
username: username || "",
password: password || ""
const 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 snapshot = await getRevision();
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,
"cwmp.connectionRequestAuth"
"cwmp.connectionRequestAuth",
);
if (authExp === undefined) {
@@ -114,64 +162,97 @@ 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;
if (udpConnectionRequestAddress) {
udpProm = udpConnectionRequest(
udpConnectionRequestAddress,
authExp,
context,
UDP_CONNECTION_REQUEST_PORT,
debug,
deviceId
);
const debug = !!getConfig(snapshot, "cwmp.debug", {}, now, evalCallback);
let udpProm = Promise.resolve(false);
if (udpConnectionRequestAddress && +stunEnable) {
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(
let status;
if (connReqJabberId && XMPP_CONFIGURED) {
status = await xmppConnectionRequest(
connReqJabberId,
authExp,
CONNECTION_REQUEST_TIMEOUT,
debug,
deviceId,
);
} else {
status = await httpConnectionRequest(
connectionRequestUrl,
authExp,
context,
CONNECTION_REQUEST_ALLOW_BASIC_AUTH,
CONNECTION_REQUEST_TIMEOUT,
debug,
deviceId
deviceId,
);
} catch (err) {
if (!udpProm) throw err;
await udpProm;
}
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 collections.devices.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 getToken(`cwmp_session_${deviceId}`);
if (token?.startsWith("cwmp_session_")) return true;
if (timeout < 500) return false;
await new Promise((resolve) => setTimeout(resolve, 500));
timeout -= Date.now() - now;
return awaitSessionStart(deviceId, lastInform, timeout);
}
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 getToken(`cwmp_session_${deviceId}`);
if (!token?.startsWith("cwmp_session_")) return true;
if (timeout < 500) return false;
await new Promise((resolve) => setTimeout(resolve, 500));
timeout -= Date.now() - now;
return 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 +290,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 +311,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,42 +326,174 @@ 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({
await collections.tasks.deleteOne({
device: task.device,
uniqueKey: task.uniqueKey
uniqueKey: task.uniqueKey,
});
}
}
await db.tasksCollection.insertMany(tasks);
await collections.tasks.insertMany(tasks);
for (const task of tasks) task._id = task._id.toString();
return tasks;
}
export async function deleteDevice(deviceId): Promise<void> {
await Promise.all([
db.tasksCollection.deleteMany({ device: deviceId }),
db.devicesCollection.deleteOne({ _id: deviceId }),
db.faultsCollection.deleteMany({
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`
}
}),
db.operationsCollection.deleteMany({
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`
}
}),
cache.del(`${deviceId}_tasks_faults_operations`)
]);
export async function deleteDevice(deviceId: string): Promise<void> {
const token = await acquireLock(`cwmp_session_${deviceId}`, 5000);
if (!token) throw new ResourceLockedError("Device is in session");
try {
await Promise.all([
collections.tasks.deleteMany({ device: deviceId }),
collections.devices.deleteOne({ _id: deviceId }),
collections.faults.deleteMany({
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
},
}),
collections.operations.deleteMany({
_id: {
$regex: `^${common.escapeRegExp(deviceId)}\\:`,
},
}),
]);
} finally {
await releaseLock(`cwmp_session_${deviceId}`, token);
}
}
export async function deleteFault(id: string): Promise<void> {
const deviceId = id.split(":", 1)[0];
const channel = id.slice(deviceId.length + 1);
const token = await acquireLock(`cwmp_session_${deviceId}`, 5000);
if (!token) throw new ResourceLockedError("Device is in session");
try {
const proms = [dbDeleteFault(id)];
if (channel.startsWith("task_"))
proms.push(deleteTask(new ObjectId(channel.slice(5))));
await Promise.all(proms);
} finally {
await releaseLock(`cwmp_session_${deviceId}`, token);
}
}
export async function deleteResource(
resource: string,
id: string,
): Promise<void> {
if (resource === "devices") {
await deleteDevice(id);
} else if (resource === "files") {
await deleteFile(id);
await cache.del("cwmp-local-cache-hash");
} else if (resource === "faults") {
await deleteFault(id);
} else if (resource === "provisions") {
await deleteProvision(id);
await cache.del("cwmp-local-cache-hash");
} else if (resource === "presets") {
await deletePreset(id);
await cache.del("cwmp-local-cache-hash");
} else if (resource === "virtualParameters") {
await deleteVirtualParameter(id);
await cache.del("cwmp-local-cache-hash");
} else if (resource === "config") {
await deleteConfig(id);
await Promise.all([
cache.del("ui-local-cache-hash"),
cache.del("cwmp-local-cache-hash"),
]);
} else if (resource === "permissions") {
await deletePermission(id);
await cache.del("ui-local-cache-hash");
} else if (resource === "users") {
await deleteUser(id);
await cache.del("ui-local-cache-hash");
} else {
throw new Error(`Unknown resource ${resource}`);
}
}
// TODO Implement validation
export async function putResource(
resource: string,
id: string,
data: any,
): Promise<void> {
if (resource === "presets") {
await putPreset(id, data);
await cache.del("cwmp-local-cache-hash");
} else if (resource === "provisions") {
await putProvision(id, data);
await cache.del("cwmp-local-cache-hash");
} else if (resource === "virtualParameters") {
await putVirtualParameter(id, data);
await cache.del("cwmp-local-cache-hash");
} else if (resource === "config") {
await putConfig(id, data);
await Promise.all([
cache.del("ui-local-cache-hash"),
cache.del("cwmp-local-cache-hash"),
]);
} else if (resource === "permissions") {
await putPermission(id, data);
await cache.del("ui-local-cache-hash");
} else if (resource === "users") {
delete data["password"];
delete data["salt"];
await putUser(id, data);
await cache.del("ui-local-cache-hash");
} else {
throw new Error(`Unknown resource ${resource}`);
}
}
export function authLocal(
snapshot: string,
username: string,
password: string,
): Promise<boolean> {
return new Promise((resolve, reject) => {
const users = getUsers(snapshot);
const user = users[username];
if (!user?.password) return void resolve(null);
hashPassword(password, user.salt)
.then((hash) => {
if (hash === user.password) resolve(true);
else resolve(false);
})
.catch(reject);
});
}
+22 -46
View File
@@ -1,33 +1,16 @@
/**
* 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 { createHash, randomBytes, pbkdf2 } from "node:crypto";
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 +33,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 };
@@ -59,7 +44,7 @@ export function parseAuthorizationHeader(authHeader): { method: string } {
// Inspired by https://github.com/jshttp/basic-auth
const USER_PASS_REGEX = /^([^:]*):(.*)$/;
const creds = USER_PASS_REGEX.exec(
Buffer.from(authHeader.slice(method.length + 1), "base64").toString()
Buffer.from(authHeader.slice(method.length + 1), "base64").toString(),
);
if (!creds) throw new Error("Unable to parse auth header");
@@ -72,7 +57,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 +67,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");
}
@@ -94,23 +81,15 @@ export function digest(
qop?: string | Buffer,
body?: string | Buffer,
cnonce?: string | Buffer,
nc?: string | Buffer
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 +101,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 +122,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";
@@ -168,7 +144,7 @@ export function solveDigest(
qop,
body,
cnonce,
nc
nc,
);
let authString = `Digest username="${username}"`;
@@ -183,7 +159,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 +168,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);
+20 -125
View File
@@ -1,140 +1,35 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { MongoClient, Collection } from "mongodb";
import * as config from "./config";
import { collections } from "./db/db.ts";
import * as config from "./config.ts";
const CLOCK_SKEW_TOLERANCE = 30000;
const MAX_CACHE_TTL = +config.get("MAX_CACHE_TTL");
let mongoClient: MongoClient;
let mongoCollection: Collection;
let mongoTimeOffset = 0;
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;
export async function get(key: string): Promise<string> {
const res = await collections.cache.findOne({ _id: key });
return res?.value;
}
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 collections.cache.deleteOne({ _id: key });
}
export async function set(
key: string,
value: string | number,
ttl: number = MAX_CACHE_TTL
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 collections.cache.replaceOne(
{ _id: key },
{ _id: key, value: value, expire: expire },
{ upsert: true }
{ 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"];
return null;
}
export async function lock(lockName, ttl): Promise<Function> {
const token = Math.random()
.toString(36)
.slice(2);
async function unlockOrExtend(extendTtl): Promise<void> {
if (!extendTtl) {
const res = await mongoCollection.deleteOne({
_id: lockName,
value: token
});
if (res["result"]["n"] !== 1) throw new Error("Lock expired");
} else {
const expire = new Date(Date.now() - mongoTimeOffset + extendTtl * 1000);
const res = await mongoCollection.updateOne(
{ _id: lockName, value: token },
{ expire: expire }
);
if (res["result"]["n"] !== 1) throw new Error("Lock expired");
}
}
const expireTest = new Date(Date.now() - mongoTimeOffset);
const expireSet = new Date(Date.now() - mongoTimeOffset + ttl * 1000);
try {
await mongoCollection.updateOne(
{ _id: lockName, expire: { $lte: expireTest } },
{ $set: { value: token, expire: expireSet } },
{ upsert: true }
);
} catch (err) {
if (err && err.code === 11000) {
await new Promise(resolve => setTimeout(resolve, 200));
return lock(lockName, ttl);
}
}
return unlockOrExtend;
export async function pop(key: string): Promise<string> {
const res = await collections.cache.findOneAndDelete({ _id: key });
return res.value?.value;
}
+46 -36
View File
@@ -1,35 +1,28 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import cluster from "cluster";
import { cpus } from "os";
import * as logger from "./logger";
import cluster, { Worker } from "node:cluster";
import { cpus } from "node:os";
import * as logger from "./logger.ts";
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 +32,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 +85,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 +94,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 {
-59
View File
@@ -1,59 +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/>.
*/
export const UNDEFINED_TYPE = "[object Undefined]";
export const NULL_TYPE = "[object Null]";
export const BOOLEAN_TYPE = "[object Boolean]";
export const NUMBER_TYPE = "[object Number]";
export const STRING_TYPE = "[object String]";
export const ARRAY_TYPE = "[object Array]";
export const OBJECT_TYPE = "[object Object]";
export const REGEXP_TYPE = "[object RegExp]";
export const DATE_TYPE = "[object Date]";
export const typeOf = (obj): string => Object.prototype.toString.call(obj);
export function generateDeviceId(deviceIdStruct): string {
// Escapes everything except alphanumerics and underscore
function esc(str): string {
return str.replace(/[^A-Za-z0-9_]/g, chr => {
const buf = Buffer.from(chr, "utf8");
let rep = "";
for (const b of buf) rep += "%" + b.toString(16).toUpperCase();
return rep;
});
}
// Guaranteeing globally unique id as defined in TR-069
if (deviceIdStruct["ProductClass"]) {
return (
esc(deviceIdStruct["OUI"]) +
"-" +
esc(deviceIdStruct["ProductClass"]) +
"-" +
esc(deviceIdStruct["SerialNumber"])
);
}
return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]);
}
// Source: http://stackoverflow.com/a/6969486
export function escapeRegExp(str): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
+43 -37
View File
@@ -1,40 +1,23 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { PermissionSet, Expression } from "../types";
import { evaluate } from "./expression";
import { PermissionSet, Expression } from "../types.ts";
import { evaluate, or } from "./expression/util.ts";
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 +38,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);
@@ -78,7 +80,7 @@ export default class Authorizer {
const validator = (
mutationType: string,
mutation: any,
any: any
any: any,
): boolean => {
if (!validators.length) return false;
@@ -87,7 +89,7 @@ export default class Authorizer {
mutation,
resourceType,
object: resource,
options: any
options: any,
};
const valueFunction = (paramName): any => {
@@ -97,13 +99,13 @@ export default class Authorizer {
if (["mutation", "options"].includes(entry)) {
value = object[entry];
for (const seg of paramName.split(".")) {
// typeof null is "object"
if (value != null && typeof value !== "object") value = null;
else value = value[seg];
if (value == null) break;
if (typeof value !== "object") value = null;
else value = value[seg];
}
} else if (object[entry]) {
value = object[entry][paramName];
if (paramName) value = object[entry][paramName];
else value = object[entry];
}
return value;
@@ -112,7 +114,7 @@ export default class Authorizer {
const res = evaluate(
validators.length > 1 ? ["OR", validators] : validators[0],
valueFunction,
Date.now()
Date.now(),
);
return !Array.isArray(res) && !!res;
};
@@ -120,4 +122,8 @@ export default class Authorizer {
this.validatorCache.set(resource, validator);
return validator;
}
public getPermissionSets(): PermissionSet[] {
return this.permissionSets;
}
}
+16
View File
@@ -0,0 +1,16 @@
export default function debounce<T>(
func: (args: T[]) => void,
timeout: number,
): (arg: T) => void {
let timer: ReturnType<typeof setTimeout>;
let args: T[] = [];
return (arg: T) => {
args.push(arg);
clearTimeout(timer);
timer = setTimeout(() => {
const argscopy = args;
args = [];
func(argscopy);
}, timeout);
};
}
+6
View File
@@ -0,0 +1,6 @@
export class ResourceLockedError extends Error {
constructor(message: string) {
super(message);
this.name = "ResourceLockedError";
}
}
-285
View File
@@ -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 };
}
-329
View File
@@ -1,329 +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, mapAsync, parseLikePattern } from "./expression-parser";
import { booleanCnf } from "./expression-cnf";
import { naiveDpll } from "./sat-solver";
import { Expression } from "../types";
const isArray = Array.isArray;
const regExpCache = new WeakMap<object, RegExp>();
const REDUCE_SKIP = {};
function reduce(exp, callback): Expression {
let loop = true;
while (loop) {
loop = false;
for (let i = 2; i < exp.length; ++i) {
const res = callback(exp[i - 1], exp[i], i - 2);
if (res !== REDUCE_SKIP) {
loop = true;
exp = exp.slice();
exp.splice(i - 1, 2, res);
}
}
}
if (exp.length === 2) return exp[1];
return exp;
}
export function likePatternToRegExp(pat, esc = "", flags = ""): RegExp {
const convChars = {
"-": "\\-",
"/": "\\/",
"\\": "\\/",
"^": "\\^",
$: "\\$",
"*": "\\*",
"+": "\\+",
"?": "\\?",
".": "\\.",
"(": "\\(",
")": "\\)",
"|": "\\|",
"[": "\\[",
"]": "\\]",
"{": "\\{",
"}": "\\}",
"\\%": ".*",
"\\_": "."
};
let chars = parseLikePattern(pat, esc);
if (!chars.length) return new RegExp("^$", flags);
chars = chars.map(c => convChars[c] || c);
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
const l = chars.length - 1;
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
return new RegExp(chars.join(""), flags);
}
function evalExp(e: Expression): Expression {
if (e[0] === "AND") {
return reduce(e, (a, b) => {
if (!isArray(a)) return a ? b : a;
if (!isArray(b)) return b ? a : b;
return REDUCE_SKIP;
});
} else if (e[0] === "OR") {
return reduce(e, (a, b) => {
if (!isArray(a)) return a ? a : b;
if (!isArray(b)) return b ? b : a;
return REDUCE_SKIP;
});
} else if (e[0] === "NOT") {
if (!isArray(e[1])) return !e[1];
else if (e[1][0] === "NOT") return e[1][1];
} else if (e[0] === "IS NULL") {
if (isArray(e[1])) return e;
else if (e[1] == null) return true;
else return null;
} else if (e[0] === "IS NOT NULL") {
if (isArray(e[1])) return e;
else if (e[1] != null) return true;
else return null;
} else if (e[0] === "LIKE") {
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
else if (
e[1] == null ||
e[2] == null ||
((e as any[]).length >= 4 && e[3] == null)
)
return null;
let r = regExpCache.get(e as any[]);
if (!r) {
r = likePatternToRegExp(e[2], e[3]);
regExpCache.set(e as any[], r);
}
return r.test(e[1]);
} else if (e[0] === "NOT LIKE") {
if (isArray(e[1]) || isArray(e[2]) || isArray(e[3])) return e;
else if (
e[1] == null ||
e[2] == null ||
((e as any[]).length >= 4 && e[3] == null)
)
return null;
let r = regExpCache.get(e as any[]);
if (!r) {
r = likePatternToRegExp(e[2], e[3]);
regExpCache.set(e as any[], r);
}
return !r.test(e[1]);
} else if (e[0] === "=") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] === e[2];
} else if (e[0] === "<>") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] !== e[2];
} else if (e[0] === ">") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] > e[2];
} else if (e[0] === ">=") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] >= e[2];
} else if (e[0] === "<") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] < e[2];
} else if (e[0] === "<=") {
if (isArray(e[1]) || isArray(e[2])) return e;
if (e[1] == null || e[2] == null) return null;
return e[1] <= e[2];
} else if (e[0] === "*") {
return reduce(e, (a, b) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return a * b;
}
return REDUCE_SKIP;
});
} else if (e[0] === "/") {
return reduce(e, (a, b, i) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return i === 0 ? a / b : a * b;
}
return REDUCE_SKIP;
});
} else if (e[0] === "+") {
return reduce(e, (a, b) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return a + b;
}
return REDUCE_SKIP;
});
} else if (e[0] === "-") {
return reduce(e, (a, b, i) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return i === 0 ? a - b : a + b;
}
return REDUCE_SKIP;
});
} else if (e[0] === "||") {
return reduce(e, (a, b) => {
if (!isArray(a) && !isArray(b)) {
if (a == null || b == null) return null;
return `${a}${b}`;
}
return REDUCE_SKIP;
});
}
return e;
}
export function evaluate(
exp,
obj,
now: number,
cb?: Function
): string | number | boolean | null;
export function evaluate(exp, obj?, now?: number, cb?: Function): Expression;
export function evaluate(exp, obj?, now?: number, cb?: Function): Expression {
return map(exp, e => {
if (cb) e = cb(e);
if (!isArray(e)) return e;
if (e[0] === "FUNC") {
if (e[1] === "NOW") {
if (now) return now;
} else if (e[1] === "UPPER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
} else if (e[1] === "LOWER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
}
} else if (e[0] === "PARAM") {
if (e[1] == null) return null;
if (obj && !isArray(e[1])) {
let v;
if (typeof obj === "function") v = obj(e[1]);
else v = obj[e[1]];
if (v == null) return null;
if (typeof v === "object") v = v.value ? v.value[0] : null;
return v;
}
}
return evalExp(e);
});
}
export async function evaluateAsync(
exp,
obj,
now: number,
cb?: Function
): Promise<string | number | boolean | null>;
export async function evaluateAsync(
exp,
obj?,
now?: number,
cb?: Function
): Promise<Expression>;
export async function evaluateAsync(
exp,
obj?,
now?: number,
cb?: Function
): Promise<Expression> {
return mapAsync(exp, async e => {
if (cb) e = await cb(e);
if (!isArray(e)) return e;
if (e[0] === "FUNC") {
if (e[1] === "NOW") {
if (now) return now;
} else if (e[1] === "UPPER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toUpperCase();
} else if (e[1] === "LOWER") {
if (e[2] == null) return null;
if (!isArray(e[2])) return `${e[2]}`.toLowerCase();
}
} else if (e[0] === "PARAM") {
if (e[1] == null) return null;
if (obj && !isArray(e[1])) {
let v = obj[e[1]];
if (v == null) return null;
if (typeof v === "object") v = v.value ? v.value[0] : null;
return v;
}
}
return evalExp(e);
});
}
export function and(exp1, exp2): Expression {
if (!isArray(exp1)) return exp1 ? exp2 : exp1;
if (!isArray(exp2)) return exp2 ? exp1 : exp2;
let res: Expression = ["AND"];
if (exp1[0] === "AND") res = res.concat(exp1.slice(1));
else res.push(exp1);
if (exp2[0] === "AND") res = res.concat(exp2.slice(1));
else res.push(exp2);
return res;
}
export function or(exp1, exp2): Expression {
if (!isArray(exp1)) return exp1 ? exp1 : exp2;
if (!isArray(exp2)) return exp2 ? exp2 : exp1;
let res: Expression = ["OR"];
if (exp1[0] === "OR") res = res.concat(exp1.slice(1));
else res.push(exp1);
if (exp2[0] === "OR") res = res.concat(exp2.slice(1));
else res.push(exp2);
return res;
}
export function not(exp): Expression {
if (isArray(exp) && exp[0] === "NOT") return exp[1];
return ["NOT", exp];
}
export function subset(exp1, exp2): boolean {
const e = evaluate(["NOT", ["OR", ["NOT", exp1], exp2]]);
if (!isArray(e)) return !e;
const { vars, clauses } = booleanCnf(e);
return !naiveDpll(clauses, vars);
}
export function extractParams(exp): Expression[] {
const params = new Set<Expression>();
map(exp, e => {
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
return e;
});
return Array.from(params);
}
+87
View File
@@ -0,0 +1,87 @@
export function and(a: bigint, b: bigint): bigint {
return a & b;
}
export function or(a: bigint, b: bigint): bigint {
return a | b;
}
export function xor(a: bigint, b: bigint): bigint {
return a ^ b;
}
export function not(a: bigint): bigint {
return ~a;
}
export function lshift(a: bigint, b: bigint): bigint {
return a << b;
}
export function rshift(a: bigint, b: bigint): bigint {
return a >> b;
}
export function add(a: bigint, b: bigint): bigint {
return a + b;
}
export function sub(a: bigint, b: bigint): bigint {
return a - b;
}
export function mul(a: bigint, b: bigint): bigint {
return a * b;
}
export function div(a: bigint, b: bigint): bigint {
return a / b;
}
export function exp(a: bigint, b: bigint): bigint {
return a ** b;
}
export function rem(a: bigint, b: bigint): bigint {
return a % b;
}
export function toNumber(a: bigint): number {
return Number(a);
}
export function eq(a: bigint, b: bigint): boolean {
return a === b;
}
export function ne(a: bigint, b: bigint): boolean {
return a !== b;
}
export function lt(a: bigint, b: bigint): boolean {
return a < b;
}
export function lte(a: bigint, b: bigint): boolean {
return a <= b;
}
export function gt(a: bigint, b: bigint): boolean {
return a > b;
}
export function gte(a: bigint, b: bigint): boolean {
return a >= b;
}
export function asUintN(a: number, b: bigint): bigint {
return BigInt.asUintN(a, b);
}
export function asIntN(a: number, b: bigint): bigint {
return BigInt.asIntN(a, b);
}
const _BigInt = BigInt;
type _bigint = bigint;
export { _BigInt as BigInt, type _bigint as bigint };
+492
View File
@@ -0,0 +1,492 @@
import * as BI from "./bigint.ts";
import { Expression } from "../../types.ts";
import { map } from "./parser.ts";
import { and, evaluateCallback } from "./util.ts";
const ZERO = BI.BigInt(0);
const ONE = BI.BigInt(1);
const TWO = BI.BigInt(2);
const NEGATIVE_ONE = BI.BigInt(-1);
class Indeterminates {
public declare map: Map<string, number>;
public declare sortedKeys: string[];
public constructor(str?: string) {
this.map = new Map();
if (str) {
this.map.set(str, 1);
this.sortedKeys = [str];
} else {
this.sortedKeys = [];
}
}
public reciprocal(): Indeterminates {
const res = new Indeterminates();
res.sortedKeys = this.sortedKeys;
res.map = new Map();
for (const [k, v] of this.map) res.map.set(k, 0 - v);
return res;
}
public static multiply(
indeterminates1: Indeterminates,
indeterminates2: Indeterminates,
): Indeterminates {
const res = new Indeterminates();
res.sortedKeys = indeterminates1.sortedKeys.slice();
res.map = new Map(indeterminates1.map);
for (const [key, val] of indeterminates2.map) {
const v = res.map.get(key);
if (!v) {
res.map.set(key, val);
res.sortedKeys.push(key);
} else {
const v2 = val + v;
if (!v2) {
res.map.delete(key);
res.sortedKeys = res.sortedKeys.filter((s) => s !== key);
} else {
res.map.set(key, v2);
}
}
}
res.sortedKeys.sort((a, b) => {
if (a.length !== b.length) return b.length - a.length;
else if (a > b) return 1;
else if (a < b) return -1;
return 0;
});
return res;
}
public static compare(a: Indeterminates, b: Indeterminates): number {
if (a.sortedKeys.length !== b.sortedKeys.length)
return b.sortedKeys.length - a.sortedKeys.length;
for (let i = 0; i < a.sortedKeys.length; ++i) {
const k1 = a.sortedKeys[i];
const w1 = a.map.get(k1);
const k2 = b.sortedKeys[i];
const w2 = b.map.get(k2);
if (w1 !== w2) return w2 - w1;
if (k1.length > k2.length) return -1;
else if (k1.length < k2.length) return 1;
else if (k1 > k2) return 1;
else if (k1 < k2) return -1;
}
return 0;
}
}
interface Term {
indeterminates: Indeterminates;
coefficientNumerator: BI.bigint;
coefficientDenominator: BI.bigint;
}
function findGcd(a: BI.bigint, b: BI.bigint): BI.bigint {
while (BI.ne(b, ZERO)) {
const t = b;
b = BI.rem(a, b);
a = t;
}
return a;
}
class Polynomial {
public declare terms: Term[];
public constructor(terms: Term[]) {
this.terms = terms;
}
public static simplifyTerms(terms: Term[]): Term[] {
const ts = terms
.slice()
.sort((a: Term, b: Term) =>
Indeterminates.compare(a.indeterminates, b.indeterminates),
);
for (let i = 1; i < ts.length; ++i) {
const t1 = ts[i - 1];
const t2 = ts[i];
if (Indeterminates.compare(t1.indeterminates, t2.indeterminates) === 0) {
const numerator = BI.add(
BI.mul(t1.coefficientNumerator, t2.coefficientDenominator),
BI.mul(t2.coefficientNumerator, t1.coefficientDenominator),
);
const denominator = BI.mul(
t1.coefficientDenominator,
t2.coefficientDenominator,
);
const gcd = findGcd(numerator, denominator);
ts[i] = {
indeterminates: t2.indeterminates,
coefficientNumerator: BI.div(numerator, gcd),
coefficientDenominator: BI.div(denominator, gcd),
};
ts[i - 1] = {
indeterminates: t1.indeterminates,
coefficientNumerator: ZERO,
coefficientDenominator: t1.coefficientDenominator,
};
}
}
return ts.filter((v) => BI.ne(v.coefficientNumerator, ZERO));
}
public static fromIndeterminate(indeterminate: Expression): Polynomial {
const indeterminates = new Indeterminates(JSON.stringify(indeterminate));
const terms = [
{
indeterminates: indeterminates,
coefficientNumerator: ONE,
coefficientDenominator: ONE,
},
];
return new Polynomial(terms);
}
public static fromConstant(constant: number): Polynomial {
const [int, frac] = Math.abs(constant).toString(2).split(".", 2);
let numerator = BI.BigInt("0b" + int);
if (constant < 0) numerator = BI.mul(numerator, NEGATIVE_ONE);
let denominator = ONE;
if (frac) {
denominator = BI.exp(TWO, BI.BigInt(frac.length));
numerator = BI.add(
BI.mul(numerator, denominator),
BI.BigInt("0b" + frac),
);
}
const terms = [
{
indeterminates: new Indeterminates(),
coefficientNumerator: numerator,
coefficientDenominator: denominator,
},
];
return new Polynomial(terms);
}
public negation(): Polynomial {
const terms = this.terms.map((t) => ({
indeterminates: t.indeterminates,
coefficientNumerator: BI.mul(t.coefficientNumerator, NEGATIVE_ONE),
coefficientDenominator: t.coefficientDenominator,
}));
return new Polynomial(terms);
}
public reciprocal(): Polynomial {
const terms = this.terms.map((t) => ({
indeterminates: t.indeterminates.reciprocal(),
coefficientNumerator: t.coefficientDenominator,
coefficientDenominator: t.coefficientNumerator,
}));
return new Polynomial(terms);
}
public constant(): Polynomial {
const terms = this.terms.filter((t) => !t.indeterminates.sortedKeys.length);
return new Polynomial(terms);
}
public add(rhs: Polynomial): Polynomial {
return new Polynomial(
Polynomial.simplifyTerms(this.terms.concat(rhs.terms)),
);
}
public subtract(rhs: Polynomial): Polynomial {
return this.add(rhs.negation());
}
public multiply(rhs: Polynomial): Polynomial {
const terms: Term[] = [];
for (const t1 of this.terms) {
for (const t2 of rhs.terms) {
const numerator = BI.mul(
t1.coefficientNumerator,
t2.coefficientNumerator,
);
const denominator = BI.mul(
t1.coefficientDenominator,
t2.coefficientDenominator,
);
const gcd = findGcd(numerator, denominator);
terms.push({
indeterminates: Indeterminates.multiply(
t1.indeterminates,
t2.indeterminates,
),
coefficientNumerator: BI.div(numerator, gcd),
coefficientDenominator: BI.div(denominator, gcd),
});
}
}
return new Polynomial(Polynomial.simplifyTerms(terms));
}
public divide(rhs: Polynomial): Polynomial {
return this.multiply(rhs.reciprocal());
}
public toString(): string {
const add: string[] = [];
for (const t of this.terms) {
const coefficient =
BI.toNumber(t.coefficientNumerator) /
BI.toNumber(t.coefficientDenominator);
const mul: string[] = [];
if (t.indeterminates.sortedKeys.length) {
for (const k of t.indeterminates.sortedKeys) {
const w = t.indeterminates.map.get(k);
for (let i = Math.abs(w); i > 0; --i) {
if (w > 0) mul.push(k);
else mul.push(`["/",1,${k}]`);
}
}
if (coefficient !== 1) mul.push(coefficient.toString());
if (mul.length > 1) add.push(`["*",${mul.join(",")}]`);
else add.push(mul["0"]);
} else {
add.push(coefficient.toString());
}
}
if (!add.length) return "0";
else if (add.length === 1) return add[0];
else return `["+",${add.join(",")}]`;
}
}
const ADDITIVE_IDENTITY = Polynomial.fromConstant(0);
const MULTIPLICATIVE_IDENTITY = Polynomial.fromConstant(1);
const SWAPPED_OPS = {
"=": "=",
"<>": "<>",
">": "<",
">=": "<=",
"<": ">",
"<=": ">=",
};
function normalizeCallback(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp;
const op = exp[0];
if (op === "FUNC" && exp[1] === "COALESCE") {
const res: Expression[] = ["CASE"];
for (let i = 2; i < exp.length; ++i)
res.push(normalizeCallback(["IS NOT NULL", exp[i]]), exp[i]);
return normalizeCallback(res);
}
if (op === "CASE") {
const res = [] as [Expression, Expression][];
for (let i = 1; i < exp.length; i += 2) {
let w = exp[i];
if (w instanceof Polynomial) w = JSON.parse(w.toString());
if (!Array.isArray(w) && !w) continue;
const t = exp[i + 1];
if (!Array.isArray(t) || t[0] !== "CASE") {
res.push([w, t]);
continue;
}
for (let j = 1; j < t.length; j += 2) res.push([and(w, t[j]), t[j + 1]]);
res.push([w, null]);
if (!Array.isArray(w) && w) break;
}
while (res[res.length - 1][1] == null) res.pop();
return ["CASE", ...res.flat()];
}
const permutations: Map<number, [Expression, Expression][]> = new Map();
for (const [i, e] of exp.entries()) {
if (!Array.isArray(e)) continue;
if (e[0] !== "CASE") continue;
const perms: [Expression, Expression][] = [];
for (let j = 1; j < e.length; j += 2) perms.push([e[j], e[j + 1]]);
perms.push([true, null]);
permutations.set(i, perms);
}
if (permutations.size) {
let res: [Expression, Expression][] = [[true, exp]];
for (const [i, perms] of permutations) {
const res2: [Expression, Expression][] = [];
for (const [w, t] of perms) {
res2.push(
...res.map((r) => {
const e = (r[1] as Expression[]).slice();
e[i] = t;
return [and(w, r[0]), e] as [Expression, Expression];
}),
);
if (!Array.isArray(w) && w) break;
}
res = res2;
}
for (const r of res) r[1] = normalizeCallback(r[1]);
if (res[0][0] === true) return res[0][1];
while (res[res.length - 1][1] == null) res.pop();
return ["CASE", ...res.flat()];
}
function toPolynomial(e: Expression): Polynomial {
if (e == null) return null;
if (e instanceof Polynomial) return e;
if (typeof e === "number") return Polynomial.fromConstant(e);
if (typeof e === "string")
return Polynomial.fromConstant(parseFloat(e) || 0);
if (typeof e === "boolean") return Polynomial.fromConstant(+e);
return Polynomial.fromIndeterminate(e);
}
if (op === "+") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
}
return args.reduce(
(previousValue, currentValue) => previousValue.add(currentValue),
ADDITIVE_IDENTITY,
) as unknown as Expression;
} else if (op === "*") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
}
return args.reduce(
(previousValue, currentValue) => previousValue.multiply(currentValue),
MULTIPLICATIVE_IDENTITY,
) as unknown as Expression;
} else if (op === "-") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
}
return args.reduce((previousValue, currentValue) =>
previousValue.subtract(currentValue),
) as unknown as Expression;
} else if (op === "/") {
const args: Polynomial[] = [];
for (let i = 1; i < exp.length; ++i) {
const p = toPolynomial(exp[i]);
if (p == null) return null;
args.push(p);
}
return args.reduce((previousValue, currentValue) =>
previousValue.divide(currentValue),
) as unknown as Expression;
} else if (["=", "<>", ">", ">=", "<", "<="].includes(op)) {
if (exp[1] == null || exp[2] == null) return null;
let lhs: Polynomial, rhs: Polynomial;
if (exp[1] instanceof Polynomial) lhs = exp[1];
else if (typeof exp[1] === "number") lhs = Polynomial.fromConstant(exp[1]);
if (exp[2] instanceof Polynomial) rhs = exp[2];
else if (typeof exp[2] === "number") rhs = Polynomial.fromConstant(exp[2]);
if (lhs || rhs) {
if (!lhs) lhs = Polynomial.fromIndeterminate(exp[1]);
if (!rhs) rhs = Polynomial.fromIndeterminate(exp[2]);
lhs = lhs.subtract(rhs);
rhs = lhs.constant().negation();
lhs = lhs.add(rhs);
if (!lhs.terms.length) {
exp = [op, JSON.parse(lhs.toString()), JSON.parse(rhs.toString())];
if (op === "=") return exp[1] === exp[2];
else if (op === "<>") return exp[1] !== exp[2];
else if (op === ">") return exp[1] > exp[2];
else if (op === ">=") return exp[1] >= exp[2];
else if (op === "<") return exp[1] < exp[2];
else if (op === "<=") return exp[1] <= exp[2];
} else {
let flipOp = 1;
const n = lhs.terms[0].coefficientNumerator;
const d = lhs.terms[0].coefficientDenominator;
if (BI.lt(n, ZERO) || BI.lt(d, ZERO)) flipOp *= -1;
const reciprocal = new Polynomial([
{
indeterminates: new Indeterminates(),
coefficientNumerator: d,
coefficientDenominator: n,
},
]);
lhs = lhs.multiply(reciprocal);
rhs = rhs.multiply(reciprocal);
const keys = lhs.terms[0].indeterminates.sortedKeys;
let invert = lhs.terms[0].indeterminates.map.get(keys[0]) < 0 ? -1 : 0;
for (const t of lhs.terms)
for (const v of t.indeterminates.map.values()) invert += v;
if (invert < 0) {
flipOp *= -1;
lhs = lhs.reciprocal();
rhs = rhs.reciprocal();
}
if (flipOp < 0) exp = [SWAPPED_OPS[op], lhs, rhs];
else exp = [op, lhs, rhs];
}
}
}
// Restore polynomial expressions
exp = exp.map((e) =>
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
);
exp = evaluateCallback(exp);
return exp;
}
export default function normalize(expr: Expression): Expression {
expr = map(expr, normalizeCallback);
if (expr instanceof Polynomial) {
expr = JSON.parse(expr.toString());
} else if (Array.isArray(expr) && expr[0] === "CASE") {
expr = expr.map((e) =>
e instanceof Polynomial ? JSON.parse(e.toString()) : e,
);
}
return expr;
}
+148
View File
@@ -0,0 +1,148 @@
import { complement } from "espresso-iisojs";
import { Expression } from "../../types";
import normalize from "./normalize";
import { Clause, SynthContext } from "./synth";
import { and, or } from "./util";
type Bookmark = Record<string, null | boolean | number | string>;
type Minterm = number[];
export function toBookmark(
sort: Record<string, number>,
row: unknown,
): Bookmark {
const bookmark: Bookmark = {};
for (const param of Object.keys(sort)) {
let v = row[param];
if (v != null && typeof v === "object") v = v.value?.[0];
bookmark[param] = v;
}
return bookmark;
}
export function bookmarkToExpression(
bookmark: Bookmark,
sort: Record<string, number>,
): Expression {
return Object.entries(sort)
.reverse()
.reduce((cur, kv) => {
const [param, asc] = kv;
if (asc < 0) {
if (bookmark[param] == null) {
return or(
["IS NOT NULL", ["PARAM", param]],
and(["IS NULL", ["PARAM", param]], cur),
);
}
let f = null;
f = or(f, [">", ["PARAM", param], bookmark[param]]);
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
} else {
let f: Expression = ["IS NULL", ["PARAM", param]];
if (bookmark[param] == null) return and(f, cur);
f = or(f, ["<", ["PARAM", param], bookmark[param]]);
return or(f, and(["=", ["PARAM", param], bookmark[param]], cur));
}
}, true as Expression);
}
function getCover(
context: SynthContext,
minterm: Minterm,
allSort: [string, number][],
): Minterm[] {
if (!allSort.length) return [[]];
const [param, sort] = allSort[0];
const cov: Minterm = [];
const nextCov: Minterm = [];
if (sort > 0) {
const lhs = new Clause.Exp(["PARAM", param]);
const isNull = context.getVar(new Clause.IsNull(lhs));
cov.push((isNull << 2) ^ 2);
}
for (const m of minterm) {
const clause = context.getClause(m >>> 2);
if (clause instanceof Clause.IsNull) {
const lhs = clause.operand.expression();
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
if (lhs[1] !== param) continue;
nextCov.push(m);
if (sort < 0 && m & 1) cov.push(m, m ^ 1);
} else if (clause instanceof Clause.Compare) {
const lhs = clause.lhs.expression();
if (!Array.isArray(lhs) || lhs[0] !== "PARAM") continue;
if (lhs[1] !== param) continue;
if (!(m & 1) && sort > 0) continue;
nextCov.push(m);
const negate = (m ^ (m >> 1)) & 1;
if (sort > 0) {
if (
(clause.op === "=" && !negate) ||
(clause.op === ">" && !negate) ||
(clause.op === "<" && negate)
) {
const c = new Clause.Compare(clause.lhs, ">", clause.rhs);
const v = context.getVar(c);
cov.push((v << 2) ^ 3);
}
} else if (sort < 0) {
if (
(clause.op === "=" && !negate) ||
(clause.op === ">" && negate) ||
(clause.op === "<" && !negate)
) {
const c = new Clause.Compare(clause.lhs, "<", clause.rhs);
const v = context.getVar(c);
cov.push((v << 2) ^ 0);
}
}
}
}
const next = getCover(context, minterm, allSort.slice(1));
return [cov, ...next.map((n) => [...nextCov, ...n])];
}
export function paginate(
fetched: Expression,
toFetch: Expression,
sort: Record<string, number>,
): [Expression, Expression] {
fetched = normalize(fetched);
if (!fetched) return [false, toFetch];
toFetch = normalize(toFetch);
if (!toFetch) return [false, toFetch];
const synth1 = Clause.fromExpression(fetched);
const synth2 = Clause.fromExpression(toFetch);
const context = new SynthContext();
const expr1Minterms = synth1.true(context);
const expr2MintermsC = complement(synth2.true(context));
const gaps = context.sanitizeMinterms(
complement([
...expr1Minterms,
...expr2MintermsC,
...context.getDcSet([...expr1Minterms, ...expr2MintermsC]),
]),
);
const cover = gaps.flatMap((m) => getCover(context, m, Object.entries(sort)));
const minterms1 = context.minimize(complement([...cover, ...expr2MintermsC]));
const minterms2 = context.minimize(
complement([...minterms1, ...expr2MintermsC]),
);
return [context.toExpression(minterms1), context.toExpression(minterms2)];
}
@@ -1,24 +1,5 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import * as parsimmon from "parsimmon";
import { Expression } from "../types";
import parsimmon from "parsimmon";
import { Expression } from "../../types.ts";
// Turn escaped characters into real ones (e.g. "\\n" becomes "\n").
function interpretEscapes(str): string {
@@ -27,9 +8,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 +21,7 @@ function interpretEscapes(str): string {
});
}
export function map(exp, callback): Expression {
export function map<T>(exp: Expression, callback: (e: Expression) => T): T {
if (!Array.isArray(exp)) return callback(exp);
let clone;
@@ -55,7 +36,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 +54,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(),
@@ -78,15 +65,13 @@ function binaryLeft(operatorsParser, nextParser): parsimmon.Parser<{}> {
rest.reduce((acc, ch) => {
const [op, another] = ch;
if (Array.isArray(acc) && op === acc[0]) return acc.concat([another]);
if (Array.isArray(another) && op === another[0])
return [op, acc].concat(another.slice(1));
return [op, acc, another];
}, first)
}, first),
);
}
const lang = parsimmon.createLanguage({
ComparisonOperator: function() {
ComparisonOperator: function () {
return parsimmon
.alt(
parsimmon.string(">="),
@@ -94,26 +79,23 @@ const lang = parsimmon.createLanguage({
parsimmon.string("<="),
parsimmon.string("="),
parsimmon.string(">"),
parsimmon.string("<")
parsimmon.string("<"),
)
.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")
.desc("NOT LIKE")
.desc("NOT LIKE"),
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
IsNullOperator: function() {
IsNullOperator: function () {
return parsimmon
.alt(
parsimmon
@@ -123,12 +105,12 @@ const lang = parsimmon.createLanguage({
parsimmon
.regexp(/is\s+not\s+null/i)
.result("IS NOT NULL")
.desc("IS NOT NULL")
.desc("IS NOT NULL"),
)
.notFollowedBy(parsimmon.regexp(/[a-zA-Z0-9_]/))
.skip(parsimmon.optWhitespace);
},
NotOperator: function() {
NotOperator: function () {
return parsimmon
.regexp(/not/i)
.result("NOT")
@@ -136,7 +118,7 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace)
.desc("NOT");
},
AndOperator: function() {
AndOperator: function () {
return parsimmon
.regexp(/and/i)
.result("AND")
@@ -144,7 +126,7 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace)
.desc("AND");
},
OrOperator: function() {
OrOperator: function () {
return parsimmon
.regexp(/or/i)
.result("OR")
@@ -152,58 +134,53 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace)
.desc("OR");
},
Parameter: function(r) {
Parameter: function (r) {
return parsimmon
.alt(
parsimmon.regexp(/[a-zA-Z0-9_.*-]+/),
r.Expression.wrap(
parsimmon.string("{").skip(parsimmon.optWhitespace),
parsimmon.string("}")
)
parsimmon.string("}"),
),
)
.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 +188,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)
parsimmon.string(")").skip(parsimmon.optWhitespace),
),
(f, args) => ["FUNC", f.toUpperCase()].concat(args)
(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(
@@ -245,28 +263,32 @@ const lang = parsimmon.createLanguage({
.skip(parsimmon.optWhitespace),
binaryLeft(
parsimmon
.alt(parsimmon.string("*"), parsimmon.string("/"))
.alt(
parsimmon.string("*"),
parsimmon.string("/"),
parsimmon.string("%"),
)
.skip(parsimmon.optWhitespace),
parsimmon.alt(
r.Value,
r.Parameter,
r.Expression.wrap(
parsimmon.string("(").skip(parsimmon.optWhitespace),
parsimmon.string(")").skip(parsimmon.optWhitespace)
)
)
)
)
parsimmon.string(")").skip(parsimmon.optWhitespace),
),
),
),
),
);
},
Comparison: function(r) {
Comparison: function (r) {
return parsimmon.alt(
parsimmon.seqMap(r.ValueExpression, r.IsNullOperator, (p, o) => [o, p]),
parsimmon.seqMap(
r.ValueExpression,
r.ComparisonOperator,
r.ValueExpression,
(p, o, v) => [o, p, v]
(p, o, v) => [o, p, v],
),
parsimmon.seqMap(
r.ValueExpression,
@@ -276,21 +298,29 @@ const lang = parsimmon.createLanguage({
.regexp(/escape/i)
.result("ESCAPE")
.skip(parsimmon.whitespace)
.desc("ESCAPE")
.desc("ESCAPE"),
),
r.ValueExpression,
(a, b, c, d) => [b, a, c, d]
(a, b, c, d) => [b, a, c, d],
),
parsimmon.seqMap(
r.ValueExpression,
r.LikeOperator,
r.ValueExpression,
(a, b, c) => [b, a, c]
)
(a, b, c) => [b, a, c],
),
);
},
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);
}
@@ -298,10 +328,10 @@ const lang = parsimmon.createLanguage({
r.OrOperator,
binaryLeft(
r.AndOperator,
unary(r.NotOperator, r.Comparison.or(r.ValueExpression))
)
unary(r.NotOperator, r.Comparison.or(r.ValueExpression)),
),
).trim(parsimmon.optWhitespace);
}
},
});
export function parse(str: string): Expression {
@@ -309,7 +339,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 +365,8 @@ export function stringify(exp, level = 0): string {
"+": 31,
"-": 31,
"*": 32,
"/": 32
"/": 32,
"%": 32,
};
const op = exp[0].toUpperCase();
@@ -344,8 +380,8 @@ export function stringify(exp, level = 0): string {
return wrap(
`${exp[1]}(${exp
.slice(2)
.map(e => stringify(e))
.join(", ")})`
.map((e) => stringify(e))
.join(", ")})`,
);
} else if (op === "PARAM") {
if (typeof exp[1] === "string") {
@@ -354,11 +390,11 @@ 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)}}`;
})
.join("")
.join(""),
);
} else {
return wrap(`{${stringify(exp[1])}}`);
@@ -370,17 +406,28 @@ export function stringify(exp, level = 0): string {
return wrap(
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
exp[2],
opLevels[op]
)} ESCAPE ${stringify(exp[3], opLevels[op])}`
opLevels[op],
)} ESCAPE ${stringify(exp[3], opLevels[op])}`,
);
} else {
return wrap(
`${stringify(exp[1], opLevels[op])} ${op} ${stringify(
exp[2],
opLevels[op]
)}`
opLevels[op],
)}`,
);
}
} else if (op === "CASE") {
const parts: string[] = ["CASE"];
for (let i = 1; i < exp.length - 1; i += 2) {
if (!Array.isArray(exp[i]) && exp[i]) {
if (exp[i + 1] != null) parts.push("ELSE", stringify(exp[i + 1]));
break;
}
parts.push("WHEN", stringify(exp[i]), "THEN", stringify(exp[i + 1]));
}
parts.push("END");
return parts.join(" ");
} else if (op in opLevels) {
const parts = exp.slice(1).map((e, i) => {
return stringify(e, opLevels[exp[0]] + Math.min(i - 1, 0));
@@ -393,7 +440,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 +455,5 @@ export function parseLikePattern(pat, esc): string[] {
while (chars[i + 1] === "%") chars[++i] = "";
}
}
return chars.filter(c => c);
return chars.filter((c) => c);
}
+972
View File
@@ -0,0 +1,972 @@
import { espresso, complement, tautology } from "espresso-iisojs";
import { Expression } from "../../types.ts";
import normalize from "./normalize.ts";
import { map, parseLikePattern } from "./parser.ts";
type Minterm = number[];
export abstract class SynthContextBase<
T extends { toString: () => string } = unknown,
U = unknown,
> {
public variables = new Map<string, number>();
protected clauses = new Map<number, U>();
public getVar(c: U): number {
const str = c.toString();
let idx = this.variables.get(str);
if (idx == null) {
idx = this.variables.size;
this.variables.set(str, idx);
this.clauses.set(idx, c);
}
return idx;
}
public getClause(v: number): U {
return this.clauses.get(v);
}
abstract getMinterms(exp: T, res: true | false | null): Minterm[];
abstract getDcSet(minterms: Minterm[]): Minterm[];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canRaise(idx: number, set: Set<number>): boolean {
return true;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canLower(idx: number, set: Set<number>): boolean {
return true;
}
bias(a: number, b: number): number {
// Bias towards 1 then lower index
return (b ^ 1) - (a ^ 1);
}
sanitizeMinterms(minterms: Minterm[]): Minterm[] {
return minterms;
}
minimize(minterms: Minterm[], dcSet: Minterm[] = []): Minterm[] {
minterms = this.sanitizeMinterms(minterms);
const canRaise = this.canRaise.bind(this);
const canLower = this.canLower.bind(this);
const bias = this.bias.bind(this);
return espresso(
minterms,
[...this.getDcSet([...minterms, ...dcSet]), ...dcSet],
{
canRaise,
canLower,
bias,
},
);
}
}
function* findIsNullDeps(exp: Expression): IterableIterator<Expression> {
if (!Array.isArray(exp)) return;
const op = exp[0];
if (op === "IS NULL" || op === "IS NOT NULL") return;
if (op === "FUNC") {
if (exp[1] === "NOW") {
return;
} else if (exp[1] === "LOWER" || exp[1] === "UPPER") {
yield* findIsNullDeps(exp[2]);
} else if (exp[1] === "ROUND") {
for (const e of exp.slice(2, 4)) yield* findIsNullDeps(e);
return;
}
} else if (op !== "PARAM") {
for (const e of exp.slice(1)) yield* findIsNullDeps(e);
return;
}
yield exp;
}
export abstract class Clause {
private _isNullable: Set<string>;
protected _expression: Expression;
abstract true(context: SynthContextBase<Clause>): Minterm[];
abstract false(context: SynthContextBase<Clause>): Minterm[];
abstract null(context: SynthContextBase<Clause>): Minterm[];
expression(): Expression {
if (this._expression !== undefined) return this._expression;
const context = createSynthContext();
const minterms = this.true(context);
const minimized = context.minimize(minterms);
this._expression = context.toExpression(minimized) as Expression;
return this._expression;
}
isBoolean(): boolean {
return true;
}
isNullable(c: Clause.IsNull): boolean {
if (!this._isNullable) {
this._isNullable = new Set(
[...this.getNullables()].map((n) => n.toString()),
);
}
return this._isNullable.has(c.toString());
}
*getNullables(): IterableIterator<Clause.IsNull> {
const exp = this.expression();
for (const e of findIsNullDeps(exp)) {
if (e === exp) yield new Clause.IsNull(this);
else yield new Clause.IsNull(new Clause.Exp(e));
}
}
toString(): string {
return JSON.stringify(this.expression());
}
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Clause {
export class Not extends Clause {
constructor(public operand: Clause) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.false(context);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.true(context);
}
null(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.null(context);
}
}
export class Or extends Clause {
constructor(public operands: Clause[]) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
for (const s of this.operands) {
const m = s.true(context);
if (m.length === 1 && !m[0].length) return [[]];
minterms.push(...m);
}
return minterms;
}
false(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[][] = [];
const operands = [...this.operands];
for (const e of operands) {
if (e instanceof Or) {
operands.push(...e.operands);
continue;
}
const m = e.false(context);
if (!m.length) return [];
minterms.push(m);
}
if (!minterms.length) return [[]];
if (minterms.length === 1) return minterms[0];
return complement(minterms.map((m) => complement(m)).flat());
}
null(context: SynthContextBase<Clause>): Minterm[] {
const trueMinterms: Minterm[] = [];
const operands = [...this.operands];
for (const s of operands) {
if (s instanceof Or) {
operands.push(...s.operands);
continue;
}
const t = s.true(context);
if (t.length === 1 && !t[0].length) return [];
trueMinterms.push(...t);
}
const nullMinterms: Minterm[] = [];
for (const s of operands) {
const n = s.null(context);
if (n.length === 1 && !n[0].length && !trueMinterms.length) return [[]];
nullMinterms.push(...n);
}
if (!trueMinterms.length) return nullMinterms;
return complement([...complement(nullMinterms), ...trueMinterms]);
}
}
export class And extends Clause {
constructor(public operands: Clause[]) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[][] = [];
const operands = [...this.operands];
for (const s of operands) {
if (s instanceof And) {
operands.push(...s.operands);
continue;
}
const m = s.true(context);
if (!m.length) return [];
if (m.length === 1 && !m[0].length) continue;
minterms.push(m);
}
if (!minterms.length) return [[]];
if (minterms.length === 1) return minterms[0];
return complement(minterms.map((m) => complement(m)).flat());
}
false(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
for (const s of this.operands) {
const m = s.false(context);
if (m.length === 1 && !m[0].length) return [[]];
minterms.push(...m);
}
return minterms;
}
null(context: SynthContextBase<Clause>): Minterm[] {
const falseMinterms: Minterm[] = [];
const operands = [...this.operands];
for (const s of operands) {
if (s instanceof And) {
operands.push(...s.operands);
continue;
}
const f = s.false(context);
if (f.length === 1 && !f[0].length) return [];
falseMinterms.push(...f);
}
const nullMinterms: Minterm[] = [];
for (const s of operands) {
const n = s.null(context);
if (n.length === 1 && !n[0].length && !falseMinterms.length)
return [[]];
nullMinterms.push(...n);
}
if (!falseMinterms.length) return nullMinterms;
return complement([...complement(nullMinterms), ...falseMinterms]);
}
}
export class Case extends Clause {
constructor(public clauses: Clause[]) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
const cumulative: Minterm[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
const w = this.clauses[i].true(context);
const t = this.clauses[i + 1].true(context);
minterms.push(
...complement([...cumulative, ...complement(w), ...complement(t)]),
);
if (i < this.clauses.length - 2) {
cumulative.push(
...complement([
...this.clauses[i].false(context),
...this.clauses[i].null(context),
]),
);
}
}
return minterms;
}
false(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
const cumulative: Minterm[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
const w = this.clauses[i].true(context);
const t = this.clauses[i + 1].false(context);
minterms.push(
...complement([...cumulative, ...complement(w), ...complement(t)]),
);
if (i < this.clauses.length - 2) {
cumulative.push(
...complement([
...this.clauses[i].false(context),
...this.clauses[i].null(context),
]),
);
}
}
return minterms;
}
null(context: SynthContextBase<Clause>): Minterm[] {
const minterms: Minterm[] = [];
const cumulative: Minterm[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
const w = this.clauses[i].true(context);
const t = this.clauses[i + 1].null(context);
minterms.push(
...complement([...cumulative, ...complement(w), ...complement(t)]),
);
cumulative.push(
...complement([
...this.clauses[i].false(context),
...this.clauses[i].null(context),
]),
);
}
minterms.push(...complement([...cumulative]));
return minterms;
}
expression(): Expression {
if (this._expression != null) return this._expression;
if (this.isBoolean()) {
this._expression = new Clause.Not(new Clause.Not(this)).expression();
return this._expression;
}
const context = createSynthContext();
const cases: { when: Minterm[]; then: Expression }[] = [];
for (let i = 0; i < this.clauses.length; i += 2) {
let minterms = this.clauses[i].true(context);
const then = this.clauses[i + 1].expression();
if (
cases.length &&
JSON.stringify(then) === JSON.stringify(cases[cases.length - 1].then)
)
minterms.push(...cases.pop().when);
minterms = context.minimize(
minterms,
cases.flatMap((c) => c.when),
);
if (!minterms.length) continue;
cases.push({ when: minterms, then });
if (minterms.length === 1 && !minterms[0].length) break;
}
while (cases[cases.length - 1].then == null) cases.pop();
if (!cases.length) return null;
this._expression = [
"CASE",
...cases.flatMap((c) => [context.toExpression(c.when), c.then]),
];
return this._expression;
}
isBoolean(): boolean {
for (let i = 1; i < this.clauses.length; i += 2)
if (!this.clauses[i].isBoolean()) return false;
return true;
}
}
export class IsNull extends Clause {
private _boolean: Clause;
constructor(public operand: Clause) {
super();
this._boolean = new Not(new Not(this));
}
true(context: SynthContextBase<Clause>): Minterm[] {
return this.operand.null(context);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return complement(this.true(context));
}
null(): Minterm[] {
return [];
}
isBoolean(): boolean {
return true;
}
*getNullables(): IterableIterator<IsNull> {
// Never returns null
}
expression(): Expression {
const nullables = [...this.operand.getNullables()];
if (nullables.length === 1 && nullables[0].operand === this.operand)
return ["IS NULL", this.operand.expression()];
return super.expression();
}
}
export class Exp extends Clause {
constructor(public exp: Expression) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
if (Array.isArray(this.exp)) return context.getMinterms(this, true);
if (this.exp) return [[]];
return [];
}
false(context: SynthContextBase<Clause>): Minterm[] {
if (Array.isArray(this.exp)) return context.getMinterms(this, false);
if (!this.exp && this.exp != null) return [[]];
return [];
}
null(context: SynthContextBase<Clause>): Minterm[] {
if (Array.isArray(this.exp)) return context.getMinterms(this, null);
if (this.exp == null) return [[]];
return [];
}
expression(): Expression {
return this.exp;
}
isBoolean(): boolean {
return this.exp === true || this.exp === false || this.exp == null;
}
}
export class Compare extends Clause {
constructor(
public lhs: Clause,
public op: ">" | "<" | "=",
public rhs: boolean | number | string,
) {
super();
}
true(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, true);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, false);
}
null(context: SynthContextBase<Clause>): Minterm[] {
return this.lhs.null(context);
}
*getNullables(): IterableIterator<IsNull> {
yield* this.lhs.getNullables();
}
isBoolean(): boolean {
return true;
}
expression(): Expression {
return [this.op, this.lhs.expression(), this.rhs];
}
}
export class Like extends Clause {
public readonly caseSensitive: boolean;
public readonly contradiction: boolean;
public readonly pattern: string[];
public readonly lhs: Clause;
constructor(
lhs: Clause,
public rhs: string,
public esc?: string,
) {
super();
const exp = lhs.expression();
let caseSensitive = true;
let contradiction = false;
if (Array.isArray(exp)) {
if (exp[0] === "FUNC") {
const func = exp[1];
if (func === "UPPER" || func === "LOWER") {
const p = func === "UPPER" ? rhs.toUpperCase() : rhs.toLowerCase();
if (p === rhs) caseSensitive = false;
else contradiction = true;
lhs = new Exp(exp[2]);
}
}
}
this.lhs = lhs;
this.pattern = parseLikePattern(rhs, esc);
this.caseSensitive = caseSensitive;
this.contradiction = contradiction;
}
true(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, true);
}
false(context: SynthContextBase<Clause>): Minterm[] {
return context.getMinterms(this, false);
}
null(context: SynthContextBase<Clause>): Minterm[] {
return this.lhs.null(context);
}
isBoolean(): boolean {
return true;
}
isNullable(c: IsNull): boolean {
return this.lhs.isNullable(c);
}
getNullables(): IterableIterator<IsNull> {
return this.lhs.getNullables();
}
expression(): Expression {
let lhs = this.lhs.expression();
if (this.contradiction) {
if (this.rhs === this.rhs.toLocaleUpperCase())
lhs = ["FUNC", "LOWER", lhs];
else lhs = ["FUNC", "UPPER", lhs];
} else if (!this.caseSensitive) {
if (this.rhs === this.rhs.toLocaleUpperCase())
lhs = ["FUNC", "UPPER", lhs];
else lhs = ["FUNC", "LOWER", lhs];
}
return ["LIKE", lhs, this.rhs, ...(this.esc ? [this.esc] : [])];
}
}
export function fromExpression(exp: Expression): Clause {
const res = map(exp, (e) => {
if (!Array.isArray(e)) return new Clause.Exp(e);
let op = e[0];
let negate = true;
if (op === "NOT LIKE") op = "LIKE";
else if (op === "IS NOT NULL") op = "IS NULL";
else if (op === "<>") op = "=";
else if (op === ">=") op = "<";
else if (op === "<=") op = ">";
else negate = false;
let clause: Clause;
if (op === "IS NULL") {
clause = new Clause.IsNull(e[1]);
} else if (op === "NOT") {
clause = new Clause.Not(e[1]);
} else if (op === "OR") {
clause = new Clause.Or(e.slice(1));
} else if (op === "AND") {
clause = new Clause.And(e.slice(1));
} else if (op === "CASE") {
clause = new Clause.Case(e.slice(1));
} else if (op === "LIKE") {
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
const esc = e[3] instanceof Exp ? e[3].expression() : null;
if (typeof rhs === "string" && typeof esc === "string")
clause = new Clause.Like(e[1], rhs, esc);
else if (typeof rhs === "string" && esc == null)
clause = new Clause.Like(e[1], rhs);
} else if (op === ">" || op === "<" || op === "=") {
const rhs = e[2] instanceof Exp ? e[2].expression() : null;
if (["boolean", "number", "string"].includes(typeof rhs)) {
clause = new Clause.Compare(
e[1],
op,
rhs as boolean | number | string,
);
}
}
if (!clause) {
const args = e.slice(1).map((a) => (a as Clause).expression());
clause = new Clause.Exp([op, ...args]);
}
if (negate) clause = new Clause.Not(clause);
return clause;
});
return res;
}
}
function groupBy<T, K>(
input: T[],
callback: (item: T) => K,
): Iterable<[K, T[]]> {
const groups = new Map<K, T[]>();
for (const item of input) {
const key = callback(item);
let arr = groups.get(key);
if (!arr) groups.set(key, (arr = []));
arr.push(item);
}
return groups.entries();
}
export class SynthContext extends SynthContextBase<Clause, Clause> {
constructor() {
super();
}
getMinterms(clause: Clause, res: true | false | null): number[][] {
const v = this.getVar(clause);
if (res === true) return [[(v << 2) ^ 3]];
else if (res === false) return [[(v << 2) ^ 1]];
else return [[v << 2, (v << 2) ^ 2]];
}
getDcSet(minterms: Minterm[]): number[][] {
const dcSet: number[][] = [];
const whitelist = new Set([...minterms.flat()].map((v) => v >> 2));
const allClauses = [...whitelist].map((v) => this.getClause(v));
const comparisons = allClauses.filter(
(c) => c instanceof Clause.Compare,
) as Clause.Compare[];
// Comparisons
for (const [, clauses] of groupBy(comparisons, (c) =>
JSON.stringify(c.lhs),
)) {
const lhs = clauses[0].lhs;
const values = new Set(clauses.map((c) => c.rhs));
const valuesSorted = [...values].sort((a, b) => {
const ta = typeof a;
const tb = typeof b;
if (ta === tb) return a > b ? 1 : -1;
else if (ta === "string") return 1;
else if (tb === "string") return -1;
return +a - +b;
});
for (const [i, v] of valuesSorted.entries()) {
const eq = this.getVar(new Clause.Compare(lhs, "=", v));
const gt = this.getVar(new Clause.Compare(lhs, ">", v));
const lt = this.getVar(new Clause.Compare(lhs, "<", v));
dcSet.push([(eq << 2) ^ 3, (gt << 2) ^ 3]);
dcSet.push([(lt << 2) ^ 3, (gt << 2) ^ 3]);
dcSet.push([(lt << 2) ^ 3, (eq << 2) ^ 3]);
dcSet.push([(lt << 2) ^ 1, (eq << 2) ^ 1, (gt << 2) ^ 1]);
const negEquivOp = [lt, eq, gt].filter((o) => !whitelist.has(o));
if (negEquivOp.length === 1) whitelist.add(negEquivOp[0]);
for (let j = 0; j < i; j++) {
const eq2 = this.getVar(
new Clause.Compare(lhs, "=", valuesSorted[j]),
);
const gt2 = this.getVar(
new Clause.Compare(lhs, ">", valuesSorted[j]),
);
const lt2 = this.getVar(
new Clause.Compare(lhs, "<", valuesSorted[j]),
);
// This is the minimum clauses required if all relavent vars
// were included in the DC set.
// dcSet.push([(eq << 2) ^ 3, (eq2 << 2) ^ 3]);
// dcSet.push([(lt << 2) ^ 1, (gt2 << 2) ^ 1]);
// But we use non-minimal set because intermediate vars
// between any two may not be present in the DC set.
dcSet.push([(gt2 << 2) ^ 1, (lt << 2) ^ 1]);
dcSet.push([(eq2 << 2) ^ 3, (lt << 2) ^ 1]);
dcSet.push([(lt2 << 2) ^ 3, (lt << 2) ^ 1]);
dcSet.push([(gt2 << 2) ^ 1, (gt << 2) ^ 3]);
dcSet.push([(gt2 << 2) ^ 1, (eq << 2) ^ 3]);
dcSet.push([(eq2 << 2) ^ 3, (gt << 2) ^ 3]);
dcSet.push([(eq2 << 2) ^ 3, (eq << 2) ^ 3]);
dcSet.push([(lt2 << 2) ^ 3, (gt << 2) ^ 3]);
dcSet.push([(lt2 << 2) ^ 3, (eq << 2) ^ 3]);
}
}
}
// LIKE
const likes = allClauses.filter(
(c) => c instanceof Clause.Like,
) as Clause.Like[];
for (const [, clauses] of groupBy(likes, (c) => JSON.stringify(c.lhs))) {
for (let i1 = 0; i1 < clauses.length; ++i1) {
const l1 = clauses[i1];
if (l1.contradiction) {
dcSet.push([(this.getVar(l1) << 2) ^ 3]);
continue;
}
for (let i2 = i1 + 1; i2 < clauses.length; ++i2) {
const l2 = clauses[i2];
if (l2.contradiction) continue;
let p1 = l1.pattern;
let p2 = l2.pattern;
if (!l1.caseSensitive || !l2.caseSensitive) {
p1 = p1.map((c) => c.toLowerCase());
p2 = p2.map((c) => c.toLowerCase());
}
if (likeDisjoint(p1, p2)) {
dcSet.push([
(this.getVar(l1) << 2) ^ 3,
(this.getVar(l2) << 2) ^ 3,
]);
} else if (
(!l1.caseSensitive || l2.caseSensitive) &&
likeImplies(p1, p2)
) {
dcSet.push([
(this.getVar(l1) << 2) ^ 2,
(this.getVar(l2) << 2) ^ 3,
]);
dcSet.push([
(this.getVar(l1) << 2) ^ 1,
(this.getVar(l2) << 2) ^ 0,
]);
} else if (
(!l2.caseSensitive || l1.caseSensitive) &&
likeImplies(p2, p1)
) {
dcSet.push([
(this.getVar(l1) << 2) ^ 3,
(this.getVar(l2) << 2) ^ 2,
]);
dcSet.push([
(this.getVar(l1) << 2) ^ 0,
(this.getVar(l2) << 2) ^ 1,
]);
}
}
}
}
for (const v of whitelist) {
const clause = this.getClause(v);
const nullables = [...clause.getNullables()].map((c) => this.getVar(c));
if (nullables.length) {
dcSet.push([
...nullables.map((n) => (n << 2) ^ 2),
(v << 2) ^ 0,
(v << 2) ^ 2,
]);
for (const n of nullables) {
dcSet.push([(n << 2) ^ 3, (v << 2) ^ 1]);
dcSet.push([(n << 2) ^ 3, (v << 2) ^ 3]);
whitelist.add(n);
}
}
if (clause instanceof Clause.IsNull) {
const str = clause.operand.toString();
if (str === '["PARAM","DeviceID.ID"]' || str === '["PARAM","_id"]')
dcSet.push([(v << 2) ^ 3]);
}
}
return dcSet.filter((m) => m.every((v) => whitelist.has(v >> 2)));
}
canRaise(idx: number, set: Set<number>): boolean {
const clause = this.getClause(idx >> 2);
if (clause instanceof Clause.IsNull) {
for (const i of set) {
if (i === idx || i & 1) continue;
const c = this.getClause(i >> 2);
if (c.isNullable(clause)) return false;
}
return true;
}
return !(idx & 1) || !set.has(idx ^ 3);
}
canLower(idx: number, set: Set<number>): boolean {
if (idx & 1) return true;
const clause = this.getClause(idx >> 2);
if (clause instanceof Clause.IsNull) return true;
return set.has(idx ^ 3) || set.has(idx ^ 1);
}
bias(a: number, b: number): number {
// Bias towards 1 then true
return ((b & 3) ^ 3) - ((a & 3) ^ 3);
}
sanitizeMinterms(minterms: Minterm[]): Minterm[] {
const res = [] as number[][];
loop: for (const m of minterms) {
const merged: Map<number, number> = new Map();
for (const i of m)
merged.set(i >> 2, (merged.get(i >> 2) || 0) | (1 << (i & 3)));
const minterm: number[] = [];
const perms: number[][] = [];
for (const [k, v] of merged) {
if ((v & 0b0011) === 0b0011) continue loop;
if ((v & 0b1100) === 0b1100) continue loop;
const clause = this.clauses.get(k);
if (!clause) throw new Error("Invalid literal");
if (clause instanceof Clause.IsNull) {
if (v === 0b0100) minterm.push((k << 2) ^ 2);
else if (v === 0b1000) minterm.push((k << 2) ^ 3);
else throw new Error("Invalid literal");
continue;
}
if ((v & 0b1010) === 0b1010) continue loop;
const isNullVars = [...clause.getNullables()].map((c) =>
this.getVar(c),
);
const t = k << 2;
if (v === 0b0101) {
if (isNullVars.length === 1) minterm.push((isNullVars[0] << 2) ^ 3);
else perms.push(isNullVars.map((n) => (n << 2) ^ 3));
} else if (v === 0b0001) {
perms.push([...isNullVars.map((n) => (n << 2) ^ 3), t ^ 3]);
} else if (v === 0b0100) {
perms.push([...isNullVars.map((n) => (n << 2) ^ 3), t ^ 1]);
} else if (v & 0b1000) {
minterm.push(t ^ 3);
} else if (v & 0b0010) {
minterm.push(t ^ 1);
}
}
let ms = [minterm];
while (perms.length) {
const newMs: number[][] = [];
const perm = perms.pop();
for (const p of perm) newMs.push(...ms.map((mm) => [...mm, p]));
ms = newMs;
}
res.push(...ms);
}
return res;
}
toExpression(sop: number[][]): Expression {
if (!sop.length) return false;
const res: Expression[] = [];
for (const s of sop) {
if (!s.length) return true;
const conjs: Expression[] = [];
for (const i of s) {
const clause = this.getClause(i >>> 2);
if (!clause) throw new Error("Invalid literal");
if (clause instanceof Clause.IsNull) {
if (!(i & 2)) throw new Error("Invalid literal");
} else if (!(i & 1)) {
// Should never be reached if minimized correctly
const isNullVars = [...clause.getNullables()].map((c) =>
this.getVar(c),
);
conjs.push(
this.toExpression([
[i ^ 3],
...isNullVars.map((n) => [(n << 2) ^ 3]),
]),
);
continue;
}
let expr = clause.expression() as Expression;
if (!(i & 1) !== !(i & 2)) expr = ["NOT", expr];
if (
Array.isArray(expr) &&
expr[0] === "NOT" &&
Array.isArray(expr[1])
) {
const e: Expression[] = expr[1];
if (e[0] === "IS NULL") expr = ["IS NOT NULL", ...e.slice(1)];
else if (e[0] === "LIKE") expr = ["NOT LIKE", ...e.slice(1)];
else if (e[0] === "=") expr = ["<>", ...e.slice(1)];
else if (e[0] === "<>") expr = ["=", ...e.slice(1)];
else if (e[0] === ">") expr = ["<=", ...e.slice(1)];
else if (e[0] === ">=") expr = ["<", ...e.slice(1)];
else if (e[0] === "<") expr = [">=", ...e.slice(1)];
else if (e[0] === "<=") expr = [">", ...e.slice(1)];
else if (e[0] === "NOT") expr = e[1];
}
conjs.push(expr);
}
if (conjs.length > 1) res.push(["AND", ...conjs]);
else res.push(conjs[0]);
}
if (res.length > 1) return ["OR", ...res];
return res[0];
}
}
// Classes aren't hoisted in JS but functions are. This function is used to
// create a new SynthContext instance from inside the Clause class.
export function createSynthContext(): SynthContext {
return new SynthContext();
}
export function likeImplies(pat1: string[], pat2: string[]): boolean {
let backtrack: [number, number] = null;
for (let i1 = 0, i2 = 0; ; ++i1, ++i2) {
while (i1 < pat1.length && pat1[i1] === "\\%") backtrack = [i1++, i2];
if (i2 >= pat2.length) return i1 >= pat1.length;
const c = i1 < pat1.length ? pat1[i1] : null;
if (c !== pat2[i2] && c !== "\\_") {
if (!backtrack) return false;
[i1, i2] = backtrack;
++backtrack[1];
}
}
}
export function likeDisjoint(pat1: string[], pat2: string[]): boolean {
const left1Idx = pat1.indexOf("\\%");
const left2Idx = pat2.indexOf("\\%");
const right1Idx = pat1.lastIndexOf("\\%");
const right2Idx = pat2.lastIndexOf("\\%");
const left1 = pat1.slice(0, left1Idx !== -1 ? left1Idx : pat1.length);
const left2 = pat2.slice(0, left2Idx !== -1 ? left2Idx : pat2.length);
const right1 = pat1.slice(right1Idx !== -1 ? right1Idx + 1 : 0).reverse();
const right2 = pat2.slice(right2Idx !== -1 ? right2Idx + 1 : 0).reverse();
for (let i = 0; i < Math.min(left1.length, left2.length); ++i) {
if (left1[i] !== left2[i] && left1[i] !== "\\_" && left2[i] !== "\\_")
return true;
}
for (let i = 0; i < Math.min(right1.length, right2.length); ++i) {
if (right1[i] !== right2[i] && right1[i] !== "\\_" && right2[i] !== "\\_")
return true;
}
if (pat1.length === left1.length)
return pat2.filter((c) => c !== "\\%").length > pat1.length;
else if (pat2.length === left2.length)
return pat1.filter((c) => c !== "\\%").length > pat2.length;
return false;
}
export function minimize(expr: Expression, boolean = false): Expression {
let synth = Clause.fromExpression(normalize(expr));
if (boolean) synth = new Clause.Not(new Clause.Not(synth));
return synth.expression();
}
export function unionDiff(
expr1: Expression,
expr2: Expression,
): [Expression, Expression] {
expr2 = normalize(expr2);
if (!expr2) return [expr1, false];
const synth2 = Clause.fromExpression(expr2);
if (!expr1) {
const e = synth2.expression();
return [e, e];
}
expr1 = normalize(expr1);
const synth1 = Clause.fromExpression(expr1);
const context = new SynthContext();
const expr2Minterms = synth2.true(context);
const expr1Minterms = synth1.true(context);
const union = context.minimize([...expr1Minterms, ...expr2Minterms]);
const diff = context.minimize(
complement([...expr1Minterms, ...complement(expr2Minterms)]),
);
return [context.toExpression(union), context.toExpression(diff)];
}
export function covers(expr1: Expression, expr2: Expression): boolean {
expr2 = normalize(expr2);
if (!expr2) return true;
expr1 = normalize(expr1);
if (!Array.isArray(expr1)) return !!expr1;
const synt1 = Clause.fromExpression(expr1);
const synt2 = Clause.fromExpression(expr2);
const context = new SynthContext();
const expr1Minterms = synt1.true(context);
const expr2Minterms = synt2.true(context);
return tautology([
...context.sanitizeMinterms(complement(expr2Minterms)),
...context.getDcSet([...expr2Minterms, ...expr1Minterms]),
...context.sanitizeMinterms(expr1Minterms),
]);
}
+393
View File
@@ -0,0 +1,393 @@
import { map, mapAsync, parseLikePattern } from "./parser.ts";
import { Expression } from "../../types.ts";
const isArray = Array.isArray;
const regExpCache = new WeakMap<any, RegExp>();
const REDUCE_SKIP = {};
function reduce(exp, callback): Expression {
let loop = true;
while (loop) {
loop = false;
for (let i = 2; i < exp.length; ++i) {
const res = callback(exp[i - 1], exp[i], i - 2);
if (res !== REDUCE_SKIP) {
loop = true;
exp = exp.slice();
exp.splice(i - 1, 2, res);
}
}
}
if (exp.length === 2) return exp[1];
return exp;
}
export function likePatternToRegExp(pat: string, esc = "", flags = ""): RegExp {
const convChars = {
"-": "\\-",
"/": "\\/",
"\\": "\\/",
"^": "\\^",
$: "\\$",
"*": "\\*",
"+": "\\+",
"?": "\\?",
".": "\\.",
"(": "\\(",
")": "\\)",
"|": "\\|",
"[": "\\[",
"]": "\\]",
"{": "\\{",
"}": "\\}",
"\\%": ".*",
"\\_": ".",
};
let chars = parseLikePattern(pat, esc);
if (!chars.length) return new RegExp("^$", flags);
chars = chars.map((c) => convChars[c] || c);
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
const l = chars.length - 1;
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
return new RegExp(chars.join(""), flags);
}
function compare(
a: boolean | number | string,
b: boolean | number | string,
): number {
if (typeof a === "boolean") a = +a;
if (typeof b === "boolean") b = +b;
if (typeof a !== typeof b) return typeof a === "string" ? 1 : -1;
return a > b ? 1 : a < b ? -1 : 0;
}
function toNumber(a: boolean | number | string): number {
switch (typeof a) {
case "number":
return a;
case "boolean":
return +a;
case "string":
return parseFloat(a) || 0;
}
}
function toString(a: boolean | number | string): string {
switch (typeof a) {
case "string":
return a;
case "number":
return a.toString();
case "boolean":
return (+a).toString();
}
}
export function evaluateCallback(exp: Expression): Expression {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "CASE") {
for (let i = 1; i < exp.length; i += 2) {
if (Array.isArray(exp[i])) return exp;
if (exp[i]) return exp[i + 1];
}
return null;
} else if (exp[0] === "FUNC") {
if (exp[1] === "COALESCE") {
const args: Expression[] = [];
for (let i = 2; i < exp.length; ++i) {
const e = exp[i];
if (e == null) continue;
args.push(e);
if (!Array.isArray(e)) break;
}
if (!args.length) return null;
if (args.length === 1) return args[0];
return ["FUNC", "COALESCE", ...args];
} else if (exp[1] === "UPPER") {
if (exp[2] == null) return null;
if (!isArray(exp[2])) return toString(exp[2]).toUpperCase();
} else if (exp[1] === "LOWER") {
if (exp[2] == null) return null;
if (!isArray(exp[2])) return toString(exp[2]).toLowerCase();
} else if (exp[1] === "ROUND") {
const n = exp[2];
const p = exp.length > 3 ? exp[3] : 0;
if (n == null || p == null) return null;
if (!isArray(n) && !isArray(p)) {
const d = 10 ** p;
const m = n * d * (1 + Number.EPSILON);
return Math.round(m) / d;
}
}
} else if (exp[0] === "PARAM") {
if (exp[1] == null) return null;
} else if (exp[0] === "AND") {
for (let i = 1; i < exp.length; ++i)
if (!Array.isArray(exp[i]) && exp[i] != null && !exp[i]) return false;
const args: Expression[] = [];
for (let i = 1; i < exp.length; ++i) {
const ee = exp[i];
if (ee == null) return null;
if (Array.isArray(ee)) {
if (ee[0] === "AND") args.push(...ee.slice(1));
else args.push(ee);
}
}
if (!args.length) return true;
if (args.length === 1) args.push(true);
return ["AND", ...args];
} else if (exp[0] === "OR") {
const args: Expression[] = [];
for (let i = 1; i < exp.length; ++i) {
const ee = exp[i];
if (Array.isArray(ee)) {
if (ee[0] === "OR") args.push(...ee.slice(1));
else args.push(ee);
} else if (ee) {
return true;
}
}
if (!args.length) return exp.some((ee) => ee == null) ? null : false;
if (args.length === 1) args.push(false);
return ["OR", ...args];
} else if (exp[0] === "NOT") {
if (exp[1] == null) return null;
if (!isArray(exp[1])) return !exp[1];
else if (exp[1][0] === "NOT") return exp[1][1];
} else if (exp[0] === "IS NULL") {
if (isArray(exp[1])) return exp;
else if (exp[1] == null) return true;
else return false;
} else if (exp[0] === "IS NOT NULL") {
if (isArray(exp[1])) return exp;
else if (exp[1] != null) return true;
else return false;
} else if (exp[0] === "LIKE") {
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
else if (
exp[1] == null ||
exp[2] == null ||
((exp as any[]).length >= 4 && exp[3] == null)
)
return null;
let r = regExpCache.get(exp as any[]);
if (!r) {
r = likePatternToRegExp(exp[2], exp[3]);
regExpCache.set(exp as any[], r);
}
return r.test(exp[1]);
} else if (exp[0] === "NOT LIKE") {
if (isArray(exp[1]) || isArray(exp[2]) || isArray(exp[3])) return exp;
else if (
exp[1] == null ||
exp[2] == null ||
((exp as any[]).length >= 4 && exp[3] == null)
)
return null;
let r = regExpCache.get(exp as any[]);
if (!r) {
r = likePatternToRegExp(exp[2], exp[3]);
regExpCache.set(exp as any[], r);
}
return !r.test(exp[1]);
} else if (exp[0] === "=") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) === 0;
} else if (exp[0] === "<>") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) !== 0;
} else if (exp[0] === ">") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) > 0;
} else if (exp[0] === ">=") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) >= 0;
} else if (exp[0] === "<") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) < 0;
} else if (exp[0] === "<=") {
if (exp[1] == null || exp[2] == null) return null;
if (isArray(exp[1]) || isArray(exp[2])) return exp;
return compare(exp[1], exp[2]) <= 0;
} else if (exp[0] === "*") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toNumber(a) * toNumber(b);
return REDUCE_SKIP;
});
} else if (exp[0] === "/") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (isArray(a) || isArray(b)) return REDUCE_SKIP;
const n1 = toNumber(a);
const n2 = toNumber(b);
if (i !== 0) return n1 * n2;
if (n2 === 0) return null;
return n1 / n2;
});
} else if (exp[0] === "+") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toNumber(a) + toNumber(b);
return REDUCE_SKIP;
});
} else if (exp[0] === "-") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b))
return i === 0 ? toNumber(a) - toNumber(b) : toNumber(a) + toNumber(b);
return REDUCE_SKIP;
});
} else if (exp[0] === "%") {
return reduce(exp, (a, b, i) => {
if (a == null || b == null) return null;
if (isArray(a) || isArray(b) || i !== 0) return REDUCE_SKIP;
const n1 = toNumber(a);
const n2 = Math.trunc(toNumber(b));
if (n2 === 0) return null;
return n1 % n2;
});
} else if (exp[0] === "||") {
return reduce(exp, (a, b) => {
if (a == null || b == null) return null;
if (!isArray(a) && !isArray(b)) return toString(a) + toString(b);
return REDUCE_SKIP;
});
}
return exp;
}
export function evaluate(
exp: Expression,
obj: Record<string, unknown> | ((e: string) => Expression),
now: number,
cb?: (e: Expression) => Expression,
): string | number | boolean | null;
export function evaluate(
exp: Expression,
obj?: Record<string, unknown> | ((e: string) => Expression),
now?: number,
cb?: (e: Expression) => Expression,
): Expression;
export function evaluate(
exp: Expression,
obj?: Record<string, unknown> | ((e: string) => Expression),
now?: number,
cb?: (e: Expression) => Expression,
): Expression {
return map(exp, (e) => {
if (cb) e = cb(e);
if (!isArray(e)) return e;
if (e[0] === "FUNC" && e[1] === "NOW") {
if (now) return now;
} else if (e[0] === "PARAM") {
if (e[1] == null) return null;
if (obj && !isArray(e[1])) {
let v;
if (typeof obj === "function") v = obj(e[1]);
else v = obj[e[1]];
if (v == null) return null;
if (typeof v === "object") v = v.value ? v.value[0] : null;
return v;
}
}
return evaluateCallback(e);
});
}
export async function evaluateAsync(
exp: Expression,
obj: Record<string, unknown>,
now: number,
cb?: (e: Expression) => Promise<Expression>,
): Promise<string | number | boolean | null>;
export async function evaluateAsync(
exp: Expression,
obj?: Record<string, unknown>,
now?: number,
cb?: (e: Expression) => Promise<Expression>,
): Promise<Expression>;
export async function evaluateAsync(
exp: Expression,
obj?: Record<string, unknown>,
now?: number,
cb?: (e: Expression) => Promise<Expression>,
): Promise<Expression> {
return mapAsync(exp, async (e) => {
if (cb) e = await cb(e);
if (!isArray(e)) return e;
if (e[0] === "FUNC") {
if (e[1] === "NOW") if (now) return now;
} else if (e[0] === "PARAM") {
if (e[1] == null) return null;
if (obj && !isArray(e[1])) {
let v = obj[e[1]];
if (v == null) return null;
if (typeof v === "object") v = v["value"] ? v["value"][0] : null;
return v as Expression;
}
}
return evaluateCallback(e);
});
}
export function and(exp1: Expression, exp2: Expression): Expression {
if (exp1 != null && !exp1) return false;
if (exp2 != null && !exp2) return false;
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
if (exp1 == null || exp2 == null) return null;
return true;
}
if (!Array.isArray(exp2) && exp1[0] === "AND") return exp1;
if (!Array.isArray(exp1) && exp2[0] === "AND") return exp2;
const res: Expression = ["AND"];
if (Array.isArray(exp1) && exp1[0] === "AND") res.push(...exp1.slice(1));
else res.push(exp1);
if (Array.isArray(exp2) && exp2[0] === "AND") res.push(...exp2.slice(1));
else res.push(exp2);
return res;
}
export function or(exp1: Expression, exp2: Expression): Expression {
if (!Array.isArray(exp1) && exp1) return true;
if (!Array.isArray(exp2) && exp2) return true;
if (!Array.isArray(exp1) && !Array.isArray(exp2)) {
if (exp1 == null || exp2 == null) return null;
return false;
}
if (!Array.isArray(exp2) && exp1[0] === "OR") return exp1;
if (!Array.isArray(exp1) && exp2[0] === "OR") return exp2;
const res: Expression = ["OR"];
if (Array.isArray(exp1) && exp1[0] === "OR") res.push(...exp1.slice(1));
else res.push(exp1);
if (Array.isArray(exp2) && exp2[0] === "OR") res.push(...exp2.slice(1));
else res.push(exp2);
return res;
}
export function extractParams(exp: Expression): Expression[] {
const params = new Set<Expression>();
map(exp, (e) => {
if (isArray(e) && e[0] === "PARAM") params.add(e[1]);
return e;
});
return Array.from(params);
}
+13 -23
View File
@@ -1,22 +1,3 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
let cache1 = new Map();
let cache2 = new Map();
const keys = new WeakMap();
@@ -39,7 +20,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 +28,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;
}
+12 -31
View File
@@ -1,28 +1,9 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import Path from "./path";
import Path from "./path.ts";
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,9 +54,9 @@ export default class PathSet {
public find(
path: Path,
superset: boolean = false,
subset: boolean = false,
depth: number = path.length
superset = false,
subset = false,
depth: number = path.length,
): Path[] {
if (path.alias) throw new Error("PathSet does not support aliased paths");
@@ -106,24 +87,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;
}
+114 -77
View File
@@ -1,66 +1,105 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
type Segments = (string | Alias)[];
type 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,
): { 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
@@ -78,32 +117,29 @@ export default class Path {
protected static parsePath(
pattern: string,
index: number
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 +154,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 +178,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 +196,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 +248,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);
-175
View File
@@ -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);
}
+218
View File
@@ -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";
}
+37 -79
View File
@@ -1,24 +1,5 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { resolve } from "path";
import { readFileSync, existsSync } from "fs";
import { resolve } from "node:path";
import { readFileSync, existsSync } from "node:fs";
// Find project root directory
export let ROOT_DIR = resolve(__dirname, "..");
@@ -32,27 +13,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 +40,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 +57,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 +80,11 @@ 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 },
XMPP_JID: { type: "string", default: "" },
XMPP_PASSWORD: { type: "string", default: "" },
};
const allConfig: { [name: string]: string | number } = {};
@@ -120,37 +96,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 +127,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 +134,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,13 +224,22 @@ 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"]);
export function get(
optionName: string,
deviceId?: string
deviceId?: string,
): string | number | boolean {
if (!deviceId) return allConfig[optionName];
@@ -310,7 +268,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;
+173 -113
View File
@@ -1,43 +1,25 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import * as crypto from "crypto";
import * as dgram from "dgram";
import { parse } from "url";
import * as http from "http";
import { evaluateAsync } from "./common/expression";
import { Expression } from "./types";
import * as auth from "./auth";
import * as extensions from "./extensions";
import * as debug from "./debug";
import * as crypto from "node:crypto";
import * as dgram from "node:dgram";
import * as http from "node:http";
import { evaluateAsync } from "./common/expression/util.ts";
import { Expression } from "./types.ts";
import * as auth from "./auth.ts";
import * as extensions from "./extensions.ts";
import * as debug from "./debug.ts";
import XmppClient from "./xmpp-client.ts";
import * as config from "../lib/config.ts";
import { encodeEntities, parseAttrs, Element } from "./xml-parser.ts";
import * as logger from "../lib/logger.ts";
async function extractAuth(
exp: Expression,
context: {},
now: number,
dflt: any
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,44 +32,42 @@ 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;
}
}
return e;
}
},
);
return [username, password, _exp];
}
function httpGet(
url: URL,
options: http.RequestOptions,
timeout,
_debug: boolean,
deviceId: string
): Promise<{ statusCode: number; headers: {} }> {
deviceId: string,
): 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 +75,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");
deviceId: string,
): 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,16 +96,15 @@ 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
options,
);
} else if (authHeader["method"] === "Digest") {
opts = Object.assign(
@@ -137,57 +113,70 @@ export async function httpConnectionRequest(
Authorization: auth.solveDigest(
username,
password,
options.path,
url.pathname + url.search,
"GET",
null,
authHeader
)
}
authHeader,
),
},
},
options
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
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 +191,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 +203,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 +217,88 @@ export async function udpConnectionRequest(
});
}
[username, password, authExp] = await extractAuth(
authExp,
context,
now,
null
);
[username, password, authExp] = await extractAuth(authExp, null);
}
client.close();
}
const XMPP_JID = config.get("XMPP_JID") as string;
const XMPP_PASSWORD = config.get("XMPP_PASSWORD") as string;
const XMPP_RESOURCE = crypto.randomBytes(8).toString("hex");
let xmppClient: XmppClient;
function xmppClientOnError(err: Error): void {
xmppClient = null;
logger.error({
message: "XMPP exception",
exception: err,
pid: process.pid,
});
}
function xmppClientOnClose(): void {
xmppClient = null;
}
export async function xmppConnectionRequest(
jid: string,
authExp: Expression,
timeout: number,
_debug: boolean,
deviceId: string,
): Promise<string> {
if (!xmppClient) {
const [host, username] = XMPP_JID.split("@").reverse();
xmppClient = await XmppClient.connect({
host,
username,
resource: XMPP_RESOURCE,
password: XMPP_PASSWORD,
timeout: 120000,
});
xmppClient.on("error", xmppClientOnError);
xmppClient.on("close", xmppClientOnClose);
xmppClient.unref();
}
let username: string;
let password: string;
[username, password, authExp] = await extractAuth(authExp, null);
while (username != null && password != null) {
const msg = `<connectionRequest xmlns="urn:broadband-forum-org:cwmp:xmppConnReq-1-0"><username>${encodeEntities(
username,
)}</username><password>${encodeEntities(
password,
)}</password></connectionRequest>`;
let res: Element, rawRes: string, rawReq: string;
try {
({ res, rawRes, rawReq } = await xmppClient.sendIqStanza(
XMPP_JID,
jid,
"get",
msg,
timeout,
));
} catch (err) {
return err.message;
}
if (_debug) {
debug.outgoingXmppStanza(deviceId, rawReq);
debug.incomingXmppStanza(deviceId, rawRes);
}
const attrs = parseAttrs(res.attrs);
const type = attrs.find((a) => a.name === "type");
if (type && type.value === "result") return "";
const error = res.children.find((c) => c.name === "error");
if (!error || !error.children[0])
return "Unexpected XMPP connection request response";
if (error.children[0].name === "service-unavailable")
return "Device is offline";
if (error.children[0].name !== "not-authorized")
return "Unexpected XMPP connection request response";
[username, password, authExp] = await extractAuth(authExp, null);
}
return "Incorrect connection request credentials";
}
+732 -554
View File
File diff suppressed because it is too large. Load diff
+286 -246
View File
@@ -1,121 +1,54 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { MongoClient, ObjectID, Collection } from "mongodb";
import { get } from "./config";
import { escapeRegExp } from "./common";
import { parse } from "./common/expression-parser";
import { ObjectId } from "mongodb";
import { decodeTag, encodeTag, escapeRegExp } from "../util.ts";
import {
DeviceData,
Attributes,
SessionFault,
Task,
Operation,
Expression
} from "./types";
import Path from "./common/path";
} from "../types.ts";
import Path from "../common/path.ts";
import { collections } from "../db/db.ts";
import { optimizeProjection } from "../db/util.ts";
import * as MongoTypes from "../db/types.ts";
export let tasksCollection: Collection,
devicesCollection: Collection,
presetsCollection: Collection,
objectsCollection: Collection,
provisionsCollection: Collection,
virtualParametersCollection: Collection,
faultsCollection: Collection,
filesCollection: Collection,
operationsCollection: Collection,
permissionsCollection: Collection,
usersCollection: Collection,
configCollection: Collection;
const INVALID_PATH_SUFFIX = "__invalid";
export let client: MongoClient;
export async function connect(): Promise<void> {
client = await MongoClient.connect("" + get("MONGODB_CONNECTION_URL"), {
useNewUrlParser: true
});
const db = client.db();
tasksCollection = db.collection("tasks");
await tasksCollection.createIndex({ device: 1, timestamp: 1 });
devicesCollection = db.collection("devices");
presetsCollection = db.collection("presets");
objectsCollection = db.collection("objects");
filesCollection = db.collection("fs.files");
provisionsCollection = db.collection("provisions");
virtualParametersCollection = db.collection("virtualParameters");
faultsCollection = db.collection("faults");
operationsCollection = db.collection("operations");
permissionsCollection = db.collection("permissions");
usersCollection = db.collection("users");
configCollection = db.collection("config");
}
export async function disconnect(): Promise<void> {
if (client) await client.close();
}
// Optimize projection by removing overlaps
// This can modify the object
function optimizeProjection(obj: {}): {} {
if (obj[""]) return { "": obj[""] };
const keys = Object.keys(obj).sort();
if (keys.length <= 1) return obj;
for (let i = 1; i < keys.length; ++i) {
const a = keys[i - 1];
const b = keys[i];
if (b.startsWith(a)) {
if (b.charAt(a.length) === "." || b.charAt(a.length - 1) === ".") {
delete obj[b];
keys.splice(i--, 1);
}
}
}
return obj;
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;
}
export async function fetchDevice(
id: string,
timestamp: number
timestamp: number,
): Promise<[Path, number, Attributes?][]> {
const res: [Path, number, Attributes?][] = [
[
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 });
const device = await collections.devices.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;
@@ -123,7 +56,7 @@ export async function fetchDevice(
if (obj["_value"] != null) {
attrs.value = [obj["_timestamp"] || 1, [obj["_value"], obj["_type"]]];
if (obj["_type"] === "xsd:dateTime")
if (obj["_type"] === "xsd:dateTime" && obj["_value"] instanceof Date)
attrs.value[1][0] = +attrs.value[1][0];
obj["_object"] = false;
@@ -133,19 +66,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 +113,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 +124,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 +135,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 +147,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 +158,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 +167,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 +191,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 +203,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 +215,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 +227,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;
}
@@ -285,7 +243,7 @@ export async function saveDevice(
deviceId: string,
deviceData: DeviceData,
isNew: boolean,
sessionTimestamp: number
sessionTimestamp: number,
): Promise<void> {
const update = { $set: {}, $unset: {}, $addToSet: {}, $pull: {} };
@@ -329,6 +287,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":
@@ -392,21 +356,37 @@ export async function saveDevice(
if (value2 != null) {
if (!update["$addToSet"]["_tags"])
update["$addToSet"]["_tags"] = { $each: [] };
update["$addToSet"]["_tags"]["$each"].push(path.segments[1]);
update["$addToSet"]["_tags"]["$each"].push(
decodeTag(path.segments[1] as string),
);
} else {
if (!update["$pull"]["_tags"]) {
update["$pull"]["_tags"] = {
$in: []
$in: [],
};
}
update["$pull"]["_tags"]["$in"].push(path.segments[1]);
update["$pull"]["_tags"]["$in"].push(
decodeTag(path.segments[1] as string),
);
}
}
break;
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;
}
@@ -424,7 +404,7 @@ export async function saveDevice(
Number.isInteger(value2 as number)
) {
update["$set"][path.toString() + "._value"] = new Date(
value2 as number
value2 as number,
);
} else {
update["$set"][path.toString() + "._value"] = value2;
@@ -436,13 +416,13 @@ export async function saveDevice(
if (valueTimestamp2 !== valueTimestamp1) {
update["$set"][path.toString() + "._timestamp"] = new Date(
valueTimestamp2
valueTimestamp2,
);
}
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 +430,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 +481,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;
}
}
}
@@ -507,78 +531,128 @@ export async function saveDevice(
delete update["$pull"];
}
const result = await devicesCollection.updateOne({ _id: deviceId }, update, {
upsert: isNew
});
const result = await collections.devices.updateOne(
{ _id: deviceId },
update,
{
upsert: isNew,
},
);
if (result.result.n !== 1)
if (!result.matchedCount && !result.upsertedCount)
throw new Error(`Device ${deviceId} not found in database`);
if (update2) {
await devicesCollection.updateOne({ _id: deviceId }, update2);
await collections.devices.updateOne({ _id: deviceId }, update2);
return;
}
}
export async function getFaults(
deviceId
deviceId: string,
): Promise<{ [channel: string]: SessionFault }> {
const res = await faultsCollection
const res = await collections.faults
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
.toArray();
const faults: { [channel: string]: SessionFault } = {};
for (const r of res) {
const channel = r._id.slice(deviceId.length + 1);
delete r._id;
delete r.channel;
delete r.device;
r.timestamp = +r.timestamp;
r.provisions = JSON.parse(r.provisions);
faults[channel] = r;
const fault: SessionFault = {
code: r.code,
message: r.message,
...(r.detail && { detail: r.detail }),
timestamp: +r.timestamp,
provisions: JSON.parse(r.provisions),
retries: r.retries,
...(r.expiry && { expiry: +r.expiry }),
};
faults[channel] = fault;
}
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: MongoTypes.Fault = {
_id: id,
device: deviceId,
channel: channel,
timestamp: new Date(fault.timestamp),
code: fault.code,
message: fault.message,
...(fault.detail && { detail: fault.detail }),
retries: fault.retries,
...(fault.expiry && { expiry: new Date(fault.expiry) }),
provisions: JSON.stringify(fault.provisions),
};
await collections.faults.replaceOne({ _id: id }, f, { upsert: true });
}
export async function deleteFault(deviceId, channel): Promise<void> {
await faultsCollection.deleteOne({ _id: `${deviceId}:${channel}` });
export async function deleteFault(
deviceId: string,
channel: string,
): Promise<void> {
await collections.faults.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 = collections.tasks
.find({ device: deviceId })
.sort({ timestamp: 1 });
const tasks = [] as Task[];
for await (const task of cur) {
if (task.timestamp) task.timestamp = +task.timestamp;
if (task.expiry) task.expiry = +task.expiry;
if (task.timestamp >= timestamp) return [tasks, +task.timestamp];
task._id = String(task._id);
for await (const t of cur) {
if (+t.timestamp >= timestamp) return [tasks, +t.timestamp];
const task: Task = {
_id: t._id.toString(),
name: t.name,
...(t.timestamp && { timestamp: +t.timestamp }),
...(t.expiry && { expiry: +t.expiry }),
...(t.name === "getParameterValues" && {
parameterNames: t.parameterNames,
}),
...(t.name === "setParameterValues" && {
parameterValues: t.parameterValues,
}),
...(t.name === "refreshObject" && {
objectName: t.objectName,
}),
...(t.name === "download" && {
fileType: t.fileType,
fileName: t.fileName,
targetFileName: t.targetFileName,
}),
...(t.name === "addObject" && {
objectName: t.objectName,
parameterValues: t.parameterValues,
}),
...(t.name === "deleteObject" && {
objectName: t.objectName,
}),
...(t.name === "provisions" && {
provisions: t.provisions,
}),
};
tasks.push(task);
// For API compatibility
if (task.name === "download" && task.file) {
if (task.name === "download" && t["file"]) {
let q;
if (ObjectID.isValid(task.file))
q = { _id: { $in: [task.file, new ObjectID(task.file)] } };
else q = { _id: task.file };
if (ObjectId.isValid(t["file"]))
q = { _id: { $in: [t["file"], new ObjectId(t["file"])] } };
else q = { _id: t["file"] };
const res = await filesCollection.find(q).toArray();
const res = await collections.files.find(q).toArray();
if (res[0]) {
if (!task.fileType) task.fileType = res[0].metadata.fileType;
@@ -591,102 +665,68 @@ export async function getDueTasks(
return [tasks, null];
}
export async function clearTasks(deviceId, taskIds): Promise<void> {
await tasksCollection.deleteMany({
_id: { $in: taskIds.map(id => new ObjectID(id)) }
export async function clearTasks(
deviceId: string,
taskIds: string[],
): Promise<void> {
await collections.tasks.deleteMany({
_id: { $in: taskIds.map((id) => new ObjectId(id)) },
});
}
export async function getOperations(
deviceId
deviceId: string,
): Promise<{ [commandKey: string]: Operation }> {
const res = await operationsCollection
const res = await collections.operations
.find({ _id: { $regex: `^${escapeRegExp(deviceId)}\\:` } })
.toArray();
const operations: { [commandKey: string]: Operation } = {};
for (const r of res) {
const commandKey = r._id.slice(deviceId.length + 1);
delete r._id;
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;
// Workaround for a bug in v1.2.1 where operation object is saved without deserialization
if (typeof r.provisions !== "string") {
delete r._id;
operations[commandKey] = r as unknown as Operation;
continue;
}
const operation: Operation = {
name: r.name,
timestamp: +r.timestamp,
channels:
typeof r.channels === "string" ? JSON.parse(r.channels) : r.channels,
retries: JSON.parse(r.retries),
provisions: JSON.parse(r.provisions),
...(r.args && { args: JSON.parse(r.args) }),
};
operations[commandKey] = operation;
}
return operations;
}
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: MongoTypes.Operation = {
_id: id,
name: operation.name,
timestamp: new Date(operation.timestamp),
channels: JSON.stringify(operation.channels),
provisions: JSON.stringify(operation.provisions),
retries: JSON.stringify(operation.retries),
args: JSON.stringify(operation.args),
};
await collections.operations.replaceOne({ _id: id }, o, {
upsert: true,
});
}
export async function deleteOperation(deviceId, commandKey): Promise<void> {
await operationsCollection.deleteOne({ _id: `${deviceId}:${commandKey}` });
}
export async function getPresets(): Promise<{}[]> {
return presetsCollection.find().toArray();
}
export async function getObjects(): Promise<{}[]> {
return objectsCollection.find().toArray();
}
export async function getProvisions(): Promise<{}[]> {
return provisionsCollection.find().toArray();
}
export async function getVirtualParameters(): Promise<{}[]> {
return virtualParametersCollection.find().toArray();
}
export function getFiles(): Promise<{}[]> {
return filesCollection.find().toArray();
}
export async function getConfig(): Promise<
{ id: string; value: Expression }[]
> {
const res = await configCollection.find().toArray();
return res.map(c => ({
id: c["_id"],
value: parse(c["value"])
}));
}
interface Permission {
role: string;
resource: string;
access: number;
filter: string;
validate: string;
}
export async function getPermissions(): Promise<Permission[]> {
return permissionsCollection.find().toArray();
}
interface User {
_id: string;
password: string;
salt: string;
roles: string;
}
export async function getUsers(): Promise<User[]> {
return usersCollection.find().toArray();
export async function deleteOperation(
deviceId: string,
commandKey: string,
): Promise<void> {
await collections.operations.deleteOne({ _id: `${deviceId}:${commandKey}` });
}
+357
View File
@@ -0,0 +1,357 @@
import * as vm from "node:vm";
import * as crypto from "node:crypto";
import * as config from "../config.ts";
import { collections } from "../db/db.ts";
import { convertOldPrecondition } from "../db/util.ts";
import * as logger from "../logger.ts";
import * as scheduling from "../scheduling.ts";
import * as expression from "../common/expression/util.ts";
import { parse } from "../common/expression/parser.ts";
import {
Preset,
Expression,
Provisions,
VirtualParameters,
Files,
Config,
} from "../types.ts";
import { LocalCache } from "../local-cache.ts";
interface Snapshot {
presets: Preset[];
provisions: Provisions;
virtualParameters: VirtualParameters;
files: Files;
config: Config;
}
function flattenObject<T extends Record<keyof T, unknown>>(
src: T,
prefix = "",
dst = {} as T,
): T {
for (const k of Object.keys(src)) {
const v = src[k];
if (typeof v === "object" && !Array.isArray(v))
flattenObject(v as T, `${prefix}${k}.`, dst);
else dst[`${prefix}${k}`] = v;
}
return dst;
}
async function fetchPresets(): Promise<[string, Preset[]]> {
const res = await collections.presets.find().toArray();
let objects = await collections.objects.find().toArray();
res.sort((a, b) => (a._id > b._id ? 1 : -1));
objects.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto
.createHash("md5")
.update(JSON.stringify(res))
.update(JSON.stringify(objects))
.digest("hex");
objects = objects.map((obj) => {
// Flatten object
obj = flattenObject(obj);
// If no keys are defined, consider all parameters as keys to keep the
// same behavior from v1.0
if (!obj["_keys"]?.length)
obj["_keys"] = Object.keys(obj).filter((k) => !k.startsWith("_"));
return obj;
});
res.sort((a, b) => {
if (a["weight"] === b["weight"])
return a["_id"] > b["_id"] ? 1 : a["_id"] < b["_id"] ? -1 : 0;
else return a["weight"] - b["weight"];
});
const presets = [] as Preset[];
for (const preset of res) {
let schedule: { md5: string; duration: number; schedule: any } = null;
if (preset["schedule"]) {
const parts = preset["schedule"].trim().split(/\s+/);
schedule = {
md5: crypto.createHash("md5").update(preset["schedule"]).digest("hex"),
duration: null,
schedule: null,
};
try {
schedule.duration = +parts.shift() * 1000;
schedule.schedule = scheduling.parseCron(parts.join(" "));
} catch (err) {
logger.warn({
message: "Invalid preset schedule",
preset: preset["_id"],
schedule: preset["schedule"],
});
schedule.schedule = false;
}
}
const events = preset["events"] || {};
let precondition = true as Expression;
if (preset["precondition"]) {
try {
precondition = parse(preset["precondition"]);
} catch (error) {
precondition = convertOldPrecondition(
JSON.parse(preset["precondition"]),
);
}
// Simplify expression
precondition = expression.evaluate(precondition);
}
const _provisions: Preset["provisions"] = [];
// Generate provisions from the old configuration format
for (const c of preset.configurations) {
switch (c.type) {
case "age":
_provisions.push(["refresh", c.name, +c.age]);
break;
case "value":
_provisions.push(["value", c.name, c.value]);
break;
case "add_tag":
_provisions.push(["tag", c.tag, true]);
break;
case "delete_tag":
_provisions.push(["tag", c.tag, false]);
break;
case "provision":
_provisions.push([c.name, ...(c.args || [])]);
break;
case "add_object":
for (const obj of objects) {
if (obj["_id"] === c.object) {
const alias = obj["_keys"]
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
.join(",");
const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 1]);
for (const k in obj) {
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
_provisions.push(["value", `${p}.${k}`, obj[k]]);
}
}
}
break;
case "delete_object":
for (const obj of objects) {
if (obj["_id"] === c.object) {
const alias = obj["_keys"]
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
.join(",");
const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 0]);
}
}
break;
default: {
const exhaustiveCheck: never = c;
throw new Error(
`Unknown configuration type ${exhaustiveCheck["type"]}`,
);
}
}
}
presets.push({
name: preset["_id"],
channel: (preset["channel"] as string) || "default",
schedule: schedule,
events: events,
precondition: precondition,
provisions: _provisions,
});
}
return [h, presets];
}
async function fetchProvisions(): Promise<[string, Provisions]> {
const res = await collections.provisions.find().toArray();
res.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto.createHash("md5").update(JSON.stringify(res)).digest("hex");
const provisions = {};
for (const r of res) {
provisions[r._id] = {};
provisions[r._id].md5 = crypto
.createHash("md5")
.update(r.script)
.digest("hex");
provisions[r._id].script = new vm.Script(
`"use strict";(function(){\n${r.script}\n})();`,
{ filename: r._id, lineOffset: -1 },
);
}
return [h, provisions];
}
async function fetchVirtualParameters(): Promise<[string, VirtualParameters]> {
const res = await collections.virtualParameters.find().toArray();
res.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto.createHash("md5").update(JSON.stringify(res)).digest("hex");
const virtualParameters = {};
for (const r of res) {
virtualParameters[r._id] = {};
virtualParameters[r._id].md5 = crypto
.createHash("md5")
.update(r.script)
.digest("hex");
virtualParameters[r._id].script = new vm.Script(
`"use strict";(function(){\n${r.script}\n})();`,
{ filename: r._id, lineOffset: -1 },
);
}
return [h, virtualParameters];
}
async function fetchFiles(): Promise<[string, Files]> {
const res = await collections.files.find().toArray();
res.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto.createHash("md5").update(JSON.stringify(res)).digest("hex");
const files = {};
for (const r of res) {
const id = r.filename || r._id.toString();
files[id] = {};
files[id].length = r.length;
}
return [h, files];
}
async function fetchConfig(): Promise<[string, Config]> {
const conf = await collections.config.find().toArray();
conf.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex");
const _config = {};
for (const c of conf) {
// Evaluate expressions to simplify them
_config[c._id] = expression.evaluate(parse(c.value));
}
return [h, _config];
}
const localCache = new LocalCache<Snapshot>("cwmp-local-cache-hash", refresh);
async function refresh(): Promise<[string, Snapshot]> {
const res = await Promise.all([
fetchPresets(),
fetchProvisions(),
fetchVirtualParameters(),
fetchFiles(),
fetchConfig(),
]);
const h = crypto.createHash("md5");
for (const r of res) h.update(r[0]);
const snapshot = {
presets: res[0][1],
provisions: res[1][1],
virtualParameters: res[2][1],
files: res[3][1],
config: res[4][1],
};
return [h.digest("hex"), snapshot];
}
export async function getRevision(): Promise<string> {
return await localCache.getRevision();
}
export function getPresets(revision: string): Preset[] {
return localCache.get(revision).presets;
}
export function getProvisions(revision: string): Provisions {
return localCache.get(revision).provisions;
}
export function getVirtualParameters(revision: string): VirtualParameters {
return localCache.get(revision).virtualParameters;
}
export function getFiles(revision: string): Files {
return localCache.get(revision).files;
}
export function getConfig(
snapshotKey: string,
key: string,
context: Record<string, unknown>,
now: number,
cb?: (e: Expression) => Expression,
): string | number | boolean | null {
const snapshot = localCache.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
const oldOpts = {
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
"cwmp.debug": "DEBUG",
"cwmp.retryDelay": "RETRY_DELAY",
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
"cwmp.cookiesPath": "COOKIES_PATH",
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
"cwmp.connectionRequestAllowBasicAuth":
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
};
if (!(key in snapshot.config)) {
if (key in oldOpts) {
let id;
if (context?.["id"]) {
id = context["id"];
} else if (cb) {
id = cb(["PARAM", "DeviceID.ID"]);
if (Array.isArray(id)) id = null;
}
return config.get(oldOpts[key], id);
}
return null;
}
const v = expression.evaluate(snapshot.config[key], context, now, cb);
return Array.isArray(v) ? null : v;
}
export function getConfigExpression(
snapshotKey: string,
key: string,
): Expression {
const snapshot = localCache.get(snapshotKey);
return snapshot.config[key];
}
+57
View File
@@ -0,0 +1,57 @@
import { MongoClient, Collection, GridFSBucket } from "mongodb";
import { get } from "../config.ts";
import * as MongoTypes from "./types.ts";
export let filesBucket: GridFSBucket;
export const collections = {
devices: null as Collection<MongoTypes.Device>,
presets: null as Collection<MongoTypes.Preset>,
objects: null as Collection<MongoTypes.Object>,
provisions: null as Collection<MongoTypes.Provision>,
virtualParameters: null as Collection<MongoTypes.VirtualParameter>,
faults: null as Collection<MongoTypes.Fault>,
tasks: null as Collection<MongoTypes.Task>,
files: null as Collection<MongoTypes.File>,
operations: null as Collection<MongoTypes.Operation>,
permissions: null as Collection<MongoTypes.Permission>,
users: null as Collection<MongoTypes.User>,
config: null as Collection<MongoTypes.Config>,
cache: null as Collection<MongoTypes.Cache>,
locks: null as Collection<MongoTypes.Lock>,
};
let clientPromise: Promise<MongoClient>;
export async function connect(): Promise<void> {
clientPromise = MongoClient.connect("" + get("MONGODB_CONNECTION_URL"));
const client = await clientPromise;
const db = client.db();
collections.tasks = db.collection("tasks");
collections.devices = db.collection("devices");
collections.presets = db.collection("presets");
collections.objects = db.collection("objects");
collections.files = db.collection("fs.files");
collections.provisions = db.collection("provisions");
collections.virtualParameters = db.collection("virtualParameters");
collections.faults = db.collection("faults");
collections.operations = db.collection("operations");
collections.permissions = db.collection("permissions");
collections.users = db.collection("users");
collections.config = db.collection("config");
collections.cache = db.collection("cache");
collections.locks = db.collection("locks");
filesBucket = new GridFSBucket(db);
await Promise.all([
collections.tasks.createIndex({ device: 1, timestamp: 1 }),
collections.cache.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }),
collections.locks.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }),
]);
}
export async function disconnect(): Promise<void> {
if (clientPromise != null) await (await clientPromise).close();
}
+642
View File
@@ -0,0 +1,642 @@
import { Filter } from "mongodb";
import { EJSON } from "bson";
import { parseLikePattern } from "../common/expression/parser.ts";
import { Expression } from "../types.ts";
import { decodeTag } from "../util.ts";
import {
SynthContextBase,
likeDisjoint,
likeImplies,
Clause,
} from "../common/expression/synth.ts";
import normalize from "../common/expression/normalize.ts";
import * as BI from "../common/expression/bigint.ts";
type Minterm = number[];
function getParam(exp: Expression, collection: string): string {
if (!Array.isArray(exp))
throw new Error("Left-hand operand must be a parameter");
if (exp[0] !== "PARAM")
throw new Error("Left-hand operand must be a parameter");
if (typeof exp[1] !== "string")
throw new Error("Left-hand operand must be a parameter");
const p = exp[1];
if (collection === "devices") {
if (p === "DeviceID.ID") return "_id";
else if (p === "DeviceID") return "_deviceId";
else if (p.startsWith("DeviceID.")) return "_deviceId._" + p.slice(9);
else if (p === "Events.Inform") return "_lastInform";
else if (p === "Events.Registered") return "_registered";
else if (p === "Events.0_BOOTSTRAP") return "_lastBootstrap";
else if (p === "Events.1_BOOT") return "_lastBoot";
else if (!p.endsWith("._value") && !p.startsWith("Tags."))
return `${p}._value`;
}
return p;
}
function getTypes(parameter: string, collection: string): string[] {
if (collection === "devices") {
if (parameter === "_id") return ["string"];
if (parameter === "_lastInform") return ["date"];
if (parameter === "_registered") return ["date"];
if (parameter === "_lastBootstrap") return ["date"];
if (parameter === "_lastBoot") return ["date"];
if (parameter.startsWith("_deviceId.")) return ["string"];
if (parameter === "Reboot._value") return ["date"];
if (parameter === "FactoryReset._value") return ["date"];
if (parameter.endsWith("_timestamp")) return ["date"];
if (parameter.startsWith("Downloads.")) {
if (parameter.endsWith("Download._value")) return ["date"];
if (parameter.endsWith("Time._value")) return ["date"];
if (parameter.endsWith("Name._value")) return ["string"];
if (parameter.endsWith("Type._value")) return ["string"];
}
if (parameter.endsWith("_value"))
return ["bool", "number", "date", "string"];
} else if (collection === "tasks") {
if (parameter === "_id") return ["oid"];
if (parameter === "timestamp") return ["date"];
if (parameter === "expiry") return ["date"];
if (parameter === "name") return ["string"];
if (parameter === "device") return ["string"];
} else if (collection === "faults") {
if (parameter === "_id") return ["string"];
if (parameter === "timestamp") return ["date"];
if (parameter === "expiry") return ["date"];
if (parameter === "code") return ["string"];
if (parameter === "retries") return ["number"];
if (parameter === "channel") return ["string"];
if (parameter === "device") return ["string"];
if (parameter === "message") return ["string"];
} else if (collection === "users") {
if (parameter === "_id") return ["string"];
if (parameter === "roles") return ["string"];
else if (parameter === "password")
throw new Error("Cannot query restricted parameters");
else if (parameter === "salt")
throw new Error("Cannot query restricted parameters");
} else if (collection === "config") {
if (parameter === "_id") return ["string"];
else if (parameter === "value") return ["string"];
} else if (collection === "files") {
if (parameter === "_id") return ["string"];
if (parameter === "metadata.fileType") return ["string"];
if (parameter === "metadata.oui") return ["string"];
if (parameter === "metadata.productClass") return ["string"];
if (parameter === "metadata.version") return ["string"];
} else if (collection === "permissions") {
if (parameter === "_id") return ["string"];
if (parameter === "role") return ["string"];
if (parameter === "resource") return ["devices"];
if (parameter === "access") return ["number"];
if (parameter === "filter") return ["string"];
if (parameter === "validate") return ["string"];
} else if (collection === "presets") {
if (parameter === "_id") return ["string"];
if (parameter === "weight") return ["number"];
if (parameter === "channel") return ["string"];
if (parameter === "precondition") return ["string"];
if (parameter.startsWith("events.")) return ["bool"];
} else if (collection === "provisions") {
if (parameter === "_id") return ["string"];
if (parameter === "script") return ["string"];
} else if (collection === "virtualParameters") {
if (parameter === "_id") return ["string"];
if (parameter === "script") return ["string"];
}
if (parameter === "_id") return ["oid", "string"];
return ["bool", "number", "date", "string"];
}
function roundOid(oid: string, roundUp: boolean): string {
const match = (oid.match(/^[0-9a-f]*/)?.[0] ?? "").slice(0, 24);
let num = BI.BigInt("0x0" + match);
let lastChar = 0;
if (oid.length > match.length) lastChar += oid.charCodeAt(match.length);
if (match.length < 24) lastChar -= 48;
if (lastChar > 0 && roundUp) num++;
num <<= BI.BigInt(4 * (24 - match.length));
if (lastChar < 0 && !roundUp) --num;
const str = num.toString(16);
if (str.length > 24 || str.startsWith("-")) return null;
return str.padStart(24, "0");
}
function groupBy<T, K>(
input: T[],
callback: (item: T) => K,
): Iterable<[K, T[]]> {
const groups = new Map<K, T[]>();
for (const item of input) {
const key = callback(item);
let arr = groups.get(key);
if (!arr) groups.set(key, (arr = []));
arr.push(item);
}
return groups.entries();
}
abstract class MongoClause {
abstract readonly parameter: string;
abstract toQuery(truthy: boolean): Filter<unknown>;
toString(): string {
return JSON.stringify(this.toQuery(true));
}
}
class MongoClauseArray extends MongoClause {
constructor(
public readonly parameter: string,
public readonly value: string,
) {
super();
}
toQuery(truthy: boolean): Filter<unknown> {
if (truthy) return { [this.parameter]: { $eq: this.value } };
else return { [this.parameter]: { $ne: this.value } };
}
}
class MongoClauseCompare<T> extends MongoClause {
constructor(
public readonly parameter: string,
public readonly op: "$eq" | "$gt" | "$lt" | "$gte" | "$lte",
public readonly value: T,
public readonly type: "" | "bool" | "number" | "string" | "date" | "oid",
) {
super();
}
toQuery(truthy: boolean): Filter<unknown> {
let v: any = this.value;
if (this.type === "date" && typeof v === "number")
v = { $date: new Date(v).toISOString() };
if (this.type === "oid" && typeof v === "string") v = { $oid: v };
if (truthy) return { [this.parameter]: { [this.op]: v } };
else if (this.op === "$eq") return { [this.parameter]: { $ne: v } };
else return { [this.parameter]: { $not: { [this.op]: v } } };
}
}
class MongoClauseType extends MongoClause {
constructor(
public readonly parameter: string,
public readonly type: string,
) {
super();
}
toQuery(truthy: boolean): Filter<unknown> {
if (truthy) return { [this.parameter]: { $type: this.type } };
else return { [this.parameter]: { $not: { $type: this.type } } };
}
}
class MongoClauseLike extends MongoClause {
readonly pattern: string[];
constructor(
public readonly parameter: string,
pat: string,
esc: string,
public readonly caseSensitive: boolean,
) {
super();
this.pattern = parseLikePattern(pat, esc);
}
toQuery(truthy: boolean): Filter<unknown> {
const convChars = {
"-": "\\-",
"/": "\\/",
"\\": "\\/",
"^": "\\^",
$: "\\$",
"*": "\\*",
"+": "\\+",
"?": "\\?",
".": "\\.",
"(": "\\(",
")": "\\)",
"|": "\\|",
"[": "\\[",
"]": "\\]",
"{": "\\{",
"}": "\\}",
"\\%": ".*",
"\\_": ".",
};
const chars = this.pattern.map((c) => convChars[c] ?? c);
chars[0] = chars[0] === ".*" ? "" : "^" + chars[0];
const l = chars.length - 1;
chars[l] = [".*", ""].includes(chars[l]) ? "" : chars[l] + "$";
const pattern = chars.join("");
const options = this.caseSensitive ? "s" : "is";
if (truthy) {
return { [this.parameter]: { $regularExpression: { options, pattern } } };
} else {
return {
[this.parameter]: {
$not: { $regularExpression: { options, pattern } },
},
};
}
}
}
class MongoSynthContext extends SynthContextBase<Clause, MongoClause> {
constructor(private readonly collection: string) {
super();
}
getMinterms(clause: Clause, res: true | false | null): number[][] {
const exp = clause.expression();
if (!Array.isArray(exp)) throw new Error("Invalid query expression");
if (res === null) {
const minterms: number[][] = [];
for (const dep of clause.getNullables()) {
const e = dep.operand.expression();
const param = getParam(e, this.collection);
if (param.startsWith("Tags.") && this.collection === "devices") {
const t = decodeTag(param.slice(5));
const c = new MongoClauseArray("_tags", t);
minterms.push([this.getVar(c) << 1]);
continue;
}
const c = new MongoClauseCompare(param, "$eq", null, "");
minterms.push([(this.getVar(c) << 1) ^ 1]);
}
return minterms;
}
if ([">", "<", "="].includes(exp[0])) {
const param = getParam(exp[1], this.collection);
let rhs = exp[2];
if (typeof rhs === "boolean") rhs = +rhs;
else if (typeof rhs !== "string" && typeof rhs !== "number")
throw new Error(`Right-hand operand must be a literal value`);
if (
param.startsWith("Tags.") &&
this.collection === "devices" &&
exp[0] === "="
) {
const t = decodeTag(param.slice(5));
const c = new MongoClauseArray("_tags", t);
if (typeof rhs === "string") rhs = 2;
if (exp[0] === "=") {
if ((rhs === 1) !== res) return [];
} else if (exp[0] === ">") {
if ((res && rhs >= 1) || (!res && rhs < 1)) return [];
} else if (exp[0] === "<") {
if ((res && rhs <= 1) || (!res && rhs > 1)) return [];
}
return [[(this.getVar(c) << 1) ^ 1]];
}
let op: "$gt" | "$lt" | "$eq" | "$gte" | "$lte";
if (exp[0] === "=") op = "$eq";
else if (exp[0] === ">") op = res ? "$gt" : "$lte";
else if (exp[0] === "<") op = res ? "$lt" : "$gte";
const possibleTypes = new Set(getTypes(param, this.collection));
const clauses: MongoClause[] = [];
if (typeof rhs === "number") {
if (possibleTypes.has("number"))
clauses.push(new MongoClauseCompare(param, op, rhs, "number"));
if (possibleTypes.has("date"))
clauses.push(new MongoClauseCompare(param, op, rhs, "date"));
if (possibleTypes.has("bool") && (rhs === 0 || rhs === 1))
clauses.push(new MongoClauseCompare(param, op, !!rhs, "bool"));
} else if (typeof rhs === "string") {
if (possibleTypes.has("string"))
clauses.push(new MongoClauseCompare(param, op, rhs, "string"));
if (possibleTypes.has("oid")) {
const oid = roundOid(rhs, op === "$lt" || op === "$lte");
if (oid && (op !== "$eq" || oid === rhs))
clauses.push(new MongoClauseCompare(param, op, oid, "oid"));
}
}
if (op === "$eq" && !res) {
clauses.push(new MongoClauseCompare(param, "$eq", null, ""));
return [clauses.map((c) => this.getVar(c) << 1)];
}
// In the following clauses we could use $type operator, but we want the
// final query to use comparison operators to check for type because the
// $type operator in MongoDB doesn't use indexes
if (typeof rhs === "number") {
if (possibleTypes.has("bool")) {
if (
(rhs > 1 && (op === "$lt" || op === "$lte")) ||
(rhs < 0 && (op === "$gt" || op === "$gte"))
)
clauses.push(new MongoClauseCompare(param, "$gte", false, "bool"));
}
if (op === "$gt" || op === "$gte") {
if (possibleTypes.has("string"))
clauses.push(new MongoClauseCompare(param, "$gte", "", "string"));
if (possibleTypes.has("oid")) {
clauses.push(
new MongoClauseCompare(
param,
"$gte",
"000000000000000000000000",
"oid",
),
);
}
}
} else if (typeof rhs === "string") {
if (op === "$lt" || op === "$lte") {
if (possibleTypes.has("bool"))
clauses.push(new MongoClauseCompare(param, "$gte", false, "bool"));
if (possibleTypes.has("number")) {
clauses.push(new MongoClauseCompare(param, "$gte", 0, "number"));
clauses.push(new MongoClauseCompare(param, "$lt", 0, "number"));
}
if (possibleTypes.has("date")) {
clauses.push(new MongoClauseCompare(param, "$gte", 0, "date"));
clauses.push(new MongoClauseCompare(param, "$lt", 0, "date"));
}
}
}
return clauses.map((c) => [(this.getVar(c) << 1) ^ 1]);
} else if (exp[0] === "LIKE") {
const pat = exp[2];
if (typeof pat !== "string")
throw new Error("Right-hand operand of 'LIKE' must be a string");
let p = exp[1];
let caseSensitive = true;
if (
Array.isArray(p) &&
p[0] === "FUNC" &&
["UPPER", "LOWER"].includes(p[1])
) {
if (p[1] === "UPPER" && pat !== pat.toUpperCase())
return res ? [] : [[]];
if (p[1] === "LOWER" && pat !== pat.toLowerCase())
return res ? [] : [[]];
caseSensitive = false;
p = p[2];
}
const param = getParam(p, this.collection);
const c = new MongoClauseLike(param, pat, exp[3], caseSensitive);
if (res) return [[(this.getVar(c) << 1) ^ 1]];
const typeClase = new MongoClauseType(param, "string");
const r = [[this.getVar(c) << 1, (this.getVar(typeClase) << 1) ^ 1]];
return r;
} else {
throw new Error("Invalid query expression");
}
}
getDcSet(minterms: Minterm[]): number[][] {
const dcSet: number[][] = [];
const vars = new Set(minterms.flat().map((l) => l >> 1));
const clauses = Array.from(vars).map((v) => this.getClause(v));
for (const [parameter, clauses2] of groupBy(clauses, (c) => c.parameter)) {
const comparisons = clauses2.filter(
(c) => c instanceof MongoClauseCompare && c.value !== null,
) as MongoClauseCompare<unknown>[];
for (const [type, clauses3] of groupBy(comparisons, (c) => c.type)) {
const isType = this.getVar(new MongoClauseType(parameter, type));
const values = new Set(clauses3.map((c) => c.value));
const valuesSorted = Array.from(values).sort((a, b) =>
a > b ? 1 : -1,
);
for (const [i, v] of valuesSorted.entries()) {
const eq = this.getVar(
new MongoClauseCompare(parameter, "$eq", v, type),
);
const gt = this.getVar(
new MongoClauseCompare(parameter, "$gt", v, type),
);
const lt = this.getVar(
new MongoClauseCompare(parameter, "$lt", v, type),
);
const gte = this.getVar(
new MongoClauseCompare(parameter, "$gte", v, type),
);
const lte = this.getVar(
new MongoClauseCompare(parameter, "$lte", v, type),
);
if (type === "bool") {
if (v === false) dcSet.push([(lt << 1) ^ 1]);
else if (v === true) dcSet.push([(gt << 1) ^ 1]);
} else if (type === "string") {
if (v === "") dcSet.push([(lt << 1) ^ 1]);
} else if (type === "oid") {
if (v < "000000000000000000000000") dcSet.push([(lte << 1) ^ 1]);
else if (v === "000000000000000000000000")
dcSet.push([(lt << 1) ^ 1]);
else if (v > "ffffffffffffffffffffffff")
dcSet.push([(gte << 1) ^ 1]);
else if (v === "ffffffffffffffffffffffff")
dcSet.push([(gt << 1) ^ 1]);
}
dcSet.push([(lt << 1) ^ 1, (gte << 1) ^ 1]);
dcSet.push([(gt << 1) ^ 1, (gte << 1) ^ 0]);
dcSet.push([(eq << 1) ^ 0, (lt << 1) ^ 0, (lte << 1) ^ 1]);
dcSet.push([(eq << 1) ^ 1, (gte << 1) ^ 0]);
dcSet.push([(eq << 1) ^ 1, (gt << 1) ^ 1]);
if (i === 0) {
dcSet.push([(isType << 1) ^ 0, (gte << 1) ^ 1]);
dcSet.push([(isType << 1) ^ 0, (lt << 1) ^ 1]);
} else {
const gt2 = this.getVar(
new MongoClauseCompare(
parameter,
"$gt",
valuesSorted[i - 1],
type,
),
);
const lte2 = this.getVar(
new MongoClauseCompare(
parameter,
"$lte",
valuesSorted[i - 1],
type,
),
);
dcSet.push([(gt2 << 1) ^ 0, (gte << 1) ^ 1]);
dcSet.push([(gt2 << 1) ^ 0, (lte2 << 1) ^ 0, (lt << 1) ^ 1]);
}
if (i === valuesSorted.length - 1)
dcSet.push([(isType << 1) ^ 1, (gt << 1) ^ 0, (lte << 1) ^ 0]);
}
}
const likes = clauses2.filter(
(c) => c instanceof MongoClauseLike,
) as MongoClauseLike[];
if (likes.length) {
const isType = this.getVar(new MongoClauseType(parameter, "string"));
for (let i1 = 0; i1 < likes.length; ++i1) {
const l1 = likes[i1];
let p1 = l1.pattern;
dcSet.push([(this.getVar(l1) << 1) ^ 1, (isType << 1) ^ 0]);
for (let i2 = i1 + 1; i2 < likes.length; ++i2) {
const l2 = likes[i2];
let p2 = l2.pattern;
if (!l1.caseSensitive || !l2.caseSensitive) {
p1 = p1.map((c) => c.toLowerCase());
p2 = p2.map((c) => c.toLowerCase());
}
if (likeDisjoint(p1, p2)) {
dcSet.push([
(this.getVar(l1) << 1) ^ 1,
(this.getVar(l2) << 1) ^ 1,
]);
} else if (
(!l1.caseSensitive || l2.caseSensitive) &&
likeImplies(p1, p2)
) {
dcSet.push([
(this.getVar(l1) << 1) ^ 0,
(this.getVar(l2) << 1) ^ 1,
]);
} else if (
(!l2.caseSensitive || l1.caseSensitive) &&
likeImplies(p2, p1)
) {
dcSet.push([
(this.getVar(l1) << 1) ^ 1,
(this.getVar(l2) << 1) ^ 0,
]);
}
}
}
}
const isNull = this.getVar(
new MongoClauseCompare(parameter, "$eq", null, ""),
);
const types = getTypes(parameter, this.collection).map((t) =>
this.getVar(new MongoClauseType(parameter, t)),
);
dcSet.push([(isNull << 1) ^ 0, ...types.map((t) => (t << 1) ^ 0)]);
for (let i = 0; i < types.length; ++i) {
const t1 = types[i];
dcSet.push([(t1 << 1) ^ 1, (isNull << 1) ^ 1]);
for (let j = i + 1; j < types.length; ++j) {
const t2 = types[j];
dcSet.push([(t1 << 1) ^ 1, (t2 << 1) ^ 1]);
}
}
if (parameter === "_id") dcSet.push([(isNull << 1) ^ 1]);
}
return dcSet;
}
canRaise(i: number, s: Set<number>): boolean {
if (!(i & 1)) return true;
const c = this.getClause(i >> 1);
if (c instanceof MongoClauseCompare) {
for (const j of s) {
if (j === i) continue;
const c2 = this.getClause(j >> 1);
if (c2.parameter !== c.parameter) continue;
if (!(j & 1)) return false;
if (c2 instanceof MongoClauseType) return false;
}
}
return true;
}
toQuery(minterms: Minterm[]): Filter<unknown> {
const or = [];
for (const minterm of minterms) {
const query = {};
loop: for (const clause of minterm) {
if (!minterm.length) return {};
const negate = !!(clause & 1);
const c = this.getClause(clause >> 1);
const q = c.toQuery(negate);
if (Object.keys(q).length !== 1)
throw new Error("Invalid query expression");
const [param, value] = Object.entries(q)[0];
if (Object.getPrototypeOf(value).constructor !== Object)
throw new Error("Invalid query expression");
const dests = [query, ...(query["$and"] ?? [])];
for (const dest of dests) {
if (!(param in dest)) {
dest[param] = value;
continue loop;
}
let src = value;
let dst = dest[param];
if (src["$not"] && dst["$not"]) {
src = src["$not"];
dst = dst["$not"];
}
if (!Object.keys(src).some((k) => k in dst)) {
Object.assign(dst, src);
continue loop;
}
}
query["$and"] ??= [];
query["$and"].push({ [param]: value });
}
or.push(query);
}
if (or.length === 1) return or[0];
return { $or: or };
}
}
export function toMongoQuery(
exp: Expression,
resource: string,
): Filter<unknown> | false {
exp = normalize(exp);
const clause = Clause.fromExpression(normalize(exp));
const context = new MongoSynthContext(resource);
const minterms = clause.true(context);
const minimized = context.minimize(minterms);
if (!minimized.length) return false;
return EJSON.deserialize(context.toQuery(minimized));
}
export function validQuery(exp: Expression, resource: string): void {
const clause = Clause.fromExpression(normalize(exp));
const context = new MongoSynthContext(resource);
clause.true(context);
}
+183
View File
@@ -0,0 +1,183 @@
import { ObjectId } from "mongodb";
import { Expression, FaultStruct } from "../types.ts";
export interface Fault {
_id: string;
device: string;
channel: string;
timestamp: Date;
provisions: string;
retries: number;
code: string;
message: string;
detail?:
| FaultStruct
| {
name: string;
message: string;
stack?: string;
};
expiry?: Date;
}
interface TaskBase {
_id: ObjectId;
timestamp?: Date;
expiry?: Date;
name: string;
device: string;
}
interface TaskGetParameterValues extends TaskBase {
name: "getParameterValues";
parameterNames: string[];
}
interface TaskSetParameterValues extends TaskBase {
name: "setParameterValues";
parameterValues: [string, string | number | boolean, string?][];
}
interface TaskRefreshObject extends TaskBase {
name: "refreshObject";
objectName: string;
}
interface TaskReboot extends TaskBase {
name: "reboot";
}
interface TaskFactoryReset extends TaskBase {
name: "factoryReset";
}
interface TaskDownload extends TaskBase {
name: "download";
fileType: string;
fileName: string;
targetFileName?: string;
}
interface TaskAddObject extends TaskBase {
name: "addObject";
objectName: string;
parameterValues: [string, string | number | boolean, string?][];
}
interface TaskDeleteObject extends TaskBase {
name: "deleteObject";
objectName: string;
}
interface TaskProvisions extends TaskBase {
name: "provisions";
provisions?: [string, ...Expression[]][];
}
export type Task =
| TaskGetParameterValues
| TaskSetParameterValues
| TaskRefreshObject
| TaskReboot
| TaskFactoryReset
| TaskDownload
| TaskAddObject
| TaskDeleteObject
| TaskProvisions;
export interface Operation {
_id: string;
name: string;
timestamp: Date;
channels: string;
retries: string;
provisions: string;
args: string;
}
export interface Config {
_id: string;
value: string;
}
export interface Cache {
_id: string;
value: string;
timestamp: Date;
expire: Date;
}
export interface Device {
_id: string;
_lastInform: Date;
_registered: Date;
_tags?: string[];
_timestamp?: Date;
}
type Configuration =
| { type: "age"; name: string; age: number }
| { type: "value"; name: string; value: boolean | number | string }
| { type: "add_tag"; tag: string }
| { type: "delete_tag"; tag: string }
| { type: "add_object"; name: string; object: string }
| { type: "delete_object"; name: string; object: string }
| { type: "provision"; name: string; args?: Expression[] };
export interface Preset {
_id: string;
weight: number;
channel: string;
events: Record<string, boolean>;
configurations: Configuration[];
}
export interface Object {
_id: string;
}
export interface Provision {
_id: string;
script: string;
}
export interface VirtualParameter {
_id: string;
script: string;
}
export interface File {
_id: string;
length: number;
filename: string;
uploadDate: Date;
metadata?: {
fileType?: string;
oui?: string;
productClass?: string;
version?: string;
};
}
export interface Permission {
_id: string;
role: string;
resource: string;
access: 1 | 2 | 3;
filter?: string;
validate?: string;
}
export interface User {
_id: string;
password: string;
roles: string;
salt: string;
}
export interface Lock {
_id: string;
value: string;
timestamp: Date;
expire: Date;
}
+123
View File
@@ -0,0 +1,123 @@
import { Expression } from "../types.ts";
import { encodeTag } from "../util.ts";
// Optimize projection by removing overlaps
// This can modify the object
export function optimizeProjection(obj: { [path: string]: 1 }): {
[path: string]: 1;
} {
if (obj[""]) return { "": obj[""] };
const keys = Object.keys(obj).sort();
if (keys.length <= 1) return obj;
for (let i = 1; i < keys.length; ++i) {
const a = keys[i - 1];
const b = keys[i];
if (b.startsWith(a)) {
if (b.charAt(a.length) === "." || b.charAt(a.length - 1) === ".") {
delete obj[b];
keys.splice(i--, 1);
}
}
}
return obj;
}
export function convertOldPrecondition(q: Record<string, unknown>): Expression {
function recursive(_query): Expression {
const expressions: Expression[] = [];
for (const [k, v] of Object.entries(_query)) {
if (k[0] === "$") {
if (k === "$and") {
const and: Expression = ["AND"];
for (const vv of Object.values(v)) and.push(recursive(vv));
expressions.push(and);
} else if (k === "$or") {
const or: Expression = ["OR"];
for (const vv of Object.values(v)) or.push(recursive(vv));
expressions.push(or);
} else {
throw new Error(`Operator ${k} not supported`);
}
} else if (k === "_tags") {
if (typeof v === "object") {
if (Array.isArray(v)) throw new Error(`Invalid type`);
const conjs: Expression[] = [];
for (const [op, val] of Object.entries(v)) {
if (op === "$ne") {
if (typeof v["$ne"] !== "string")
throw new Error("Only string values are allowed for _tags");
conjs.push(["IS NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
} else if (op === "$eq") {
if (typeof v["$eq"] !== "string")
throw new Error("Only string values are allowed for _tags");
conjs.push(["IS NOT NULL", ["PARAM", `Tags.${encodeTag(val)}`]]);
} else {
throw new Error(`Invalid tag query`);
}
}
if (conjs.length === 1) expressions.push(conjs[0]);
else if (conjs.length > 1) expressions.push(["AND", ...conjs]);
} else {
expressions.push([
"IS NOT NULL",
["PARAM", `Tags.${encodeTag(v as string)}`],
]);
}
} else if (k.startsWith("Tags.")) {
let exists: boolean;
if (typeof v === "boolean") exists = v;
else if (v.hasOwnProperty("$eq")) exists = !!v["$eq"];
else if (v.hasOwnProperty("$ne")) exists = !v["$ne"];
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
else throw new Error(`Invalid tag query`);
expressions.push([exists ? "IS NOT NULL" : "IS NULL", ["PARAM", k]]);
} else if (typeof v === "object") {
if (Array.isArray(v)) throw new Error(`Invalid type`);
const exps: Expression[] = [];
for (const [kk, vv] of Object.entries(v)) {
if (kk === "$eq") {
exps.push(["=", ["PARAM", k], vv]);
} else if (kk === "$ne") {
exps.push([
"OR",
["<>", ["PARAM", k], vv],
["IS NULL", ["PARAM", k]],
]);
} else if (kk === "$lt") {
exps.push(["<", ["PARAM", k], vv]);
} else if (kk === "$lte") {
exps.push(["<=", ["PARAM", k], vv]);
} else if (kk === "$gt") {
exps.push([">", ["PARAM", k], vv]);
} else if (kk === "$gte") {
exps.push([">=", ["PARAM", k], vv]);
} else {
throw new Error(`Operator ${kk} not supported`);
}
if (!["string", "number", "boolean"].includes(typeof vv))
throw new Error(`Invalid value for ${kk} operator`);
}
if (exps.length === 1) {
expressions.push(exps[0]);
} else if (exps.length > 1) {
const and: Expression = ["AND"];
expressions.push(and.concat(exps));
}
} else {
expressions.push(["=", ["PARAM", k], v]);
}
}
if (expressions.length === 1) return expressions[0];
if (expressions.length === 0) return true;
return ["AND", ...expressions];
}
// empty filter
if (!Object.keys(q).length) return true;
return recursive(q);
}
+96 -64
View File
@@ -1,32 +1,9 @@
/**
* 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,
ServerResponse,
ClientRequest,
RequestOptions
} from "http";
import { Socket } from "net";
import { appendFileSync } from "fs";
import * as yaml from "yaml";
import * as config from "./config";
import { IncomingMessage, ServerResponse, ClientRequest } from "node:http";
import { Socket } from "node:net";
import { appendFileSync } from "node:fs";
import { stringify } from "./common/yaml.ts";
import * as config from "./config.ts";
import { getSocketEndpoints } from "./server.ts";
const DEBUG_FILE = "" + config.get("DEBUG_FILE");
const DEBUG_FORMAT = "" + config.get("DEBUG_FORMAT");
@@ -45,26 +22,27 @@ function getConnectionTimestamp(connection: Socket): Date {
export function incomingHttpRequest(
httpRequest: IncomingMessage,
deviceId: string,
body: string
body: string,
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpRequest.connection;
const con = httpRequest.socket;
const socketEndpoints = getSocketEndpoints(con);
const msg = {
event: "incoming HTTP request",
timestamp: now,
remoteAddress: con.remoteAddress,
remoteAddress: socketEndpoints.remoteAddress,
deviceId: deviceId,
connection: getConnectionTimestamp(con),
localPort: con.localPort,
localPort: socketEndpoints.localPort,
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`);
@@ -73,24 +51,25 @@ export function incomingHttpRequest(
export function outgoingHttpResponse(
httpResponse: ServerResponse,
deviceId: string,
body: string
body: string,
): void {
if (!DEBUG_FILE) return;
const now = new Date();
const con = httpResponse.connection;
const con = httpResponse.socket;
const socketEndpoints = getSocketEndpoints(con);
const msg = {
event: "outgoing HTTP response",
timestamp: now,
remoteAddress: con.remoteAddress,
remoteAddress: socketEndpoints.remoteAddress,
deviceId: deviceId,
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 +78,28 @@ export function outgoingHttpResponse(
export function outgoingHttpRequest(
httpRequest: ClientRequest,
deviceId: string,
options: RequestOptions,
body: string
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,26 +108,27 @@ export function outgoingHttpRequest(
export function outgoingHttpRequestError(
httpRequest: ClientRequest,
deviceId: string,
options: RequestOptions,
err: Error
method: "GET" | "PUT" | "POST" | "DELETE",
url: URL,
err: Error,
): void {
if (!DEBUG_FILE) return;
const now = new Date();
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`);
@@ -156,24 +137,24 @@ export function outgoingHttpRequestError(
export function incomingHttpResponse(
httpResponse: IncomingMessage,
deviceId: string,
body: string
body: string,
): 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`);
@@ -183,7 +164,7 @@ export function outgoingUdpMessage(
remoteAddress: string,
deviceId: string,
remotePort: number,
body: string
body: string,
): void {
if (!DEBUG_FILE) return;
const now = new Date();
@@ -193,11 +174,62 @@ 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`);
}
export function clientError(remoteAddress: string, err: Error): void {
if (!DEBUG_FILE) return;
const now = new Date();
const msg = {
event: "client error",
timestamp: now,
remoteAddress: remoteAddress,
error: err.message,
};
if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
}
export function outgoingXmppStanza(deviceId: string, body: string): void {
if (!DEBUG_FILE) return;
const now = new Date();
const msg = {
event: "outgoing XMPP stanza",
timestamp: now,
deviceId: deviceId,
body: body,
};
if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
}
export function incomingXmppStanza(deviceId: string, body: string): void {
if (!DEBUG_FILE) return;
const now = new Date();
const msg = {
event: "incoming XMPP stanza",
timestamp: now,
deviceId: deviceId,
body: body,
};
if (DEBUG_FORMAT === "yaml")
appendFileSync(DEBUG_FILE, "---\n" + stringify(msg));
else if (DEBUG_FORMAT === "json")
appendFileSync(DEBUG_FILE, JSON.stringify(msg) + "\n");
else throw new Error(`Unrecognized DEBUG_FORMAT option`);
+141 -56
View File
@@ -1,110 +1,192 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import Path from "./common/path";
import * as config from "./config";
import * as device from "./device";
import * as scheduling from "./scheduling";
import Path from "./common/path.ts";
import * as config from "./config.ts";
import * as device from "./device.ts";
import * as scheduling from "./scheduling.ts";
import { SessionContext, Declaration } from "./types.ts";
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 +195,7 @@ export function download(sessionContext, provision, declarations): boolean {
pathSet: 1,
attrGet: null,
attrSet: null,
defer: true
defer: true,
});
declarations.push({
@@ -122,22 +204,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 +233,7 @@ export function instances(
pathSet: null,
attrGet: null,
attrSet: null,
defer: true
defer: true,
});
if (endRevision === startRevision) return false;
@@ -156,7 +241,7 @@ export function instances(
const unpacked = device.unpack(
sessionContext.deviceData,
path,
startRevision + 1
startRevision + 1,
);
count = Math.max(0, unpacked.length + count);
}
@@ -167,7 +252,7 @@ export function instances(
pathSet: count,
attrGet: null,
attrSet: null,
defer: true
defer: true,
});
return true;
+38 -53
View File
@@ -1,29 +1,18 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import Path from "./common/path";
import { DeviceData, Declaration, Clear } from "./types";
import Path from "./common/path.ts";
import {
DeviceData,
Declaration,
Clear,
AttributeTimestamps,
Attributes,
} from "./types.ts";
const CHANGE_FLAGS = {
object: 2,
writable: 4,
value: 8
value: 8,
notification: 16,
accessList: 32,
};
function parseBool(v): boolean {
@@ -35,8 +24,8 @@ function parseBool(v): boolean {
}
export function sanitizeParameterValue(
parameterValue: [string | number | boolean, string]
): [string | number | boolean, string?] {
parameterValue: [string | number | boolean, string],
): [string | number | boolean, string] {
if (parameterValue[0] != null) {
switch (parameterValue[1]) {
case "xsd:boolean":
@@ -46,13 +35,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 +57,7 @@ export function sanitizeParameterValue(
}
break;
default:
parameterValue[0] = JSON.parse(JSON.stringify(parameterValue[0]));
parameterValue[0] = "" + parameterValue[0];
break;
}
}
@@ -86,7 +68,7 @@ export function sanitizeParameterValue(
export function getAliasDeclarations(
path: Path,
timestamp: number,
attrGet = null
attrGet = null,
): Declaration[] {
const stripped = path.stripAlias();
let decs: Declaration[] = [
@@ -96,8 +78,8 @@ export function getAliasDeclarations(
pathSet: null,
attrGet: attrGet,
attrSet: null,
defer: true
}
defer: true,
},
];
if (path.alias) {
@@ -107,8 +89,8 @@ export function getAliasDeclarations(
for (const [p] of alias as [Path, string][]) {
decs = decs.concat(
getAliasDeclarations(parent.concat(p), timestamp, {
value: timestamp
})
value: timestamp,
}),
);
}
}
@@ -121,7 +103,7 @@ export function getAliasDeclarations(
export function unpack(
deviceData: DeviceData,
path: Path,
revision?: number
revision?: number,
): Path[] {
let allMatches = [] as Path[];
if (!path.alias) {
@@ -189,18 +171,18 @@ export function unpack(
}
export function clear(
deviceData,
path,
timestamp,
attributes,
changeFlags = 0
deviceData: DeviceData,
path: Path,
timestamp: number,
attributes: AttributeTimestamps,
changeFlags = 0,
): void {
const changeTrackers = {};
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))
@@ -211,7 +193,7 @@ export function clear(
path,
true,
true,
descendantsTimestamp ? 99 : path.length
descendantsTimestamp ? 99 : path.length,
)) {
const tracker = deviceData.trackers.get(p);
for (const k in tracker) changeTrackers[k] |= tracker[k];
@@ -271,9 +253,9 @@ function compareEquality(a, b): boolean {
export function set(
deviceData: DeviceData,
path: Path,
timestamp,
attributes,
toClear?: Clear[]
timestamp: number,
attributes: Attributes,
toClear?: Clear[],
): Clear[] {
path = deviceData.paths.add(path);
@@ -300,7 +282,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);
@@ -314,7 +296,7 @@ export function set(
else if (
!compareEquality(
attributes[attrName][1],
currentAttributes[attrName][1]
currentAttributes[attrName][1],
)
)
changeFlags |= CHANGE_FLAGS[attrName];
@@ -333,7 +315,7 @@ export function set(
path.slice(0, path.length - 1),
timestamp,
{ object: [timestamp, 1] },
toClear
toClear,
);
}
}
@@ -376,7 +358,7 @@ export function track(
deviceData: DeviceData,
path: Path,
marker: string,
attributes?
attributes?: string[],
): void {
path = deviceData.paths.add(path);
let f = 1;
@@ -393,7 +375,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];
+37 -41
View File
@@ -1,62 +1,39 @@
/**
* 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 { spawn, ChildProcess } from "child_process";
import * as crypto from "crypto";
import * as config from "./config";
import { Fault } from "./types";
import { spawn, ChildProcess } from "node:child_process";
import * as crypto from "node:crypto";
import readline from "node:readline";
import * as config from "./config.ts";
import { Fault } from "./types.ts";
import { ROOT_DIR } from "./config.ts";
import * as logger from "./logger.ts";
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 +43,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 +81,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();
@@ -108,6 +104,6 @@ export async function killAll(): Promise<void> {
Object.entries(processes).map(([k, p]) => {
delete processes[k];
return kill(p);
})
}),
);
}
+160
View File
@@ -0,0 +1,160 @@
import { IncomingMessage } from "node:http";
import { TLSSocket } from "node:tls";
import { parseCIDR, parse, IPv6, IPv4 } from "ipaddr.js";
import * as config from "./config.ts";
import { getSocketEndpoints } from "./server.ts";
interface RequestOrigin {
localAddress: string;
localPort: number;
remoteAddress: string;
remotePort: number;
host: string;
encrypted: boolean;
}
const FORWARDED_HEADER = "" + config.get("FORWARDED_HEADER");
const cache = new WeakMap<IncomingMessage, RequestOrigin>();
const cidrs: [IPv4 | IPv6, number][] = [];
for (const str of FORWARDED_HEADER.split(",").map((s) => s.trim())) {
try {
cidrs.push(parseCIDR(str));
} catch (err) {
// Not a valid CIDR format, try parsing as IP
try {
const ip = parse(str);
cidrs.push([ip, ip.toByteArray().length * 8]);
} catch (err) {
// Not a valid IP either, ignore
}
}
}
function parseForwardedHeader(str: string): { [name: string]: string } {
str = str.toLowerCase();
const res: { [name: string]: string } = {};
let keyIdx = 0;
let valueIdx = -1;
let key: string;
for (let i = 0; i < str.length; ++i) {
const char = str.charCodeAt(i);
if (char === 61 /* = */) {
if (keyIdx >= 0) {
key = str.slice(keyIdx, i).trim();
keyIdx = -1;
valueIdx = i + 1;
}
} else if (char === 59 /* ; */) {
if (valueIdx >= 0) res[key] = str.slice(valueIdx, i).trim();
valueIdx = -1;
keyIdx = i + 1;
} else if (char === 44 /* , */) {
if (valueIdx >= 0) res[key] = str.slice(valueIdx, i).trim();
return res;
} else if (char === 34 /* " */) {
if (valueIdx >= 0) {
const quoteIdx = i;
if (!str.slice(valueIdx, quoteIdx).trim()) {
for (i = i + 1; i < str.length; ++i) {
const c = str.charCodeAt(i);
if (c === 92 /* \ */) ++i;
if (c === 34 /* " */) {
res[key] = JSON.parse(str.slice(quoteIdx, i + 1).trim());
valueIdx = -1;
keyIdx = i + 1;
break;
}
}
}
}
}
}
if (valueIdx >= 0) res[key] = str.slice(valueIdx).trim();
return res;
}
export function getRequestOrigin(request: IncomingMessage): RequestOrigin {
let origin = cache.get(request);
if (!origin) {
const soc = request.socket;
const socketEndpoints = getSocketEndpoints(soc);
origin = {
localAddress: socketEndpoints.localAddress,
localPort: socketEndpoints.localPort,
remoteAddress: socketEndpoints.remoteAddress,
remotePort: socketEndpoints.remotePort,
host: request.headers["host"],
encrypted: !!(request.socket as TLSSocket).encrypted,
};
const header = request.headers["forwarded"];
if (header) {
const ip = parse(socketEndpoints.remoteAddress);
if (
cidrs.some((cidr) => ip.kind() === cidr[0].kind() && ip.match(cidr))
) {
const parsed = parseForwardedHeader(header);
if (parsed["proto"] === "https") {
origin.encrypted = true;
origin.localPort = 443;
} else if (parsed["proto"] === "http") {
origin.encrypted = false;
origin.localPort = 80;
}
if (parsed["host"]) {
origin.host = parsed["host"];
const [, port] = parsed["host"].split(":", 2);
origin.localPort = +port || origin.localPort;
}
if (parsed["for"]) {
if (parsed["for"].startsWith("[")) {
const i = parsed["for"].lastIndexOf("]");
if (i >= 0) {
origin.remoteAddress = parsed["for"].slice(1, i);
origin.remotePort =
parseInt(parsed["for"].slice(i + 2)) || origin.remotePort;
}
} else {
const i = parsed["for"].lastIndexOf(":");
if (i >= 0) {
origin.remoteAddress = parsed["for"].slice(0, i);
origin.remotePort =
parseInt(parsed["for"].slice(i + 1)) || origin.remotePort;
} else {
origin.remoteAddress = parsed["for"];
}
}
}
if (parsed["by"]) {
if (parsed["by"].startsWith("[")) {
const i = parsed["by"].lastIndexOf("]");
if (i >= 0) {
origin.localAddress = parsed["by"].slice(1, i);
origin.localPort =
parseInt(parsed["by"].slice(i + 2)) || origin.localPort;
}
} else {
const i = parsed["by"].lastIndexOf(":");
if (i >= 0) {
origin.localAddress = parsed["by"].slice(0, i);
origin.localPort =
parseInt(parsed["by"].slice(i + 1)) || origin.localPort;
} else {
origin.localAddress = parsed["by"];
}
}
}
}
}
cache.set(request, origin);
}
return origin;
}
+198 -58
View File
@@ -1,63 +1,203 @@
/**
* 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 url from "node:url";
import { IncomingMessage, ServerResponse } from "node:http";
import { PassThrough, pipeline, Readable } from "node:stream";
import { createHash } from "node:crypto";
import { filesBucket, collections } from "./db/db.ts";
import * as logger from "./logger.ts";
import { getRequestOrigin } from "./forwarded.ts";
import memoize from "./common/memoize.ts";
import * as url from "url";
import * as querystring from "querystring";
import { GridFSBucket } from "mongodb";
import * as db from "./db";
import * as logger from "./logger";
const getFile = memoize(
async (
etag: string,
size: number,
filename: string,
): Promise<Iterable<Buffer>> => {
const chunks: Buffer[] = [];
// Using for-await over the download stream can throw ERR_STREAM_PREMATURE_CLOSE
// for very small files. Possibly a bug in MongoDB driver or Nodejs itself.
// Using a PassThrough stream to avoid this.
const downloadStream = pipeline(
filesBucket.openDownloadStreamByName(filename),
new PassThrough(),
(err) => {
if (err) throw err;
},
);
for await (const chunk of downloadStream) chunks.push(chunk);
// Node 12-14 don't throw error when stream is closed prematurely.
// However, we don't need to check for that since we're checking file size.
if (size !== chunks.reduce((a, b) => a + b.length, 0))
throw new Error("File size mismatch");
return chunks;
},
);
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));
async function* partialContent(
chunks: Iterable<Buffer>,
start: number,
end: number,
): AsyncIterable<Buffer> {
let bytesToSkip = start;
let bytesToRead = end - start;
const log = {
message: "Fetch file",
filename: filename,
remoteAddress: request.connection.remoteAddress
};
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 bucket = new GridFSBucket(db.client.db());
const downloadStream = bucket.openDownloadStreamByName(filename);
downloadStream.pipe(response);
logger.accessInfo(log);
});
} else {
response.writeHead(405, { Allow: "GET" });
response.end("405 Method Not Allowed");
for (let chunk of chunks) {
if (bytesToRead <= 0) return;
if (bytesToSkip >= chunk.length) {
bytesToSkip -= chunk.length;
continue;
}
chunk = chunk.subarray(bytesToSkip, bytesToSkip + bytesToRead);
bytesToRead -= chunk.length;
bytesToSkip = 0;
yield chunk;
}
}
function generateETag(file: {
_id: string;
uploadDate: Date;
length: number;
}): string {
const hash = createHash("md5");
hash.update(`${file._id}-${file.uploadDate.getTime()}-${file.length}`);
return hash.digest("hex");
}
function matchEtag(etag: string, header: string): boolean {
for (let t of header.split(",")) {
t = t.trim();
if (t.startsWith("W/")) t = t.substring(2);
try {
t = JSON.parse(t);
} catch (e) {
// Ignore
}
if (t === "*") return true;
if (etag === t) return true;
}
return false;
}
export async function listener(
request: IncomingMessage,
response: ServerResponse,
): Promise<void> {
if (request.method !== "GET" && request.method !== "HEAD") {
response.writeHead(405, { Allow: "GET, HEAD" });
response.end("405 Method Not Allowed");
return;
}
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,
method: request.method,
};
const file = await collections.files.findOne({ _id: filename });
if (!file) {
response.writeHead(404);
response.end();
log.message += " not found";
logger.accessError(log);
return;
}
logger.accessInfo(log);
const etag = generateETag(file);
const lastModified = file["uploadDate"];
lastModified.setMilliseconds(0);
let status = 200;
let start = 0;
let end = file.length;
if (request.headers["if-match"])
if (!matchEtag(etag, request.headers["if-match"])) status = 412;
if (request.headers["if-unmodified-since"]) {
const d = new Date(request.headers["if-unmodified-since"]);
if (lastModified > d) status = 412;
}
if (request.headers["if-none-match"]) {
if (matchEtag(etag, request.headers["if-none-match"])) status = 304;
} else if (request.headers["if-modified-since"]) {
const d = new Date(request.headers["if-modified-since"]);
if (lastModified <= d) status = 304;
}
if (request.headers.range && status === 200) {
const match = request.headers.range.match(/^bytes=(\d*)-(\d*)$/);
status = 416;
if (match && (match[1] || match[2])) {
if (match[2]) end = parseInt(match[2]) + 1;
if (match[1]) start = parseInt(match[1]);
else start = file.length - parseInt(match[2]);
if (start < end && end <= file.length) status = 206;
}
if (request.headers["if-range"]) {
const h = request.headers["if-range"] as string;
const d = new Date(h);
if (!matchEtag(etag, h) && !(lastModified <= d)) {
status = 200;
start = 0;
end = file.length;
}
}
}
if (status === 412) {
response.writeHead(412);
response.end();
return;
}
if (status === 304) {
response.writeHead(304, {
ETag: etag,
"Last-Modified": lastModified.toUTCString(),
});
response.end();
return;
}
if (status === 416) {
response.writeHead(416, {
"Content-Range": `bytes */${file.length}`,
"Content-Length": "0",
});
response.end();
return;
}
response.writeHead(status, {
"Content-Type": "application/octet-stream",
"Content-Length": end - start,
"Accept-Ranges": "bytes",
ETag: etag,
"Last-Modified": lastModified.toUTCString(),
...(status === 206 && {
"Content-Range": `bytes ${start}-${end - 1}/${file.length}`,
}),
});
if (request.method === "HEAD") {
response.end();
return;
}
const chunks = await getFile(etag, file.length, filename);
pipeline(Readable.from(partialContent(chunks, start, end)), response, () => {
// Ignore errors resulting from client disconnecting
});
}
+8 -22
View File
@@ -1,21 +1,4 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import Path from "./common/path.ts";
const WILDCARD_MULTIPLIER = 2;
const UNDISCOVERED_DEPTH = 7;
@@ -27,9 +10,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 +23,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;
+78 -87
View File
@@ -1,26 +1,14 @@
/**
* 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 localCache from "./local-cache";
import { generateSalt, hashPassword } from "./auth";
import * as db from "./ui/db";
import { del } from "./cache";
import { getRevision, getUiConfig, getUsers } from "./ui/local-cache.ts";
import { generateSalt, hashPassword } from "./auth.ts";
import { collections } from "./db/db.ts";
import {
putConfig,
putPermission,
putPreset,
putProvision,
putUser,
} from "./ui/db.ts";
import { del } from "./cache.ts";
interface Status {
users: boolean;
@@ -59,8 +47,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;
@@ -84,22 +72,24 @@ declare("Device.ManagementServer.PeriodicInformTime", {value: daily}, {value: in
`.trim();
export async function getStatus(): Promise<Status> {
const configSnapshot = await localCache.getCurrentSnapshot();
const users = localCache.getUsers(configSnapshot);
const presets = localCache.getPresets(configSnapshot);
const ui = localCache.getUiConfig(configSnapshot);
const [configSnapshot, presetCount] = await Promise.all([
getRevision(),
collections.presets.countDocuments(),
]);
const users = getUsers(configSnapshot);
const ui = getUiConfig(configSnapshot);
return {
users: !Object.keys(users).length,
presets: !presets.length,
presets: !presetCount,
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 +107,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 +125,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 +139,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 +268,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 +288,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 +312,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 +321,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,23 +368,24 @@ 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 },
];
}
if (resources["permissions"]) {
for (const p of resources["permissions"]) {
p["_id"] = `${p["role"]}:${p["resource"]}:${p["access"]}`;
proms.push(db.putPermission(p["_id"], p));
proms.push(putPermission(p["_id"], p));
}
}
@@ -405,21 +396,21 @@ export async function seed(options): Promise<void> {
u["roles"] = (u["roles"] || []).join(",");
u["_id"] = u["username"];
delete u["username"];
proms.push(db.putUser(u["_id"], u));
proms.push(putUser(u["_id"], u));
}
}
if (resources["provisions"]) {
for (const p of resources["provisions"])
proms.push(db.putProvision(p["_id"], p));
proms.push(putProvision(p["_id"], p));
}
if (resources["presets"])
for (const p of resources["presets"]) proms.push(db.putPreset(p["_id"], p));
for (const p of resources["presets"]) proms.push(putPreset(p["_id"], p));
if (resources["config"])
for (const c of resources["config"]) proms.push(db.putConfig(c["_id"], c));
for (const c of resources["config"]) proms.push(putConfig(c["_id"], c));
await proms;
return del("presets_hash");
await Promise.all([del("ui-local-cache-hash"), del("cwmp-local-cache-hash")]);
}
+1 -20
View File
@@ -1,28 +1,9 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
interface Instance {
[name: string]: string;
}
export default class InstanceSet {
private set: Set<Instance>;
private declare set: Set<Instance>;
public constructor() {
this.set = new Set();
+50 -501
View File
@@ -1,524 +1,73 @@
/**
* 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 { setTimeoutPromise } from "./util.ts";
import { get, set } from "./cache.ts";
import { acquireLock, releaseLock } from "./lock.ts";
import * as vm from "vm";
import * as crypto from "crypto";
import * as config from "./config";
import * as db from "./db";
import * as cache from "./cache";
import { mongoQueryToFilter } from "./mongodb-functions";
import * as logger from "./logger";
import * as scheduling from "./scheduling";
import * as expression from "./common/expression";
import { parse } from "./common/expression-parser";
import { Preset, Expression } from "./types";
const REFRESH = 5000;
const EVICT_TIMEOUT = 120000;
interface Permissions {
[role: string]: {
[access: number]: {
[resource: string]: {
access: number;
filter: Expression;
validate?: Expression;
};
};
};
}
export class LocalCache<T> {
private nextRefresh = 1;
private currentRevision: string = null;
private snapshots: Map<string, T> = new Map();
const REFRESH = 3000;
const EVICT_TIMEOUT = 60000;
constructor(
private cacheKey: string,
private callback: () => Promise<[string, T]>,
) {}
const snapshots = new Map();
let currentSnapshot = null;
let nextRefresh = 1;
function computeHash(snapshot): string {
// MD5 hash for presets, provisions, virtual parameters for detecting changes
const h = crypto.createHash("md5");
for (const p of snapshot.presets) {
h.update(JSON.stringify(p.name));
h.update(JSON.stringify(p.channel));
h.update(JSON.stringify(p.schedule));
h.update(JSON.stringify(p.events));
h.update(JSON.stringify(p.precondition));
h.update(JSON.stringify(p.provisions));
async getRevision(): Promise<string> {
if (Date.now() > this.nextRefresh) await this.refresh();
return this.currentRevision;
}
let keys;
keys = Object.keys(snapshot.provisions).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(snapshot.provisions[k].md5);
keys = Object.keys(snapshot.virtualParameters).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(snapshot.virtualParameters[k].md5);
keys = Object.keys(snapshot.config).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(JSON.stringify(snapshot.config[k]));
keys = Object.keys(snapshot.files).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(JSON.stringify(snapshot.files[k]));
keys = Object.keys(snapshot.users).sort();
h.update(JSON.stringify(keys));
for (const k of keys) h.update(JSON.stringify(snapshot.users[k]));
const roles = Object.keys(snapshot.permissions).sort();
h.update(JSON.stringify(roles));
for (const r of roles) {
const levels = Object.keys(snapshot.permissions[r]).sort();
h.update(JSON.stringify(levels));
for (const l of levels) {
keys = Object.keys(snapshot.permissions[r][l]).sort();
h.update(JSON.stringify(keys));
for (const k of keys)
h.update(JSON.stringify(snapshot.permissions[r][l][k]));
}
hasRevision(revision: string): boolean {
return this.snapshots.has(revision);
}
return h.digest("hex");
}
function flattenObject(src, prefix = "", dst = {}): {} {
for (const k of Object.keys(src)) {
const v = src[k];
if (typeof v === "object" && !Array.isArray(v))
flattenObject(v, `${prefix}${k}.`, dst);
else dst[`${prefix}${k}`] = v;
get(revision: string): T {
const snapshot = this.snapshots.get(revision);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot;
}
return dst;
}
async function fetchPresets(): Promise<Preset[]> {
const res = await db.getPresets();
let objects = await db.getObjects();
objects = objects.map(obj => {
// Flatten object
obj = flattenObject(obj);
// If no keys are defined, consider all parameters as keys to keep the
// same behavior from v1.0
if (!obj["_keys"] || !obj["_keys"].length)
obj["_keys"] = Object.keys(obj).filter(k => !k.startsWith("_"));
return obj;
});
res.sort((a, b) => {
if (a["weight"] === b["weight"])
return a["_id"] > b["_id"] ? 1 : a["_id"] < b["_id"] ? -1 : 0;
else return a["weight"] - b["weight"];
});
const presets = [] as Preset[];
for (const preset of res) {
let schedule: { md5: string; duration: number; schedule: any } = null;
if (preset["schedule"]) {
const parts = preset["schedule"].trim().split(/\s+/);
schedule = {
md5: crypto
.createHash("md5")
.update(preset["schedule"])
.digest("hex"),
duration: null,
schedule: null
};
try {
schedule.duration = +parts.shift() * 1000;
schedule.schedule = scheduling.parseCron(parts.join(" "));
} catch (err) {
logger.warn({
message: "Invalid preset schedule",
preset: preset["_id"],
schedule: preset["schedule"]
});
schedule.schedule = false;
}
async refresh(): Promise<void> {
if (!this.nextRefresh) {
await setTimeoutPromise(20);
await this.refresh();
return;
}
const events = preset["events"] || {};
let precondition = true as Expression;
if (preset["precondition"]) {
try {
precondition = parse(preset["precondition"]);
} catch (error) {
precondition = mongoQueryToFilter(JSON.parse(preset["precondition"]));
}
const now = Date.now();
if (now < this.nextRefresh) return;
this.nextRefresh = 0;
// Simplify expression
precondition = expression.evaluate(precondition);
const dbHash = await get(this.cacheKey);
if (this.currentRevision && dbHash === this.currentRevision) {
this.nextRefresh = now + (REFRESH - (now % REFRESH));
return;
}
const _provisions = preset["provisions"] || [];
const lockToken = await acquireLock(this.cacheKey, 5000);
// Generate provisions from the old configuration format
for (const c of preset["configurations"]) {
switch (c.type) {
case "age":
_provisions.push(["refresh", c.name, c.age]);
break;
const [hash, snapshot] = await this.callback();
case "value":
_provisions.push(["value", c.name, c.value]);
break;
case "add_tag":
_provisions.push(["tag", c.tag, true]);
break;
case "delete_tag":
_provisions.push(["tag", c.tag, false]);
break;
case "provision":
_provisions.push([c.name].concat(c.args || []));
break;
case "add_object":
for (const obj of objects) {
if (obj["_id"] === c.object) {
const alias = obj["_keys"]
.map(k => `${k}:${JSON.stringify(obj[k])}`)
.join(",");
const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 1]);
for (const k in obj) {
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
_provisions.push(["value", `${p}.${k}`, obj[k]]);
}
}
}
break;
case "delete_object":
for (const obj of objects) {
if (obj["_id"] === c.object) {
const alias = obj["_keys"]
.map(k => `${k}:${JSON.stringify(obj[k])}`)
.join(",");
const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 0]);
}
}
break;
default:
throw new Error(`Unknown configuration type ${c.type}`);
}
if (this.currentRevision) {
const r = this.currentRevision;
const s = this.snapshots.get(r);
setTimeout(() => {
if (this.snapshots.get(r) === s) this.snapshots.delete(r);
}, EVICT_TIMEOUT).unref();
}
presets.push({
name: preset["_id"],
channel: (preset["channel"] as string) || "default",
schedule: schedule,
events: events,
precondition: precondition,
provisions: _provisions
});
}
this.currentRevision = hash;
this.snapshots.set(hash, snapshot);
return presets;
}
async function fetchProvisions(): Promise<{}> {
const res = await db.getProvisions();
const provisions = {};
for (const r of res) {
provisions[r["_id"]] = {};
provisions[r["_id"]].md5 = crypto
.createHash("md5")
.update(r["script"])
.digest("hex");
provisions[r["_id"]].script = new vm.Script(
`"use strict";(function(){\n${r["script"]}\n})();`,
{ filename: r["_id"], lineOffset: -1, timeout: 50 }
);
}
return provisions;
}
async function fetchVirtualParameters(): Promise<{}> {
const res = await db.getVirtualParameters();
const virtualParameters = {};
for (const r of res) {
virtualParameters[r["_id"]] = {};
virtualParameters[r["_id"]].md5 = crypto
.createHash("md5")
.update(r["script"])
.digest("hex");
virtualParameters[r["_id"]].script = new vm.Script(
`"use strict";(function(){\n${r["script"]}\n})();`,
{ filename: r["_id"], lineOffset: -1, timeout: 50 }
);
}
return virtualParameters;
}
async function fetchPermissions(): Promise<Permissions> {
const perms = await db.getPermissions();
const permissions: Permissions = {};
for (const p of perms) {
if (!permissions[p.role]) permissions[p.role] = {};
if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {};
permissions[p.role][p.access][p.resource] = {
access: p.access,
filter: parse(p.filter || "true")
};
if (p.validate)
permissions[p.role][p.access][p.resource].validate = parse(p.validate);
}
return permissions;
}
async function fetchFiles(): Promise<{}> {
const res = await db.getFiles();
const files = {};
for (const r of res) {
const id = r["filename"] || r["_id"].toString();
files[id] = {};
files[id].length = r["length"];
files[id].md5 = r["md5"];
files[id].contentType = r["contentType"];
}
return files;
}
async function fetchUsers(): Promise<{}> {
const _users = await db.getUsers();
const users = {};
for (const user of _users) {
users[user._id] = {
password: user.password,
salt: user.salt,
roles: user.roles.split(",").map(s => s.trim())
};
}
return users;
}
async function fetchConfig(): Promise<[{}, {}]> {
const conf = await db.getConfig();
conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0));
const ui = {
filters: {},
device: {},
index: {},
overview: {}
};
const _config = {};
for (const c of conf) {
// Evaluate expressions to simplify them
const val = expression.evaluate(c.value);
_config[c.id] = val;
if (c.id.startsWith("ui.")) {
const keys = c.id.split(".");
// remove the first key(ui)
keys.shift();
let ref = ui;
while (keys.length > 1) {
const k = keys.shift();
if (typeof ref[k] !== "object") ref[k] = {};
ref = ref[k];
}
ref[keys[0]] = val;
if (lockToken) {
if (hash !== dbHash) await set(this.cacheKey, hash, 300);
await releaseLock(this.cacheKey, lockToken);
}
this.nextRefresh = now + (REFRESH - (now % REFRESH));
}
return [_config, ui];
}
async function refresh(): Promise<void> {
if (!nextRefresh) {
await new Promise(resolve => setTimeout(resolve, 20));
await refresh();
return;
}
nextRefresh = 0;
const now = Date.now();
const dbHash = await cache.get("presets_hash");
if (currentSnapshot && dbHash === currentSnapshot) {
nextRefresh = now + (REFRESH - (now % REFRESH));
return;
}
const unlockOrExtend = await cache.lock("presets_hash_lock", 3);
const res = await Promise.all([
fetchPresets(),
fetchProvisions(),
fetchVirtualParameters(),
fetchFiles(),
fetchPermissions(),
fetchUsers(),
fetchConfig()
]);
const snapshot = {
presets: res[0],
provisions: res[1],
virtualParameters: res[2],
files: res[3],
permissions: res[4],
users: res[5],
config: res[6][0],
ui: res[6][1]
};
if (currentSnapshot) {
const h = currentSnapshot;
const s = snapshots.get(h);
setTimeout(() => {
if (snapshots.get(h) === s) snapshots.delete(h);
}, EVICT_TIMEOUT).unref();
}
currentSnapshot = computeHash(snapshot);
snapshots.set(currentSnapshot, snapshot);
await cache.set("presets_hash", currentSnapshot, 300);
await unlockOrExtend(0);
nextRefresh = now + (REFRESH - (now % REFRESH));
}
export async function getCurrentSnapshot(): Promise<string> {
if (Date.now() > nextRefresh) await refresh();
return currentSnapshot;
}
export function hasSnapshot(hash): boolean {
return snapshots.has(hash);
}
export function getPresets(snapshotKey): Preset[] {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.presets;
}
export function getProvisions(
snapshotKey
): { [name: string]: { md5: string; script: vm.Script } } {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.provisions;
}
export function getVirtualParameters(
snapshotKey
): { [name: string]: { md5: string; script: vm.Script } } {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.virtualParameters;
}
export function getFiles(
snapshotKey
): { [name: string]: { length: number; md5: string; contentType: string } } {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.files;
}
export function getConfig(
snapshotKey,
key,
context?,
now?
): string | number | boolean | null {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
const oldOpts = {
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
"cwmp.debug": "DEBUG",
"cwmp.retryDelay": "RETRY_DELAY",
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
"cwmp.cookiesPath": "COOKIES_PATH",
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
"cwmp.connectionRequestAllowBasicAuth":
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT"
};
if (!(key in snapshot.config)) {
if (key in oldOpts)
return config.get(oldOpts[key], context ? context.id : null);
else return null;
}
return expression.evaluate(snapshot.config[key], context, now || Date.now());
}
export function getConfigExpression(snapshotKey, key): Expression {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.config[key];
}
export function getUsers(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.users;
}
export function getPermissions(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.permissions;
}
export function getUiConfig(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.ui;
}
+50
View File
@@ -0,0 +1,50 @@
import { collections } from "./db/db.ts";
const CLOCK_SKEW_TOLERANCE = 30000;
export async function acquireLock(
lockName: string,
ttl: number,
timeout = 0,
token = Math.random().toString(36).slice(2),
): Promise<string> {
try {
const now = Date.now();
const r = await collections.locks.findOneAndUpdate(
{ _id: lockName, value: token },
{
$set: {
expire: new Date(now + ttl + CLOCK_SKEW_TOLERANCE),
},
$currentDate: { timestamp: true },
},
{ upsert: true, returnDocument: "after" },
);
if (Math.abs(r.value.timestamp.getTime() - now) > CLOCK_SKEW_TOLERANCE)
throw new Error("Database clock skew too great");
} catch (err) {
if (err.code !== 11000) throw err;
if (!(timeout > 0)) return null;
const w = 50 + Math.random() * 50;
await new Promise((resolve) => setTimeout(resolve, w));
return acquireLock(lockName, ttl, timeout - w, token);
}
return token;
}
export async function releaseLock(
lockName: string,
token: string,
): Promise<void> {
const res = await collections.locks.deleteOne({
_id: lockName,
value: token,
});
if (res.deletedCount !== 1) throw new Error("Lock expired");
}
export async function getToken(lockName: string): Promise<string> {
const res = await collections.locks.findOne({ _id: lockName });
return res?.value;
}
+80 -71
View File
@@ -1,26 +1,15 @@
/**
* 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 os from "os";
import * as config from "./config";
import * as fs from "node:fs";
import * as os from "node:os";
import * as config from "./config.ts";
import { getRequestOrigin } from "./forwarded.ts";
import {
SessionContext,
AcsRequest,
CpeRequest,
CpeFault,
InformRequest,
Fault,
} from "./types.ts";
const REOPEN_EVERY = 60000;
@@ -66,7 +55,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 +79,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 +93,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 +109,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 +135,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 +213,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 +229,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 +272,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 +287,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 +312,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);
}
-629
View File
@@ -1,629 +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 { ObjectID } from "mongodb";
import { map, parse, stringify } from "./common/expression-parser";
import { likePatternToRegExp, evaluate } from "./common/expression";
import { Expression, Fault, Task } from "./types";
const isArray = Array.isArray;
export function processDeviceFilter(filter): Expression {
return map(filter, exp => {
if (!isArray(exp)) return exp;
if (exp[0] === "PARAM") {
const p = exp[1];
if (p === "DeviceID.ID") return ["PARAM", "_id"];
else if (p === "DeviceID") return ["PARAM", "_deviceId"];
else if (p.startsWith("DeviceID."))
return ["PARAM", "_deviceId._" + p.slice(9)];
else if (p === "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"];
else if (p === "Events.1_BOOT") return ["PARAM", "_lastBoot"];
} else if (
isArray(exp[1]) &&
exp[1][0] === "PARAM" &&
exp[1][1].startsWith("Tags.")
) {
const t = exp[1][1].slice(5);
if (exp[0] === "IS NULL") return ["<>", ["PARAM", "_tags"], t];
else if (exp[0] === "IS NOT NULL") return ["=", ["PARAM", "_tags"], t];
else if (exp[0] === "=" && exp[2] === true)
return ["=", ["PARAM", "_tags"], t];
} else if (
["=", "<>", ">", ">=", "<", "<=", "LIKE", "NOT LIKE"].includes(exp[0])
) {
let e = map(exp, ee => {
if (
isArray(ee) &&
ee[0] === "PARAM" &&
typeof ee[1] === "string" &&
!ee[1].startsWith("_")
)
return ["PARAM", `${ee[1]}._value`];
return ee;
});
if (typeof e[2] === "number") {
const alt = (e as any[]).slice();
alt[2] = new Date(e[2]);
e = ["OR", e, alt];
}
return e;
}
return exp;
});
}
export function processTasksFilter(filter): Expression {
return map(filter, exp => {
if (!isArray(exp)) return exp;
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
const e = exp.slice();
if (e[1][0] === "PARAM" && e[1][1] === "_id") e[2] = new ObjectID(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "timestamp")
e[2] = new Date(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
e[2] = new Date(e[2]);
return e;
}
return exp;
});
}
export function processFaultsFilter(filter): Expression {
return map(filter, exp => {
if (!isArray(exp)) return exp;
if (["=", "<>", ">", ">=", "<", "<="].includes(exp[0])) {
const e = exp.slice();
if (e[1][0] === "PARAM" && e[1][1] === "timestamp") e[2] = new Date(e[2]);
else if (e[1][0] === "PARAM" && e[1][1] === "expiry")
e[2] = new Date(e[2]);
return e;
}
return exp;
});
}
export function filterToMongoQuery(exp: Expression): {} {
const ops = {
OR: 0,
AND: 0,
NOT: 0,
"=": 1,
"<>": 1,
">": 1,
">=": 1,
"<": 1,
"<=": 1,
LIKE: 1,
"NOT LIKE": 1,
"IS NULL": 1,
"IS NOT NULL": 1
};
function recursive(filter, negate, res = {}): {} {
const op = filter[0];
if (ops[op] === 0) {
for (let i = 1; i < filter.length; ++i) {
if (!isArray(filter[i]) || ops[filter[i][0]] == null)
throw new Error(`Invalid expression in ${op} clause`);
}
if ((!negate && op === "AND") || (negate && op === "OR")) {
res["$and"] = res["$and"] || [];
for (let i = 1; i < filter.length; ++i)
res["$and"].push(recursive(filter[i], negate));
} else if ((!negate && op === "OR") || (negate && op === "AND")) {
res["$or"] = res["$or"] || [];
for (let i = 1; i < filter.length; ++i)
res["$or"].push(recursive(filter[i], negate));
} else if (op === "NOT") {
recursive(filter[1], !negate, res);
}
} else if (ops[op] === 1) {
if (isArray(filter[2])) {
throw new Error(`Right operand of ${op} clause is not a primitive`);
} else if (
["LIKE", "NOT LIKE"].includes(op) &&
isArray(filter[1]) &&
filter[1][0] === "FUNC" &&
["UPPER", "LOWER"].includes(filter[1][1])
) {
if (
(filter[1][1] === "UPPER" && filter[2] !== filter[2].toUpperCase()) ||
(filter[1][1] === "LOWER" && filter[2] !== filter[2].toLowerCase())
) {
throw new Error(
`Cannot compare ${
filter[1][1]
}() against non-${filter[1][1].toLowerCase()}case pattern`
);
}
} else if (!isArray(filter[1]) || filter[1][0] !== "PARAM") {
throw new Error(`Left operand of ${op} clause is not a parameter`);
}
if (op === "=") {
const param = filter[1][1];
const p = (res[param] = res[param] || {});
if (negate) p["$ne"] = filter[2];
else p["$eq"] = filter[2];
} else if (op === "<>") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$ne"] = filter[2];
if (param !== "_tags") p["$exists"] = true;
} else if (op === ">") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$gt"] = filter[2];
} else if (op === ">=") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$gte"] = filter[2];
} else if (op === "<") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$lt"] = filter[2];
} else if (op === "<=") {
const param = filter[1][1];
let p = (res[param] = res[param] || {});
if (negate) p = p["$not"] = p["$not"] || {};
p["$lte"] = filter[2];
} else if (op === "IS NULL") {
const param = filter[1][1];
res[param] = { $exists: negate };
} else if (op === "IS NOT NULL") {
const param = filter[1][1];
res[param] = { $exists: !negate };
} else {
if (op === "NOT LIKE") negate = !negate;
let param;
let flags;
if (filter[1][0] === "FUNC" && filter[1][1] === "UPPER") {
param = filter[1][2][1];
flags = "i";
} else if (filter[1][0] === "FUNC" && filter[1][1] === "LOWER") {
param = filter[1][2][1];
flags = "i";
} else {
param = filter[1][1];
flags = "";
}
const r = likePatternToRegExp(filter[2], filter[3], flags);
if (negate) res[param] = { $not: r };
else res[param] = r;
}
} else {
throw new Error(`Unrecognized operator ${op}`);
}
return res;
}
const _exp = evaluate(exp, null, Date.now());
if (!isArray(_exp)) {
if (_exp === true) return {};
throw new Error("Primitives are not valid queries");
}
return recursive(_exp, false);
}
export function processDeviceProjection(projection: {}): {} {
if (!projection) return projection;
const p = {};
for (const [k, v] of Object.entries(projection)) {
if (k === "DeviceID.ID") {
p["_id"] = 1;
} else if (k.startsWith("DeviceID")) {
p["_deviceId._SerialNumber"] = v;
p["_deviceId._OUI"] = v;
p["_deviceId._ProductClass"] = v;
p["_deviceId._Manufacturer"] = v;
} else if (k.startsWith("Tags")) {
p["_tags"] = v;
} else if (k.startsWith("Events")) {
p["_lastInform"] = v;
p["_registered"] = v;
p["_lastBoot"] = v;
p["_lastBootstrap"] = v;
} else {
p[k] = v;
}
}
return p;
}
export function processDeviceSort(sort: {}): {} {
if (!sort) return sort;
const s = {};
for (const [k, v] of Object.entries(sort)) {
if (k === "DeviceID.ID") s["_id"] = v;
else if (k.startsWith("DeviceID.")) s[`_deviceId._${k.slice(9)}`] = v;
else if (k === "Events.Inform") s["_lastInform"] = v;
else if (k === "Events.Registered") s["_registered"] = v;
else if (k === "Events.1_BOOT") s["_lastBoot"] = v;
else if (k === "Events.0_BOOTSTRAP") s["_lastBootstrap"] = v;
else s[`${k}._value`] = v;
}
return s;
}
function parseDate(d): number | string {
const n = +d;
return isNaN(n) ? "" + d : n;
}
interface FlatDevice {
[param: string]: {
object: boolean;
objectTimestamp: number;
writable: boolean;
writableTimestamp: number;
value: [string | number | boolean, string];
valueTimestamp: number;
};
}
export function flattenDevice(device): FlatDevice {
function recursive(input, root, output, timestamp): void {
for (const [name, tree] of Object.entries(input)) {
if (!root) {
if (name === "_lastInform") {
output["Events.Inform"] = {
value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_registered") {
output["Events.Registered"] = {
value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_lastBoot") {
output["Events.1_BOOT"] = {
value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_lastBootstrap") {
output["Events.0_BOOTSTRAP"] = {
value: [parseDate(tree), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_id") {
output["DeviceID.ID"] = {
value: [tree, "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_deviceId") {
output["DeviceID.Manufacturer"] = {
value: [tree["_Manufacturer"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
output["DeviceID.OUI"] = {
value: [tree["_OUI"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
output["DeviceID.ProductClass"] = {
value: [tree["_ProductClass"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
output["DeviceID.SerialNumber"] = {
value: [tree["_SerialNumber"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
} else if (name === "_tags") {
output["Tags"] = {
writable: true,
writableTimestamp: timestamp,
object: true,
objectTimestamp: timestamp
};
for (const t of tree as string[]) {
output[`Tags.${t}`] = {
value: [true, "xsd:boolean"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp
};
}
}
}
if (name.startsWith("_")) continue;
let childrenTimestamp = timestamp;
if (!root) childrenTimestamp = +(input["_timestamp"] || 1);
else if (+input["_timestamp"] > timestamp)
childrenTimestamp = +input["_timestamp"];
const attrs = {};
if (tree["_value"] != null) {
attrs["value"] = [
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
tree["_type"]
];
attrs["valueTimestamp"] = +(tree["_timestamp"] || childrenTimestamp);
attrs["object"] = false;
attrs["objectTimestamp"] = childrenTimestamp;
} else if (tree["_object"] != null) {
attrs["object"] = tree["_object"];
attrs["objectTimestamp"] = childrenTimestamp;
}
if (tree["_writable"] != null) {
attrs["writable"] = tree["_writable"];
attrs["writableTimestamp"] = childrenTimestamp;
}
const r = root ? `${root}.${name}` : name;
output[r] = attrs;
if (attrs["object"] || tree["object"] == null)
recursive(tree, r, output, childrenTimestamp);
}
}
const newDevice = {};
const timestamp = new Date(device["_lastInform"] || 1).getTime();
recursive(device, "", newDevice, timestamp);
return newDevice;
}
export function flattenFault(fault): Fault {
const f = Object.assign({}, fault);
if (f.timestamp) f.timestamp = +f.timestamp;
if (f.expiry) f.expiry = +f.expiry;
return f;
}
export function flattenTask(task): Task {
const t = Object.assign({}, task);
t._id = "" + t._id;
if (t.timestamp) t.timestamp = +t.timestamp;
if (t.expiry) t.expiry = +t.expiry;
return t;
}
export function mongoQueryToFilter(query): Expression {
function recursive(_query): Expression {
const expressions: Expression[] = [];
for (const [k, v] of Object.entries(_query)) {
if (k[0] === "$") {
if (k === "$and") {
const and: Expression = ["AND"];
for (const vv of Object.values(v)) and.push(recursive(vv));
expressions.push(and);
} else if (k === "$or") {
const or: Expression = ["OR"];
for (const vv of Object.values(v)) or.push(recursive(vv));
expressions.push(or);
} else {
throw new Error(`Operator ${k} not supported`);
}
} else if (k === "_tags") {
if (typeof v === "object") {
if (isArray(v)) throw new Error(`Invalid type`);
if (v.hasOwnProperty("$ne"))
expressions.push(["IS NULL", ["PARAM", `Tags.${v["$ne"]}`]]);
else if (v.hasOwnProperty("$eq"))
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v["$eq"]}`]]);
else throw new Error(`Invalid tag query`);
} else {
expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]);
}
} else if (k.startsWith("Tags.")) {
let exists: boolean;
if (typeof v === "boolean") exists = v;
else if (v.hasOwnProperty("$eq")) exists = !!v["$eq"];
else if (v.hasOwnProperty("$ne")) exists = !v["$ne"];
else if (v.hasOwnProperty("$exists")) exists = !!v["$exists"];
else throw new Error(`Invalid tag query`);
expressions.push([exists ? "IS NOT NULL" : "IS NULL", ["PARAM", k]]);
} else if (typeof v === "object") {
if (isArray(v)) throw new Error(`Invalid type`);
const exps: Expression[] = [];
for (const [kk, vv] of Object.entries(v)) {
let op;
switch (kk) {
case "$eq":
op = "=";
break;
case "$ne":
op = "<>";
break;
case "$lt":
op = "<";
break;
case "$lte":
op = "<=";
break;
case "$gt":
op = ">";
break;
case "$gte":
op = ">=";
break;
default:
throw new Error(`Operator ${kk} not supported`);
}
exps.push([op, ["PARAM", k], vv]);
}
if (exps.length === 1) 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);
}
// empty filter
if (!Object.keys(query).length) return true;
return recursive(query);
}
export function flattenPreset(preset): {} {
const p = Object.assign({}, preset);
if (p.precondition) {
try {
// Try parse to check expression validity
parse(p.precondition);
} catch (error) {
p.precondition = mongoQueryToFilter(JSON.parse(p.precondition));
p.precondition = p.precondition.length ? stringify(p.precondition) : "";
}
}
if (p.events) {
const e = [];
for (const [k, v] of Object.entries(p.events)) e.push(v ? k : `-${k}`);
p.events = e.join(", ");
}
const provision = p.configurations[0];
if (
p.configurations.length === 1 &&
provision.type === "provision" &&
provision.name &&
provision.name.length
) {
p.provision = provision.name;
p.provisionArgs = provision.args
? JSON.stringify(provision.args).slice(1, -1)
: "";
}
delete p.configurations;
return p;
}
export function flattenFile(file): {} {
const f = {};
f["_id"] = file["_id"];
if (file.metadata) {
f["metadata.fileType"] = file.metadata.fileType || "";
f["metadata.oui"] = file.metadata.oui || "";
f["metadata.productClass"] = file.metadata.productClass || "";
f["metadata.version"] = file.metadata.version || "";
}
return f;
}
export function preProcessPreset(data): {} {
const preset = Object.assign({}, data);
if (!preset.precondition) preset.precondition = "";
// Try parse to check expression validity
parse(preset.precondition);
preset.weight = parseInt(preset.weight) || 0;
const events = {};
if (preset.events) {
for (let e of preset.events.split(",")) {
let v = true;
e = e.trim();
if (e.startsWith("-")) {
v = false;
e = e.slice(1).trim();
}
if (e) events[e] = v;
}
}
preset.events = events;
if (!preset.provision) throw new Error("Invalid preset provision");
const configuration = {
type: "provision",
name: preset.provision,
args: null
};
if (preset.provisionArgs)
configuration.args = JSON.parse(`[${preset.provisionArgs}]`);
delete preset.provision;
delete preset.provisionArgs;
preset.configurations = [configuration];
return preset;
}
+637 -676
View File
File diff suppressed because it is too large. Load diff
+98
View File
@@ -0,0 +1,98 @@
import { platform } from "node:os";
import { exec } from "node:child_process";
import { domainToASCII } from "node:url";
export interface PingResult {
packetsTransmitted: number;
packetsReceived: number;
packetLoss: number;
min: number;
avg: number;
max: number;
mdev: number;
}
function isValidHost(host: string): boolean {
// Valid chars in IPv4, IPv6, domain names
if (/^[a-zA-Z0-9\-.:[\]-]+$/.test(host)) return true;
// Check if input is an IDN convert to Punycode
// Can't merge with above because domainToASCII doesn't accept IP addresses
return /^[a-zA-Z0-9\-.:[\]-]+$/.test(domainToASCII(host));
}
export function parsePing(osPlatform: string, stdout: string): PingResult {
let parseRegExp1: RegExp, parseRegExp2: RegExp, parsed: PingResult;
switch (osPlatform) {
case "linux":
parseRegExp1 =
/(\d+) packets transmitted, (\d+) .*received, ([\d.]+)% .*loss[^]*= ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+)/;
parseRegExp2 =
/(\d+) packets transmitted, (\d+) .*received, ([\d.]+)% .*loss/;
break;
case "freebsd":
parseRegExp1 =
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss\nround-trip min\/avg\/max\/stddev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/;
parseRegExp2 =
/(\d+) packets transmitted, (\d+) packets received, ([\d.]+)% packet loss/;
break;
}
const m1 = stdout.match(parseRegExp1);
if (m1) {
parsed = {
packetsTransmitted: +m1[1],
packetsReceived: +m1[2],
packetLoss: +m1[3],
min: +m1[4],
avg: +m1[5],
max: +m1[6],
mdev: +m1[7],
};
} else {
const m2 = stdout.match(parseRegExp2);
if (m2) {
parsed = {
packetsTransmitted: +m2[1],
packetsReceived: +m2[2],
packetLoss: +m2[3],
min: null,
avg: null,
max: null,
mdev: null,
};
}
}
return parsed;
}
export function ping(
host: string,
callback: (err: Error, res?: PingResult, stdout?: string) => void,
): void {
// Validate input to prevent possible remote code execution
// Credit to Alex Hordijk for reporting this vulnerability
if (!isValidHost(host)) return callback(new Error("Invalid host"));
host = host.replace("[", "").replace("]", "");
let cmd: string;
switch (platform()) {
case "linux":
cmd = `ping -w 1 -i 0.2 -c 3 ${host}`;
break;
case "freebsd":
// Send a single packet because on FreeBSD only superuser can send
// packets that are only 200 ms apart.
cmd = `ping -t 1 -c 3 ${host}`;
break;
default:
return callback(new Error("Platform not supported"));
}
exec(cmd, (err, stdout) => {
if (err) return callback(err);
const parsed: PingResult = parsePing(platform(), stdout);
return callback(err, parsed, stdout);
});
}
+32 -119
View File
@@ -1,23 +1,6 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import * as common from "./common";
function 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 +17,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 +35,25 @@ function normalize(input): any {
return input;
}
export function expandValue(value): any[] {
if (common.typeOf(value) === common.ARRAY_TYPE) {
const EXPAND_OPS = new Set([
"$eq",
"$gt",
"$gte",
"$in",
"$lt",
"$lte",
"$ne",
"$nin",
]);
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;
}
@@ -69,7 +63,8 @@ export function expandValue(value): any[] {
const values = [];
for (const [k, v] of Object.entries(value)) {
keys.push(k);
values.push(expandValue(v));
if (EXPAND_OPS.has(k)) values.push(expandValue(v));
else values.push([v]);
indices.push(0);
}
@@ -103,12 +98,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) {
@@ -116,7 +113,7 @@ export function expand(query): {} {
if (v && (v["$ne"] != null || v["$not"] != null)) {
if (Object.keys(v).length > 1)
throw new Error("Cannot mix $ne or $not with other operators");
for (const c of conditions) newQuery["$an"].push(c);
for (const c of conditions) newQuery["$and"].push(c);
} else {
newQuery["$and"].push({ $or: conditions });
}
@@ -129,100 +126,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":
+55 -63
View File
@@ -1,30 +1,11 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import * as vm from "vm";
import * as vm from "node:vm";
import seedrandom from "seedrandom";
import * as device from "./device";
import * as extensions from "./extensions";
import * as logger from "./logger";
import * as scheduling from "./scheduling";
import Path from "./common/path";
import { Fault, SessionContext, ScriptResult } from "./types";
import * as device from "./device.ts";
import * as extensions from "./extensions.ts";
import * as logger from "./logger.ts";
import * as scheduling from "./scheduling.ts";
import Path from "./common/path.ts";
import { Fault, SessionContext, ScriptResult } from "./types.ts";
// Used for throwing to exit user script and commit
const COMMIT = Symbol();
@@ -34,7 +15,7 @@ const EXT = Symbol();
const UNDEFINED = undefined;
const context = vm.createContext();
const context = vm.createContext(undefined, { microtaskMode: "afterEvaluate" });
let state;
@@ -50,7 +31,7 @@ function runExtension(sessionContext, key, extCall): Promise<Fault> {
}
let prom = re.get(key);
if (!prom) {
if (prom == null) {
re.set(
key,
(prom = new Promise((resolve, reject) => {
@@ -62,7 +43,7 @@ function runExtension(sessionContext, key, extCall): Promise<Fault> {
resolve(fault);
})
.catch(reject);
}))
})),
);
}
@@ -78,7 +59,7 @@ class SandboxDate {
number?,
number?,
number?,
number?
number?,
]
) {
if (argumentList.length) return new Date(...argumentList);
@@ -109,6 +90,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 +108,7 @@ function random(): number {
return state.rng();
}
random.seed = function(s) {
random.seed = function (s) {
state.rng = seedrandom(s);
};
@@ -125,7 +116,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) {
@@ -133,7 +124,7 @@ class ParameterWrapper {
unpacked = device.unpack(
state.sessionContext.deviceData,
path,
state.revision
state.revision,
);
}
@@ -141,18 +132,18 @@ class ParameterWrapper {
const attr = state.sessionContext.deviceData.attributes.get(
unpacked[0],
state.revision
state.revision,
)[attrName];
if (!attr) return UNDEFINED;
return attr[1];
}
},
});
}
Object.defineProperty(this, "path", {
get: function() {
get: function () {
if (state.uncommitted) commit();
if (state.revision !== unpackedRevision) {
@@ -160,18 +151,18 @@ class ParameterWrapper {
unpacked = device.unpack(
state.sessionContext.deviceData,
path,
state.revision
state.revision,
);
}
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) {
@@ -179,17 +170,17 @@ class ParameterWrapper {
unpacked = device.unpack(
state.sessionContext.deviceData,
path,
state.revision
state.revision,
);
}
if (!unpacked.length) return UNDEFINED;
return unpacked.length;
}
},
});
this[Symbol.iterator] = function*() {
this[Symbol.iterator] = function* () {
if (state.uncommitted) commit();
if (state.revision !== unpackedRevision) {
@@ -197,7 +188,7 @@ class ParameterWrapper {
unpacked = device.unpack(
state.sessionContext.deviceData,
path,
state.revision
state.revision,
);
}
@@ -210,7 +201,7 @@ class ParameterWrapper {
function declare(
path: string,
timestamps: { [attr: string]: number },
values: { [attr: string]: any }
values: { [attr: string]: any },
): ParameterWrapper {
state.uncommitted = true;
if (!timestamps) timestamps = {};
@@ -225,7 +216,7 @@ function declare(
pathSet: null,
attrGet: null,
attrSet: null,
defer: true
defer: true,
};
const attrs = new Set();
@@ -245,6 +236,7 @@ function declare(
}
for (const [attrName, attrTimestamp] of Object.entries(timestamps)) {
if (!(attrTimestamp >= 1)) continue;
if (attrName === "path") {
declaration.pathGet = attrTimestamp;
} else {
@@ -275,14 +267,14 @@ function commit(): void {
throw COMMIT;
} else if (state.revision > state.maxRevision + 1) {
throw new Error(
"Declare function should not be called from within a try/catch block"
"Declare function should not be called from within a try/catch block",
);
}
}
function ext(): 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 +284,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,20 +324,20 @@ function errorToFault(err: Error): Fault {
message: err.message,
detail: {
name: err.name,
message: err.message
}
message: err.message,
},
};
if (err.stack) {
fault.detail["stack"] = err.stack;
// Trim the stack trace at the self-executing anonymous wrapper function
const stackTrimIndex = fault.detail["stack"].match(
/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/
/\s+at\s[^\s]+\s+at\s[^\s]+\s\(vm\.js.+\)/,
);
if (stackTrimIndex) {
fault.detail["stack"] = fault.detail["stack"].slice(
0,
stackTrimIndex.index
stackTrimIndex.index,
);
}
}
@@ -355,11 +347,11 @@ function errorToFault(err: Error): Fault {
export async function run(
script: vm.Script,
globals: {},
globals: Record<string, unknown>,
sessionContext: SessionContext,
startRevision: number,
maxRevision: number,
extCounter = 0
extCounter = 0,
): Promise<ScriptResult> {
state = {
sessionContext: sessionContext,
@@ -370,7 +362,7 @@ export async function run(
extensions: {},
clear: [],
rng: null,
extCounter: extCounter
extCounter: extCounter,
};
for (const n of Object.keys(context)) delete context[n];
@@ -380,7 +372,7 @@ export async function run(
let ret, status;
try {
ret = script.runInContext(context, { displayErrors: false });
ret = script.runInContext(context, { displayErrors: false, timeout: 50 });
status = 0;
} catch (err) {
if (err === COMMIT) {
@@ -393,7 +385,7 @@ export async function run(
clear: null,
declare: null,
done: false,
returnValue: null
returnValue: null,
};
}
}
@@ -404,7 +396,7 @@ export async function run(
await Promise.all(
Object.entries(_state.extensions).map(async ([k, v]) => {
fault = (await runExtension(_state.sessionContext, k, v)) || fault;
})
}),
);
if (fault) {
@@ -413,7 +405,7 @@ export async function run(
clear: null,
declare: null,
done: false,
returnValue: null
returnValue: null,
};
}
@@ -424,7 +416,7 @@ export async function run(
sessionContext,
startRevision,
maxRevision,
extCounter - _state.extCounter
extCounter - _state.extCounter,
);
}
@@ -433,6 +425,6 @@ export async function run(
clear: _state.clear,
declare: _state.declarations,
done: status === 0,
returnValue: ret
returnValue: ret,
};
}
+17 -31
View File
@@ -1,30 +1,8 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import * as crypto from "crypto";
import * as later from "later";
import * as crypto from "node:crypto";
import * as later from "@breejs/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 +11,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;
+131 -47
View File
@@ -1,30 +1,14 @@
/**
* 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 http from "http";
import * as https from "https";
import * as path from "path";
import { ROOT_DIR } from "./config";
import { readFileSync } from "node:fs";
import * as http from "node:http";
import * as https from "node:https";
import { Socket } from "node:net";
import * as path from "node:path";
import { X509Certificate, createPrivateKey } from "node:crypto";
import { ROOT_DIR } from "./config.ts";
let server: http.Server | https.Server;
let listener: (...args) => void;
let listener: http.RequestListener;
let stopping = false;
function closeServer(timeout, callback): void {
if (!server) return void callback();
@@ -46,42 +30,137 @@ 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;
requestTimeout?: number;
onConnection?: (socket: Socket) => void;
onClientError?: (err: Error, socket: Socket) => void;
}
interface SocketEndpoint {
localAddress: string;
localPort: number;
remoteAddress: string;
remotePort: number;
remoteFamily: "IPv4" | "IPv6";
}
// Save this info as they're not accessible after a socket has been closed
const socketEndpoints: WeakMap<Socket, SocketEndpoint> = new WeakMap();
type Promisify<T extends (...args: any) => any> = (
...args: Parameters<T>
) => Promise<ReturnType<T>>;
function getValidPrivKeys(value: string): Buffer[] {
return value.split(":").map((str) => {
str = str.trim();
const buf = str.startsWith("-----BEGIN ")
? Buffer.from(str)
: readFileSync(path.resolve(ROOT_DIR, str));
try {
createPrivateKey(buf);
} catch (err) {
throw new Error(`Invalid private key: ${err.message}`);
}
return buf;
});
}
function getValidCerts(value: string): Buffer[] {
return value.split(":").map((str) => {
str = str.trim();
const buf = str.startsWith("-----BEGIN ")
? Buffer.from(str)
: readFileSync(path.resolve(ROOT_DIR, str));
try {
new X509Certificate(buf);
} catch (err) {
throw new Error(`Invalid certificate: ${err.message}`);
}
return buf;
});
}
export function start(
port,
networkInterface,
ssl,
_listener,
onConnection?,
keepAliveTimeout: number = 0
options: ServerOptions,
_listener: Promisify<http.RequestListener>,
): void {
listener = _listener;
listener = (req, res) => {
if (stopping) res.setHeader("Connection", "close");
_listener(req, res).catch((err) => {
try {
res.socket.unref();
if (res.headersSent) {
res.writeHead(500, { Connection: "close" });
res.end(`${err.name}: ${err.message}`);
}
} catch (err) {
// Ignore
}
throw err;
});
};
if (ssl && ssl.key && ssl.cert) {
const options = {
key: ssl.key
.split(":")
.map(f => fs.readFileSync(path.resolve(ROOT_DIR, f.trim()))),
cert: ssl.cert
.split(":")
.map(f => fs.readFileSync(path.resolve(ROOT_DIR, f.trim())))
if (options.ssl) {
const opts = {
key: getValidPrivKeys(options.ssl.key),
cert: getValidCerts(options.ssl.cert),
};
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.on("connection", (socket: Socket) => {
socketEndpoints.set(socket, {
localAddress: socket.localAddress,
localPort: socket.localPort,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
remoteFamily: socket.remoteFamily as "IPv4" | "IPv6",
});
});
if (options.onClientError) {
server.on("clientError", (err, socket: Socket) => {
if (err["code"] !== "ECONNRESET" && socket.writable)
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
// As per Node docs: This event is guaranteed to be passed an instance
// of the <net.Socket> class
options.onClientError(err, socket as Socket);
});
}
server.timeout = options.timeout || 0;
if (options.keepAliveTimeout != null)
server.keepAliveTimeout = options.keepAliveTimeout;
if (options.requestTimeout != null)
server.requestTimeout = options.requestTimeout;
server.listen({ port: options.port, host: options.host });
}
export function stop(): Promise<void> {
export function stop(terminateConnections = true): Promise<void> {
stopping = terminateConnections;
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Could not close server in a timely manner"));
@@ -89,3 +168,8 @@ export function stop(): Promise<void> {
closeServer(20000, resolve);
});
}
export function getSocketEndpoints(socket: Socket): SocketEndpoint {
// TLSSocket keeps a reference to the raw TCP socket in _parent
return socketEndpoints.get(socket["_parent"] ?? socket);
}
+988 -590
View File
File diff suppressed because it is too large. Load diff
-745
View File
@@ -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)
};
}
+483 -141
View File
@@ -1,43 +1,36 @@
/**
* 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,
Element,
parseAttrs,
encodeEntities,
decodeEntities
} from "./xml-parser";
import memoize from "./common/memoize";
decodeEntities,
} from "./xml-parser.ts";
import memoize from "./common/memoize.ts";
import { version as VERSION } from "../package.json";
import {
CpeGetResponse,
CpeSetResponse,
InformRequest,
AcsResponse,
CpeRequest,
FaultStruct,
SpvFault,
CpeFault,
SoapMessage,
TransferCompleteRequest
} from "./types";
TransferCompleteRequest,
AcsRequest,
type GetParameterNamesResponse,
type GetParameterValuesResponse,
type GetParameterAttributesResponse,
type SetParameterValuesResponse,
type SetParameterAttributesResponse,
type AddObjectResponse,
type DeleteObject,
type DeleteObjectResponse,
type RebootResponse,
type FactoryResetResponse,
type DownloadResponse,
GetRPCMethodsRequest,
RequestDownloadRequest,
AcsResponse,
} from "./types.ts";
import Path from "./common/path.ts";
const SERVER_NAME = `GenieACS/${VERSION}`;
@@ -47,60 +40,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 +106,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][] {
xml: Element,
): [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 +156,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 +182,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 +192,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 +268,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 +281,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 +330,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 +411,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 +478,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 || ""
methodRequest.username || "",
)}</Username><Password>${encodeEntities(
methodRequest.password || ""
)}</Password><FileSize>${methodRequest.fileSize ||
"0"}</FileSize><TargetFileName>${encodeEntities(
methodRequest.targetFileName || ""
)}</TargetFileName><DelaySeconds>${methodRequest.delaySeconds ||
"0"}</DelaySeconds><SuccessURL>${encodeEntities(
methodRequest.successUrl || ""
methodRequest.password || "",
)}</Password><FileSize>${
methodRequest.fileSize || "0"
}</FileSize><TargetFileName>${encodeEntities(
methodRequest.targetFileName || "",
)}</TargetFileName><DelaySeconds>${
methodRequest.delaySeconds || "0"
}</DelaySeconds><SuccessURL>${encodeEntities(
methodRequest.successUrl || "",
)}</SuccessURL><FailureURL>${encodeEntities(
methodRequest.failureUrl || ""
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 +522,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 +584,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 +621,7 @@ function InformResponse(): string {
return "<cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse>";
}
function GetRPCMethods(): AcsResponse {
function GetRPCMethods(): GetRPCMethodsRequest {
return { name: "GetRPCMethods" };
}
@@ -405,12 +629,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 +655,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 +700,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,
};
}
@@ -452,8 +711,25 @@ function RequestDownloadResponse(): string {
return "<cwmp:RequestDownloadResponse></cwmp:RequestDownloadResponse>";
}
function AcsFault(f: CpeFault): string {
return `<soap-env:Body:Fault><faultcode>${encodeEntities(
f.faultCode,
)}</faultcode><faultstring>${encodeEntities(
f.faultString,
)}</faultstring><detail><cwmp:Fault><FaultCode>${encodeEntities(
f.detail.faultCode,
)}</FaultCode><FaultString>${encodeEntities(
f.detail.faultString,
)}</FaultString></cwmp:Fault></detail></soap-env:Body:Fault>`;
}
function faultStruct(xml: Element): FaultStruct {
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 +740,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 +757,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 +792,46 @@ 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,
unknownMethod: null,
};
if (!body.length) return rpc;
@@ -555,7 +870,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 +878,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 +925,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;
@@ -632,7 +953,8 @@ export function request(body: string, cwmpVersion, warn): SoapMessage {
rpc.cpeFault = fault(methodElement);
break;
default:
throw new Error(`8000 Method not supported ${methodElement.localName}`);
rpc.unknownMethod = methodElement.localName;
break;
}
return rpc;
@@ -653,13 +975,19 @@ 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;
acsFault?: CpeFault;
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 +1008,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 +1022,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,8 +1047,12 @@ 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}`,
);
}
} else if (rpc.acsFault) {
body = AcsFault(rpc.acsFault);
}
headers["Content-Type"] = 'text/xml; charset="utf-8"';
@@ -721,6 +1063,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>`,
};
}
+261 -94
View File
@@ -1,27 +1,9 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import Path from "./common/path";
import PathSet from "./common/path-set";
import VersionedMap from "./versioned-map";
import InstanceSet from "./instance-set";
import { IncomingMessage, ServerResponse } from "http";
import { IncomingMessage, ServerResponse } from "node:http";
import { Script } from "node:vm";
import Path from "./common/path.ts";
import PathSet from "./common/path-set.ts";
import VersionedMap from "./versioned-map.ts";
import InstanceSet from "./instance-set.ts";
export type Expression = string | number | boolean | null | any[];
@@ -51,18 +33,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,13 +63,22 @@ 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[];
}?,
];
export interface SyncState {
@@ -90,8 +87,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>;
@@ -120,7 +120,7 @@ export interface SessionContext {
AttributeTimestamps,
AttributeValues,
AttributeTimestamps,
AttributeValues
AttributeValues,
][][];
revisions: number[];
rpcCount: number;
@@ -131,7 +131,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 +138,12 @@ 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 +151,7 @@ export interface SessionContext {
}
export interface Task {
_id: string;
_id?: string;
name: string;
parameterNames?: string[];
parameterValues?: [string, string | number | boolean, string?][];
@@ -160,6 +160,7 @@ export interface Task {
fileName?: string;
targetFileName?: string;
expiry?: number;
provisions?: [string, ...Expression[]][];
}
export interface Operation {
@@ -176,44 +177,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 +270,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 +353,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 +364,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 +408,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,8 +429,14 @@ export interface Declaration {
export type Clear = [
Path,
number,
{ object?: number; writable?: number; value?: number }?,
number?
{
object?: number;
writable?: number;
value?: number;
notification?: number;
accessList?: number;
}?,
number?,
];
export interface Preset {
@@ -321,8 +444,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 +484,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;
@@ -340,6 +506,7 @@ export interface SoapMessage {
cpeRequest?: CpeRequest;
cpeFault?: CpeFault;
cpeResponse?: CpeResponse;
unknownMethod?: string;
}
export interface ScriptResult {
+99 -70
View File
@@ -1,23 +1,4 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import { Z_SYNC_FLUSH } from "zlib";
import { constants } from "node:zlib";
import Koa from "koa";
import Router from "koa-router";
import * as jwt from "jsonwebtoken";
@@ -25,15 +6,17 @@ import koaStatic from "koa-static";
import koaCompress from "koa-compress";
import koaBodyParser from "koa-bodyparser";
import koaJwt from "koa-jwt";
import * as config from "./config";
import api from "./ui/api";
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 * as init from "./init";
import * as config from "./config.ts";
import api from "./ui/api.ts";
import Authorizer from "./common/authorizer.ts";
import * as logger from "./logger.ts";
import * as localCache from "./ui/local-cache.ts";
import { PermissionSet } from "./types.ts";
import { authLocal } from "./api-functions.ts";
import * as init from "./init.ts";
import { version as VERSION } from "../package.json";
import memoize from "./common/memoize.ts";
import { APP_JS, APP_CSS, FAVICON_PNG } from "../build/assets.ts";
declare module "koa" {
interface Request {
@@ -47,22 +30,31 @@ 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", (err, ctx) => {
setTimeout(() => {
// Ignored errors resulting from aborted requests
if (ctx?.req.aborted) return;
// Ignore client errors (e.g. malicious path)
if (err.status === 400) return;
throw err;
});
});
koa.use(async (ctx, next) => {
const configSnapshot = await localCache.getCurrentSnapshot();
const configSnapshot = await localCache.getRevision();
ctx.state.configSnapshot = configSnapshot;
ctx.set("X-Config-Snapshot", configSnapshot);
ctx.set("GenieACS-Version", VERSION);
@@ -73,19 +65,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 +116,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 +134,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 +160,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 +185,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,36 +206,39 @@ 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) => {
// koa-router seems to tolerate double slashes in the URL but that can
// be problematic when using relatives asset paths in HTML
if (ctx.path.endsWith("//")) return;
const permissionSets: PermissionSet[] =
ctx.state.authorizer.getPermissionSets();
let wizard = "";
if (!Object.keys(localCache.getUsers(ctx.state.configSnapshot)).length)
wizard = '<script>window.location.hash = "#!/wizard";</script>';
ctx.body = `
ctx.body = `<!DOCTYPE html>
<html>
<head>
<title>GenieACS</title>
<link rel="shortcut icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="app.css">
<link rel="shortcut icon" type="image/png" href="${FAVICON_PNG}" />
<link rel="stylesheet" href="${APP_CSS}">
</head>
<body>
<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)};
window.username = ${JSON.stringify(
ctx.state.user ? ctx.state.user.username : ""
ctx.state.user ? ctx.state.user.username : "",
)};
window.permissionSets = ${JSON.stringify(permissionSets)};
</script>
<script src="app.js"></script>${wizard}
<script type="module" src="${APP_JS}"></script>${wizard}
</body>
</html>
`;
@@ -228,8 +246,19 @@ router.get("/", async ctx => {
koa.use(
koaCompress({
flush: Z_SYNC_FLUSH
})
gzip: {
flush: constants.Z_SYNC_FLUSH,
},
deflate: {
flush: constants.Z_SYNC_FLUSH,
},
br: {
flush: constants.BROTLI_OPERATION_FLUSH,
params: {
[constants.BROTLI_PARAM_QUALITY]: 5,
},
},
}),
);
koa.use(router.routes());
-207
View File
@@ -1,207 +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 { exec } from "child_process";
import { ObjectID } from "mongodb";
import * as db from "./db";
import { del } from "../cache";
import { getUsers } from "../local-cache";
import { hashPassword } from "../auth";
import {
insertTasks,
watchTask,
connectionRequest,
deleteDevice
} from "../api-functions";
async function deleteFault(id): Promise<void> {
const deviceId = id.split(":", 1)[0];
const channel = id.slice(deviceId.length + 1);
await Promise.all([
db.deleteFault(id),
channel.startsWith("task_")
? db.deleteTask(new ObjectID(channel.slice(5)))
: null
]);
await del(`${deviceId}_tasks_faults_operations`);
}
export async function deleteResource(resource, id): Promise<void> {
switch (resource) {
case "devices":
await deleteDevice(id);
break;
case "files":
await db.deleteFile(id);
break;
case "faults":
await deleteFault(id);
break;
case "provisions":
await db.deleteProvision(id);
break;
case "presets":
await db.deletePreset(id);
break;
case "virtualParameters":
await db.deleteVirtualParameter(id);
break;
case "config":
await db.deleteConfig(id);
break;
case "permissions":
await db.deletePermission(id);
break;
case "users":
await db.deleteUser(id);
break;
}
await del("presets_hash");
}
export async function postTasks(
deviceId,
tasks,
timeout
): Promise<{ connectionRequest: string; tasks: any[] }> {
for (const task of tasks) {
delete task._id;
task.device = deviceId;
}
tasks = await insertTasks(tasks);
const statuses = tasks.map(t => {
return { _id: t._id, status: "pending" };
});
await del(`${deviceId}_tasks_faults_operations`);
try {
await connectionRequest(deviceId);
} catch (err) {
return {
connectionRequest: err.message,
tasks: statuses
};
}
const sample = tasks[tasks.length - 1];
// Waiting for session to finish or timeout
await watchTask(deviceId, sample._id, timeout);
const promises = [];
for (const s of statuses) {
promises.push(db.query("tasks", ["=", ["PARAM", "_id"], s._id]));
promises.push(
db.query("faults", ["=", ["PARAM", "_id"], `${deviceId}:task_${s._id}`])
);
}
const res = await Promise.all(promises);
for (const [i, r] of statuses.entries()) {
if (res[i * 2].length === 0) {
r.status = "done";
} else if (res[i * 2 + 1].length === 1) {
r.status = "fault";
r.fault = res[i * 2 + 1][0];
}
db.deleteTask(r._id);
}
return { connectionRequest: "OK", tasks: statuses };
}
interface PingResponse {
packetsTransmitted: number;
packetsReceived: number;
packetLoss: string;
min: number;
avg: number;
max: number;
mdev: number;
}
export 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> {
if (resource === "presets") {
await db.putPreset(id, data);
} else if (resource === "provisions") {
await db.putProvision(id, data);
} else if (resource === "virtualParameters") {
await db.putVirtualParameter(id, data);
} else if (resource === "config") {
await db.putConfig(id, data);
} else if (resource === "permissions") {
await db.putPermission(id, data);
} else if (resource === "users") {
delete data.password;
delete data.salt;
await db.putUser(id, data);
}
await del("presets_hash");
}
export function authSimple(snapshot, username, password): Promise<string[]> {
return new Promise((resolve, reject) => {
const users = getUsers(snapshot);
const user = users[username];
if (!user || !user.password) return void resolve(null);
hashPassword(password, user.salt)
.then(hash => {
if (hash === user.password) resolve(user.roles);
else resolve(null);
})
.catch(reject);
});
}
+373 -185
View File
@@ -1,33 +1,22 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
import * as stream from "stream";
import { PassThrough } from "node:stream";
import Router from "koa-router";
import * as db from "./db";
import * as apiFunctions from "./api-functions";
import { evaluate, and, extractParams } from "../common/expression";
import { parse } from "../common/expression-parser";
import * as logger from "../logger";
import { getConfig } from "../local-cache";
import { QueryOptions, Expression } from "../types";
import { generateSalt, hashPassword } from "../auth";
import { del } from "../cache";
import { ObjectId } from "mongodb";
import * as db from "./db.ts";
import * as apiFunctions from "../api-functions.ts";
import { evaluate, and, extractParams } from "../common/expression/util.ts";
import { parse } from "../common/expression/parser.ts";
import * as logger from "../logger.ts";
import { getConfig } from "../ui/local-cache.ts";
import { Expression, Task } from "../types.ts";
import { generateSalt, hashPassword } from "../auth.ts";
import { del } from "../cache.ts";
import Authorizer from "../common/authorizer.ts";
import { ping } from "../ping.ts";
import { decodeTag } from "../util.ts";
import { stringify as yamlStringify } from "../common/yaml.ts";
import { ResourceLockedError } from "../common/errors.ts";
import { acquireLock, releaseLock } from "../lock.ts";
import { collections } from "../db/db.ts";
const router = new Router();
export default router;
@@ -50,7 +39,7 @@ const RESOURCE_IDS = {
permissions: "_id",
users: "_id",
faults: "_id",
tasks: "_id"
tasks: "_id",
};
const resources = {
@@ -63,78 +52,103 @@ 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);
if (!res[0]) return void (ctx.status = 404);
const { value: device } = await db.query("devices", filter).next();
if (!device) return void (ctx.status = 404);
ctx.type = "text/csv";
ctx.attachment(
`device-${ctx.params.id}-${new Date()
.toISOString()
.replace(/[:.]/g, "")}.csv`
.replace(/[:.]/g, "")}.csv`,
);
ctx.body = new stream.PassThrough();
ctx.body = new 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];
for (const k of Object.keys(device).sort()) {
const p = device[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(),
`"${String(value).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 +161,18 @@ for (const [resource, flags] of Object.entries(resources)) {
logger.accessInfo(log);
});
router.get(`/${resource}`, async (ctx, next) => {
const options: QueryOptions = {};
let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
router.get(`/${resource}`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const options: Parameters<typeof db.query>[2] = {};
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,44 +184,46 @@ 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"]],
]);
}
ctx.body = new stream.PassThrough();
ctx.body = new PassThrough();
ctx.type = "application/json";
let c = 0;
ctx.body.write("[\n");
await db.query(resource, filter, options, obj => {
for await (const obj of db.query(resource, filter, options))
ctx.body.write((c++ ? "," : "") + JSON.stringify(obj) + "\n");
});
ctx.body.end("]");
ctx.body.end("]");
logger.accessInfo(log);
});
// CSV download
router.get(`/${resource}.csv`, async (ctx, next) => {
const options: QueryOptions = { projection: {} };
let filter: Expression = true;
if (ctx.request.query.filter) filter = parse(ctx.request.query.filter);
router.get(`/${resource}.csv`, async (ctx) => {
const authorizer: Authorizer = ctx.state.authorizer;
const options: Parameters<typeof db.query>[2] = { projection: {} };
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 +231,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,38 +254,51 @@ 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"]],
]);
}
ctx.body = new stream.PassThrough();
ctx.body = new PassThrough();
ctx.type = "text/csv";
ctx.attachment(
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`
`${resource}-${new Date(now).toISOString().replace(/[:.]/g, "")}.csv`,
);
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 => {
for await (const obj of db.query(resource, filter, options)) {
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();
}
}
}
@@ -278,79 +311,102 @@ for (const [resource, flags] of Object.entries(resources)) {
return v;
});
ctx.body.write(arr.join(",") + "\n");
});
}
ctx.body.end();
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);
const count = await db.count(resource, filter);
if (!res.length) return void next();
if (!count) 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);
const { value: res } = await db.query(resource, filter).next();
if (!res.length) return void next();
if (!res) return void (ctx.status = 404);
logger.accessInfo(log);
ctx.body = res[0];
ctx.body = res;
});
// 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();
const { value: res } = await db.query(resource, filter).next();
if (!res) return void (ctx.status = 404);
const validate = authorizer.getValidator(resource, res[0]);
const validate = authorizer.getValidator(resource, res);
if (!validate("delete")) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
await apiFunctions.deleteResource(resource, ctx.params.id);
try {
await apiFunctions.deleteResource(resource, ctx.params.id);
} catch (err) {
if (err instanceof ResourceLockedError) {
log.message += " failed";
logger.accessWarn(log);
ctx.status = 503;
ctx.body = err.message;
return;
}
throw err;
}
logger.accessInfo(log);
@@ -359,19 +415,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 +454,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 +493,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 +515,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,69 +528,156 @@ 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 deviceId = ctx.params.id;
const authorizer: Authorizer = ctx.state.authorizer;
const log = {
message: "Commit tasks",
context: ctx,
deviceId: ctx.params.id,
tasks: null
deviceId: deviceId,
tasks: null,
};
const authorizer = ctx.state.authorizer;
const filter = ["=", ["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();
const validate = authorizer.getValidator("devices", devices[0]);
for (const t of ctx.request.body) {
if (!validate("task", t)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
const socketTimeout: number = ctx.socket.timeout;
// Extend socket timeout while waiting for session
if (socketTimeout) ctx.socket.setTimeout(300000);
const token = await acquireLock(`cwmp_session_${deviceId}`, 5000, 30000);
if (!token) {
log.message += " failed";
logger.accessWarn(log);
ctx.body = "Device is in session";
ctx.status = 503;
// Restore socket timeout
if (socketTimeout) ctx.socket.setTimeout(socketTimeout);
return;
}
let device;
let statuses: { _id: string; status: string }[];
try {
const filter = and(authorizer.getFilter("devices", 3), [
"=",
["PARAM", "DeviceID.ID"],
deviceId,
]);
device = (await db.query("devices", filter).next()).value;
if (!device) return void (ctx.status = 404);
const validate = authorizer.getValidator("devices", device);
for (const t of ctx.request.body) {
if (!validate("task", t)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
}
}
let tasks = ctx.request.body as Task[];
for (const task of tasks) {
delete task._id;
task["device"] = deviceId;
}
tasks = await apiFunctions.insertTasks(tasks);
statuses = tasks.map((t) => ({ _id: t._id, status: "pending" }));
} finally {
await releaseLock(`cwmp_session_${deviceId}`, token);
}
const onlineThreshold = getConfig(
ctx.state.configSnapshot,
"cwmp.deviceOnlineThreshold",
{},
Date.now(),
(exp) => {
if (!Array.isArray(exp)) return exp;
if (exp[0] === "PARAM") {
const p = device[exp[1]];
if (p?.value) return p.value[0];
} else if (exp[0] === "FUNC") {
if (exp[1] === "REMOTE_ADDRESS") {
for (const root of ["InternetGatewayDevice", "Device"]) {
const p = device[`${root}.ManagementServer.ConnectionRequestURL`];
if (p?.value) return new URL(p.value[0]).hostname;
}
return null;
}
}
return exp;
},
) as number;
const lastInform = device["Events.Inform"].value[0] as number;
let status = await apiFunctions.connectionRequest(deviceId, device);
if (!status) {
const sessionStarted = await apiFunctions.awaitSessionStart(
deviceId,
lastInform,
onlineThreshold,
);
if (!sessionStarted) {
status = "No contact from CPE";
} else {
const sessionEnded = await apiFunctions.awaitSessionEnd(deviceId, 120000);
if (!sessionEnded) status = "Session took too long to complete";
}
}
// TODO Pass context and timestamp
const onlineThreshold = getConfig(
ctx.state.configSnapshot,
"cwmp.deviceOnlineThreshold"
);
if (!status) {
const promises = statuses.map((t) =>
collections.faults.count({ _id: `${deviceId}:task_${t._id}` }),
);
const res = await apiFunctions.postTasks(
ctx.params.id,
ctx.request.body,
onlineThreshold
);
const res = await Promise.all(promises);
for (const [i, r] of statuses.entries())
r.status = res[i] ? "fault" : "done";
}
log.tasks = res.tasks.map(t => t._id).join(",");
await Promise.all(statuses.map((t) => db.deleteTask(new ObjectId(t._id))));
// Restore socket timeout
if (socketTimeout) ctx.socket.setTimeout(socketTimeout);
log.tasks = statuses.map((t) => t._id).join(",");
logger.accessInfo(log);
ctx.set("Connection-Request", res.connectionRequest);
ctx.body = res.tasks;
ctx.set("Connection-Request", status || "OK");
ctx.body = statuses;
});
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();
const { value: res } = await db.query("devices", filter).next();
if (!res) return void (ctx.status = 404);
const validate = authorizer.getValidator("devices", res[0]);
const validate = authorizer.getValidator("devices", res);
if (!validate("tags", ctx.request.body)) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
@@ -517,33 +697,37 @@ 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) => {
if (!ctx.state.user) return void (ctx.status = 401);
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
ctx.request.body.authPassword,
))
) {
logUnauthorizedWarning(log);
@@ -553,16 +737,20 @@ 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 res = await db.query("users", filter);
if (!res.length) return void next();
const newPassword = ctx.request.body.newPassword;
if (ctx.state.user) {
const validate = authorizer.getValidator("users", res[0]);
const filter = and(authorizer.getFilter("users", 3), [
"=",
["PARAM", RESOURCE_IDS.users],
username,
]);
const { value: res } = await db.query("users", filter).next();
if (!res) return void (ctx.status = 404);
const validate = authorizer.getValidator("users", res);
if (!validate("password", { password: newPassword })) {
logUnauthorizedWarning(log);
return void (ctx.status = 403);
@@ -573,7 +761,7 @@ router.put("/users/:id/password", async (ctx, next) => {
const password = await hashPassword(newPassword, salt);
await db.putUser(username, { password, salt });
await del("presets_hash");
await del("ui-local-cache-hash");
logger.accessInfo(log);
ctx.body = "";
+561 -382
View File
@@ -1,414 +1,593 @@
/**
* 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 { Script } from "node:vm";
import { Readable } from "node:stream";
import { Collection, ObjectId, WithoutId } from "mongodb";
import { encodeTag } from "../util.ts";
import { evaluate } from "../common/expression/util.ts";
import { Expression, Fault, Task } from "../types.ts";
import { collections, filesBucket } from "../db/db.ts";
import { convertOldPrecondition, optimizeProjection } from "../db/util.ts";
import * as MongoTypes from "../db/types.ts";
import { parse, parseList, stringify } from "../common/expression/parser.ts";
import { toMongoQuery } from "../db/synth.ts";
import { MongoClient, GridFSBucket } from "mongodb";
import { Script } from "vm";
import * as config from "../config";
import * as mongodbFunctions from "../mongodb-functions";
import * as expression from "../common/expression";
import { QueryOptions } from "../types";
function processDeviceProjection(
projection: Record<string, 1>,
): Record<string, 1> {
if (!projection) return projection;
const p = {};
for (const [k, v] of Object.entries(projection)) {
if (k === "DeviceID.ID") {
p["_id"] = 1;
} else if (k.startsWith("DeviceID")) {
p["_deviceId._SerialNumber"] = v;
p["_deviceId._OUI"] = v;
p["_deviceId._ProductClass"] = v;
p["_deviceId._Manufacturer"] = v;
} else if (k.startsWith("Tags")) {
p["_tags"] = v;
} else if (k.startsWith("Events")) {
p["_lastInform"] = v;
p["_registered"] = v;
p["_lastBoot"] = v;
p["_lastBootstrap"] = v;
} else {
p[k] = v;
}
}
const CACHE_TTL = 300000;
let _clientPromise = null;
const RESOURCE_COLLECTION = {
files: "fs.files"
};
function ensureIndexes(client): void {
client
.db()
.collection("cache")
.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
return p;
}
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);
function processDeviceSort(
sort: Record<string, number>,
): Record<string, number> {
if (!sort) return sort;
const s = {};
for (const [k, v] of Object.entries(sort)) {
if (k === "DeviceID.ID") s["_id"] = v;
else if (k.startsWith("DeviceID.")) s[`_deviceId._${k.slice(9)}`] = v;
else if (k === "Events.Inform") s["_lastInform"] = v;
else if (k === "Events.Registered") s["_registered"] = v;
else if (k === "Events.1_BOOT") s["_lastBoot"] = v;
else if (k === "Events.0_BOOTSTRAP") s["_lastBootstrap"] = v;
else s[`${k}._value`] = v;
}
return s;
}
function parseDate(d: Date): number | string {
const n = +d;
return isNaN(n) ? "" + d : n;
}
interface FlatAttributes {
object?: boolean;
objectTimestamp?: number;
writable?: boolean;
writableTimestamp?: number;
value?: [string | number | boolean, string];
valueTimestamp?: number;
notification?: number;
notificationTimestamp?: number;
accessList?: string[];
accessListTimestamp?: number;
}
export interface FlatDevice {
[param: string]: FlatAttributes;
}
export function flattenDevice(device: Record<string, unknown>): FlatDevice {
function recursive(
input,
root: string,
output: FlatDevice,
timestamp: number,
): void {
for (const [name, tree] of Object.entries(input)) {
if (!root) {
if (name === "_lastInform") {
output["Events.Inform"] = {
value: [parseDate(tree as Date), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_registered") {
output["Events.Registered"] = {
value: [parseDate(tree as Date), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_lastBoot") {
output["Events.1_BOOT"] = {
value: [parseDate(tree as Date), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_lastBootstrap") {
output["Events.0_BOOTSTRAP"] = {
value: [parseDate(tree as Date), "xsd:dateTime"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_id") {
output["DeviceID.ID"] = {
value: [tree as string, "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_deviceId") {
output["DeviceID.Manufacturer"] = {
value: [tree["_Manufacturer"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["DeviceID.OUI"] = {
value: [tree["_OUI"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["DeviceID.ProductClass"] = {
value: [tree["_ProductClass"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
output["DeviceID.SerialNumber"] = {
value: [tree["_SerialNumber"], "xsd:string"],
valueTimestamp: timestamp,
writable: false,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
} else if (name === "_tags") {
output["Tags"] = {
writable: false,
writableTimestamp: timestamp,
object: true,
objectTimestamp: timestamp,
};
for (const t of tree as string[]) {
output[`Tags.${encodeTag(t)}`] = {
value: [true, "xsd:boolean"],
valueTimestamp: timestamp,
writable: true,
writableTimestamp: timestamp,
object: false,
objectTimestamp: timestamp,
};
}
}
}
if (name.startsWith("_")) continue;
let childrenTimestamp = timestamp;
if (!root) childrenTimestamp = +(input["_timestamp"] || 1);
else if (+input["_timestamp"] > timestamp)
childrenTimestamp = +input["_timestamp"];
const attrs: FlatAttributes = {};
if (tree["_value"] != null) {
attrs.value = [
tree["_value"] instanceof Date ? +tree["_value"] : tree["_value"],
tree["_type"],
];
attrs.valueTimestamp = +(tree["_timestamp"] || childrenTimestamp);
attrs.object = false;
attrs.objectTimestamp = childrenTimestamp;
} else if (tree["_object"] != null) {
attrs.object = tree["_object"];
attrs.objectTimestamp = childrenTimestamp;
}
if (tree["_writable"] != null) {
attrs.writable = tree["_writable"];
attrs.writableTimestamp = childrenTimestamp;
}
if (tree["_notification"] != null) {
attrs.notification = tree["_notification"];
attrs.notificationTimestamp = +tree["_attributesTimestamp"] || 1;
}
if (tree["_accessList"] != null) {
attrs.accessList = tree["_accessList"];
attrs.accessListTimestamp = +tree["_attributesTimestamp"] || 1;
}
const r = root ? `${root}.${name}` : name;
output[r] = attrs;
if (attrs.object || tree["object"] == null)
recursive(tree, r, output, childrenTimestamp);
}
}
const newDevice: FlatDevice = {};
const timestamp = new Date((device["_lastInform"] as Date) || 1).getTime();
recursive(device, "", newDevice, timestamp);
return newDevice;
}
function flattenFault(fault: unknown): Fault {
const f = Object.assign({}, fault) as Fault;
if (f.timestamp) f.timestamp = +f.timestamp;
if (f["expiry"]) f["expiry"] = +f["expiry"];
return f as Fault;
}
function flattenTask(task: unknown): Task {
const t = Object.assign({}, task) as Task;
t._id = "" + t._id;
if (t["timestamp"]) t["timestamp"] = +t["timestamp"];
if (t.expiry) t.expiry = +t.expiry;
return t;
}
function flattenPreset(
preset: Record<string, unknown>,
): Record<string, unknown> {
const p = Object.assign({}, preset);
if (p.precondition) {
try {
// Try parse to check expression validity
parse(p.precondition as string);
} catch (error) {
p.precondition = convertOldPrecondition(
JSON.parse(p.precondition as string),
);
});
p.precondition = (p.precondition as string).length
? stringify(p.precondition as Expression)
: "";
}
}
return _clientPromise;
if (p.events) {
const e = [];
for (const [k, v] of Object.entries(p.events)) e.push(v ? k : `-${k}`);
p.events = e.join(", ");
}
const provision = p.configurations[0];
if (
(p.configurations as any[]).length === 1 &&
provision.type === "provision" &&
provision.name &&
provision.name.length
) {
p.provision = provision.name;
p.provisionArgs = provision.args
? provision.args.map((a) => stringify(a)).join(", ")
: "";
}
delete p.configurations;
return p;
}
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);
});
function flattenFile(file: Record<string, unknown>): Record<string, unknown> {
const f = {};
f["_id"] = file["_id"];
if (file.metadata) {
f["metadata.fileType"] = file["metadata"]["fileType"] || "";
f["metadata.oui"] = file["metadata"]["oui"] || "";
f["metadata.productClass"] = file["metadata"]["productClass"] || "";
f["metadata.version"] = file["metadata"]["version"] || "";
}
return f;
}
export function query(
resource,
filter,
function preProcessPreset(data: Record<string, unknown>): MongoTypes.Preset {
const preset = Object.assign({}, data);
if (!preset.precondition) preset.precondition = "";
// Try parse to check expression validity
parse(preset.precondition as string);
preset.weight = parseInt(preset.weight as string) || 0;
const events = {};
if (preset.events) {
for (let e of (preset.events as string).split(",")) {
let v = true;
e = e.trim();
if (e.startsWith("-")) {
v = false;
e = e.slice(1).trim();
}
if (e) events[e] = v;
}
}
preset.events = events;
if (!preset.provision) throw new Error("Invalid preset provision");
const configuration = {
type: "provision",
name: preset.provision,
args: null,
};
if (preset.provisionArgs)
configuration.args = parseList(preset.provisionArgs as string);
delete preset.provision;
delete preset.provisionArgs;
preset.configurations = [configuration];
return preset as unknown as MongoTypes.Preset;
}
interface QueryOptions {
projection?: any;
skip?: number;
limit?: number;
sort?: {
[param: string]: number;
};
}
export async function* query(
resource: string,
filter: Expression,
options?: QueryOptions,
callback?: (doc: any) => void
): Promise<any[]> {
): AsyncGenerator<any, void, undefined> {
options = options || {};
let q;
filter = expression.evaluate(filter, null, Date.now());
filter = evaluate(filter, null, Date.now());
const q = toMongoQuery(filter, resource);
if (!q) return;
if (Array.isArray(filter)) {
if (resource === "devices") {
filter = mongodbFunctions.processDeviceFilter(filter);
} else if (resource === "tasks") {
filter = mongodbFunctions.processTasksFilter(filter);
} else if (resource === "faults") {
filter = mongodbFunctions.processFaultsFilter(filter);
} else if (resource === "users") {
// Protect against brute force, and dictionary attacks
const params = expression.extractParams(filter);
if (params.includes("password") || params.includes("salt"))
return Promise.reject(new Error("Invalid users filter"));
}
q = mongodbFunctions.filterToMongoQuery(filter);
} else if (filter != null && !filter) {
return Promise.resolve([]);
}
return new Promise((resolve, reject) => {
getClient().then(client => {
const collection = client
.db()
.collection(RESOURCE_COLLECTION[resource] || resource);
const cursor = collection.find(q);
if (options.projection) {
let projection = options.projection;
if (resource === "devices") {
projection = mongodbFunctions.processDeviceProjection(
options.projection
);
}
if (resource === "presets") projection.configurations = 1;
cursor.project(projection);
}
if (resource === "users") cursor.project({ password: 0, salt: 0 });
if (options.skip) cursor.skip(options.skip);
if (options.limit) cursor.limit(options.limit);
if (options.sort) {
let s = Object.entries(options.sort)
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
.reduce(
(obj, [k, v]) =>
Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }),
{}
);
if (resource === "devices") s = mongodbFunctions.processDeviceSort(s);
cursor.sort(s);
}
if (!callback) {
cursor.toArray((err, docs) => {
if (err) return reject(err);
if (resource === "devices")
docs = docs.map(d => mongodbFunctions.flattenDevice(d));
else if (resource === "faults")
docs = docs.map(d => mongodbFunctions.flattenFault(d));
else if (resource === "tasks")
docs = docs.map(d => mongodbFunctions.flattenTask(d));
else if (resource === "presets")
docs = docs.map(d => mongodbFunctions.flattenPreset(d));
else if (resource === "files")
docs = docs.map(d => mongodbFunctions.flattenFile(d));
return resolve(docs);
});
} else {
cursor.forEach(
doc => {
if (resource === "devices")
doc = mongodbFunctions.flattenDevice(doc);
else if (resource === "faults")
doc = mongodbFunctions.flattenFault(doc);
else if (resource === "tasks")
doc = mongodbFunctions.flattenTask(doc);
else if (resource === "presets")
doc = mongodbFunctions.flattenPreset(doc);
else if (resource === "files")
doc = mongodbFunctions.flattenFile(doc);
callback(doc);
},
err => {
if (err) reject(err);
else resolve();
}
);
}
});
});
}
export function count(resource, filter): Promise<number> {
let q;
filter = expression.evaluate(filter, null, Date.now());
if (Array.isArray(filter)) {
const collection = collections[resource] as Collection<any>;
const cursor = collection.find(q);
if (options.projection) {
let projection = options.projection;
if (resource === "devices")
filter = mongodbFunctions.processDeviceFilter(filter);
else if (resource === "tasks")
filter = mongodbFunctions.processTasksFilter(filter);
else if (resource === "faults")
filter = mongodbFunctions.processFaultsFilter(filter);
q = mongodbFunctions.filterToMongoQuery(filter);
} else if (filter != null && !filter) {
return Promise.resolve(0);
projection = processDeviceProjection(options.projection);
if (resource === "presets") projection.configurations = 1;
projection = optimizeProjection(projection);
cursor.project(projection);
}
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);
});
});
});
if (resource === "users") cursor.project({ password: 0, salt: 0 });
if (options.skip) cursor.skip(options.skip);
if (options.limit) cursor.limit(options.limit);
if (options.sort) {
let s = Object.entries(options.sort)
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
.reduce(
(obj, [k, v]) =>
Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }),
{},
);
if (resource === "devices") s = processDeviceSort(s);
cursor.sort(s);
}
for await (let doc of cursor) {
if (resource === "devices") doc = flattenDevice(doc);
else if (resource === "faults") doc = flattenFault(doc);
else if (resource === "tasks") doc = flattenTask(doc);
else if (resource === "presets") doc = flattenPreset(doc);
else if (resource === "files") doc = flattenFile(doc);
yield doc;
}
}
export function updateDeviceTags(deviceId, tags): Promise<void> {
return new Promise((resolve, reject) => {
const add = [];
const pull = [];
export function count(resource: string, filter: Expression): Promise<number> {
const collection = collections[resource] as Collection<any>;
filter = evaluate(filter, null, Date.now());
const q = toMongoQuery(filter, resource);
if (!q) return Promise.resolve(0);
return collection.countDocuments(q);
}
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);
}
export async function updateDeviceTags(
deviceId: string,
tags: Record<string, boolean>,
): Promise<void> {
const add = [];
const pull = [];
for (let [tag, onOff] of Object.entries(tags)) {
tag = tag.trim();
if (onOff) add.push(tag);
else pull.push(tag);
}
const object = {};
if (add?.length) object["$addToSet"] = { _tags: { $each: add } };
if (pull?.length) object["$pullAll"] = { _tags: pull };
await collections.devices.updateOne({ _id: deviceId }, object);
}
export async function putPreset(
id: string,
object: Record<string, unknown>,
): Promise<void> {
const p = preProcessPreset(object);
await collections.presets.replaceOne({ _id: id }, p, { upsert: true });
}
export async function deletePreset(id: string): Promise<void> {
await collections.presets.deleteOne({ _id: id });
}
export async function putProvision(
id: string,
object: { script: string },
): Promise<void> {
if (!object.script) object.script = "";
try {
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
filename: id,
lineOffset: -1,
});
} catch (err) {
if (err.stack?.startsWith(`${id}:`)) {
return Promise.reject(
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`),
);
}
getClient()
.then(client => {
const collection = client.db().collection("devices");
const object = {};
if (add && add.length) object["$addToSet"] = { _tags: { $each: add } };
if (pull && pull.length) object["$pullAll"] = { _tags: pull };
collection.updateOne({ _id: deviceId }, object, err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
return Promise.reject(err);
}
await collections.provisions.replaceOne({ _id: id }, object, {
upsert: true,
});
}
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);
});
export async function deleteProvision(id: string): Promise<void> {
await collections.provisions.deleteOne({ _id: id });
}
function deleteResource(resource, id): Promise<void> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const collection = client
.db()
.collection(RESOURCE_COLLECTION[resource] || resource);
collection.deleteOne({ _id: id }, err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
}
export function putPreset(id, object): Promise<void> {
object = mongodbFunctions.preProcessPreset(object);
return putResource("presets", id, object);
}
export function deletePreset(id): Promise<void> {
return deleteResource("presets", id);
}
export function putProvision(id, object): Promise<void> {
export async function putVirtualParameter(
id: string,
object: { script: string },
): Promise<void> {
if (!object.script) object.script = "";
try {
new Script(`"use strict";(function(){\n${object.script}\n})();`);
} catch (error) {
return Promise.reject(error);
}
return putResource("provisions", id, object);
}
export function deleteProvision(id): Promise<void> {
return deleteResource("provisions", id);
}
export function putVirtualParameter(id, object): Promise<void> {
if (!object.script) object.script = "";
try {
new Script(`"use strict";(function(){\n${object.script}\n})();`);
} catch (error) {
return Promise.reject(error);
}
return putResource("virtualParameters", id, object);
}
export function deleteVirtualParameter(id): Promise<void> {
return deleteResource("virtualParameters", id);
}
export function putConfig(id, object): Promise<void> {
return putResource("config", id, object);
}
export function deleteConfig(id): Promise<void> {
return deleteResource("config", id);
}
export function putPermission(id, object): Promise<void> {
return putResource("permissions", id, object);
}
export function deletePermission(id): Promise<void> {
return deleteResource("permissions", id);
}
export function putUser(id, object): Promise<void> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const collection = client.db().collection("users");
// update instead of replace to keep the password if not set by user
collection.updateOne(
{ _id: id },
{ $set: object },
{ upsert: true },
err => {
if (err) return void reject(err);
resolve();
}
);
})
.catch(reject);
});
}
export function deleteUser(id): Promise<void> {
return deleteResource("users", id);
}
export function putFile(filename, metadata, contentStream): Promise<void> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const bucket = new GridFSBucket(client.db());
const uploadStream = bucket.openUploadStreamWithId(filename, filename, {
metadata: metadata
});
uploadStream.on("error", reject);
contentStream.on("error", reject);
uploadStream.on("finish", resolve);
contentStream.pipe(uploadStream);
})
.catch(reject);
});
}
export function deleteFile(filename): Promise<void> {
return new Promise((resolve, reject) => {
getClient()
.then(client => {
const bucket = new GridFSBucket(client.db());
bucket.delete(filename, err => {
if (err) return void reject(err);
resolve();
});
})
.catch(reject);
});
}
export function deleteFault(id): Promise<void> {
return deleteResource("faults", id);
}
export function deleteTask(id): Promise<void> {
return deleteResource("tasks", id);
}
export function disconnect(): void {
if (_clientPromise) {
_clientPromise.then(client => {
client.close();
new Script(`"use strict";(function(){\n${object.script}\n})();`, {
filename: id,
lineOffset: -1,
});
} catch (err) {
if (err.stack?.startsWith(`${id}:`)) {
return Promise.reject(
new Error(`${err.name} at ${err.stack.split("\n", 1)[0]}`),
);
}
return Promise.reject(err);
}
await collections.virtualParameters.replaceOne({ _id: id }, object, {
upsert: true,
});
}
export async function deleteVirtualParameter(id: string): Promise<void> {
await collections.virtualParameters.deleteOne({ _id: id });
}
export async function putConfig(
id: string,
object: WithoutId<MongoTypes.Config>,
): Promise<void> {
await collections.config.replaceOne({ _id: id }, object, { upsert: true });
}
export async function deleteConfig(id: string): Promise<void> {
await collections.config.deleteOne({ _id: id });
}
export async function putPermission(
id: string,
object: WithoutId<MongoTypes.Permission>,
): Promise<void> {
await collections.permissions.replaceOne({ _id: id }, object, {
upsert: true,
});
}
export async function deletePermission(id: string): Promise<void> {
await collections.permissions.deleteOne({ _id: id });
}
export async function putUser(
id: string,
object: Partial<WithoutId<MongoTypes.User>>,
): Promise<void> {
// update instead of replace to keep the password if not set by user
await collections.users.updateOne(
{ _id: id },
{ $set: object },
{ upsert: true },
);
}
export async function deleteUser(id: string): Promise<void> {
await collections.users.deleteOne({ _id: id });
}
export function downloadFile(filename: string): Readable {
return filesBucket.openDownloadStreamByName(filename);
}
export function putFile(
filename: string,
metadata: Record<string, string>,
contentStream: Readable,
): Promise<void> {
return new Promise((resolve, reject) => {
const uploadStream = filesBucket.openUploadStreamWithId(
filename as unknown as ObjectId,
filename,
{
metadata: metadata,
},
);
let readableEnded = false;
contentStream.on("end", () => {
readableEnded = true;
});
contentStream.on("close", () => {
// In Node versions prior to 15, the stream will not emit an error if the
// connection is closed before the stream is finished.
// For Node 12.9+ we can just use stream.readableEnded
if (!readableEnded)
uploadStream.destroy(new Error("Stream closed prematurely"));
});
contentStream.on("error", (err) => {
uploadStream.destroy(err);
});
uploadStream.on("error", reject);
uploadStream.on("finish", resolve);
contentStream.pipe(uploadStream);
});
}
export async function deleteFile(filename: string): Promise<void> {
await filesBucket.delete(filename as any);
}
export async function deleteFault(id: string): Promise<void> {
await collections.faults.deleteOne({ _id: id });
}
export async function deleteTask(id: ObjectId): Promise<void> {
await collections.tasks.deleteOne({ _id: id });
}
+187
View File
@@ -0,0 +1,187 @@
import * as crypto from "node:crypto";
import * as config from "../config.ts";
import { collections } from "../db/db.ts";
import * as expression from "../common/expression/util.ts";
import { parse } from "../common/expression/parser.ts";
import { Expression, Users, Permissions, Config, UiConfig } from "../types.ts";
import { LocalCache } from "../local-cache.ts";
interface Snapshot {
permissions: Permissions;
users: Users;
config: Config;
ui: UiConfig;
}
async function fetchPermissions(): Promise<[string, Permissions]> {
const perms = await collections.permissions.find().toArray();
perms.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto
.createHash("md5")
.update(JSON.stringify(perms))
.digest("hex");
const permissions: Permissions = {};
for (const p of perms) {
if (!permissions[p.role]) permissions[p.role] = {};
if (!permissions[p.role][p.access]) permissions[p.role][p.access] = {};
permissions[p.role][p.access][p.resource] = {
access: p.access,
filter: parse(p.filter || "true"),
};
if (p.validate)
permissions[p.role][p.access][p.resource].validate = parse(p.validate);
}
return [h, permissions];
}
async function fetchUsers(): Promise<[string, Users]> {
const _users = await collections.users.find().toArray();
_users.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto
.createHash("md5")
.update(JSON.stringify(_users))
.digest("hex");
const users = {};
for (const user of _users) {
users[user._id] = {
password: user.password,
salt: user.salt,
roles: user.roles.split(",").map((s) => s.trim()),
};
}
return [h, users];
}
async function fetchConfig(): Promise<[string, Config, UiConfig]> {
const conf = await collections.config.find().toArray();
conf.sort((a, b) => (a._id > b._id ? 1 : -1));
const h = crypto.createHash("md5").update(JSON.stringify(conf)).digest("hex");
const ui = {
filters: {},
device: {},
index: {},
overview: {},
pageSize: null,
};
const _config = {};
for (const c of conf) {
// Evaluate expressions to simplify them
const val = expression.evaluate(parse(c.value));
_config[c._id] = val;
if (c._id.startsWith("ui.")) {
const keys = c._id.split(".");
if (!(keys[1] in ui)) continue;
// remove the first key(ui)
keys.shift();
let ref = ui;
while (keys.length > 1) {
const k = keys.shift();
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
ref = ref[k];
}
ref[keys[0]] = val;
}
}
return [h, _config, ui];
}
const localCache = new LocalCache<Snapshot>("ui-local-cache-hash", refresh);
async function refresh(): Promise<[string, Snapshot]> {
const res = await Promise.all([
fetchPermissions(),
fetchUsers(),
fetchConfig(),
]);
const h = crypto.createHash("md5");
for (const r of res) h.update(r[0]);
const snapshot = {
permissions: res[0][1],
users: res[1][1],
config: res[2][1],
ui: res[2][2],
};
return [h.digest("hex"), snapshot];
}
export async function getRevision(): Promise<string> {
return await localCache.getRevision();
}
export function getConfig(
revision: string,
key: string,
context: Record<string, unknown>,
now: number,
cb?: (e: Expression) => Expression,
): string | number | boolean | null {
const snapshot = localCache.get(revision);
if (!snapshot) throw new Error("Cache snapshot does not exist");
const oldOpts = {
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
"cwmp.debug": "DEBUG",
"cwmp.retryDelay": "RETRY_DELAY",
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
"cwmp.cookiesPath": "COOKIES_PATH",
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
"cwmp.connectionRequestAllowBasicAuth":
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
};
if (!(key in snapshot.config)) {
if (key in oldOpts) {
let id;
if (context?.["id"]) {
id = context["id"];
} else if (cb) {
id = cb(["PARAM", "DeviceID.ID"]);
if (Array.isArray(id)) id = null;
}
return config.get(oldOpts[key], id);
}
return null;
}
const v = expression.evaluate(snapshot.config[key], context, now, cb);
return Array.isArray(v) ? null : v;
}
export function getConfigExpression(revision: string, key: string): Expression {
const snapshot = localCache.get(revision);
return snapshot.config[key];
}
export function getUsers(revision: string): Users {
const snapshot = localCache.get(revision);
return snapshot.users;
}
export function getPermissions(revision: string): Permissions {
const snapshot = localCache.get(revision);
return snapshot.permissions;
}
export function getUiConfig(revision: string): UiConfig {
const snapshot = localCache.get(revision);
return snapshot.ui;
}
+70
View File
@@ -0,0 +1,70 @@
import { EventEmitter } from "node:events";
export function generateDeviceId(
deviceIdStruct: Record<string, string>,
): string {
// Escapes everything except alphanumerics and underscore
function esc(str): string {
return str.replace(/[^A-Za-z0-9_]/g, (chr) => {
const buf = Buffer.from(chr, "utf8");
let rep = "";
for (const b of buf) rep += "%" + b.toString(16).toUpperCase();
return rep;
});
}
// Guaranteeing globally unique id as defined in TR-069
if (deviceIdStruct["ProductClass"]) {
return (
esc(deviceIdStruct["OUI"]) +
"-" +
esc(deviceIdStruct["ProductClass"]) +
"-" +
esc(deviceIdStruct["SerialNumber"])
);
}
return esc(deviceIdStruct["OUI"]) + "-" + esc(deviceIdStruct["SerialNumber"]);
}
// Source: http://stackoverflow.com/a/6969486
export function escapeRegExp(str: string): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
export function encodeTag(tag: string): string {
return encodeURIComponent(tag)
.replace(
/[!~*'().]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
)
.replace(/0x(?=[0-9A-Z]{2})/g, "0%78")
.replace(/%/g, "0x");
}
export function decodeTag(tag: string): string {
return decodeURIComponent(tag.replace(/0x(?=[0-9A-Z]{2})/g, "%"));
}
export function once(
emitter: EventEmitter,
event: string,
timeout: number,
): Promise<unknown[]> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Event ${event} timed out after ${timeout} ms`));
}, timeout);
emitter.once(event, (...args: unknown[]) => {
clearTimeout(timer);
resolve(args);
});
});
}
export function setTimeoutPromise(delay: number, ref = true): Promise<void> {
return new Promise((resolve) => {
const timerId = setTimeout(resolve, delay);
if (!ref) timerId.unref();
});
}
+8 -26
View File
@@ -1,22 +1,3 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
const NONEXISTENT = Symbol();
const UNDEFINED = undefined;
@@ -26,10 +7,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 +27,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];
@@ -138,7 +119,7 @@ export default class VersionedMap<K, V> {
const del = revisionsObj.delete || 0;
const mutations = Object.keys(revisionsObj).reduce(
(acc, cur) => (cur === "delete" ? acc : acc | (1 << +cur)),
del
del,
);
const revisions = [];
@@ -168,6 +149,7 @@ export default class VersionedMap<K, V> {
public *diff(): IterableIterator<[K, V, V]> {
for (const [key, revisions] of this.map) {
if (revisions.length === 1) continue;
let first = revisions[0];
let last = revisions[revisions.length - 1];
if (first === NONEXISTENT && last === NONEXISTENT) continue;
@@ -193,7 +175,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 -30
View File
@@ -1,22 +1,3 @@
/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
*/
const CHAR_SINGLE_QUOTE = 39;
const CHAR_DOUBLE_QUOTE = 34;
const CHAR_LESS_THAN = 60;
@@ -53,12 +34,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 +72,7 @@ export function parseAttrs(string: string): Attribute[] {
name: name,
namespace: namespace,
localName: localName,
value: value
value: value,
};
attrs.push(e);
name = "";
@@ -103,10 +85,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 +112,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 "&quot;":
return '"';
@@ -161,15 +145,15 @@ export function decodeEntities(string): string {
});
}
export function encodeEntities(string): string {
export function encodeEntities(string: string): string {
const entities = {
"&": "&amp;",
'"': "&quot;",
"'": "&apos;",
"<": "&lt;",
">": "&gt;"
">": "&gt;",
};
return string.replace(/[&"'<>]/g, m => entities[m]);
return string.replace(/[&"'<>]/g, (m) => entities[m]);
}
export function parseXml(string: string): Element {
@@ -186,7 +170,7 @@ export function parseXml(string: string): Element {
attrs: "",
text: "",
bodyIndex: 0,
children: []
children: [],
};
const stack: Element[] = [root];
@@ -330,7 +314,7 @@ export function parseXml(string: string): Element {
: "",
text: "",
bodyIndex: i + 1,
children: []
children: [],
};
parent.children.push(e);
if (!selfClosing) stack.push(e);
+466
View File
@@ -0,0 +1,466 @@
import * as net from "node:net";
import * as tls from "node:tls";
import { EventEmitter } from "node:events";
import { createHash, createHmac, randomBytes } from "node:crypto";
import { parseXml, Element, parseAttrs } from "./xml-parser.ts";
function encodeBase64(str: string): string {
return Buffer.from(str).toString("base64");
}
function decodeBase64(str: string): string {
return Buffer.from(str, "base64").toString();
}
function detectStreamTag(data: string): number {
const i1 = data.indexOf("<stream:stream");
if (i1 < 0) throw new Error("Cannot detect opening stream tag");
const i2 = data.indexOf(">", i1);
if (i2 < 0) throw new Error("Cannot detect opening stream tag");
return i2 + 1;
}
function xmppStream<T>(
socket: net.Socket,
callback: Generator<void, T, Element>,
): Promise<T> {
return new Promise((resolve, reject) => {
const onError = (err: Error): void => {
socket.removeListener("error", onError);
reject(err);
};
socket.on("error", onError);
const onData = (chunk: Buffer): void => {
try {
const str = chunk.toString("utf8");
const xml = parseXml(str);
const { value, done } = callback.next(xml.children[0]);
if (done) {
socket.removeListener("error", onError);
socket.removeListener("data", onData);
resolve(value as T);
}
} catch (err) {
socket.removeListener("error", onError);
socket.removeListener("data", onData);
reject(err);
}
};
socket.once("data", (chunk: Buffer) => {
try {
const str = chunk.toString("utf8");
const i = detectStreamTag(str);
const streamTagStr = str.slice(0, i);
const xml = parseXml(streamTagStr + "</stream:stream>");
callback.next(xml.children[0]);
chunk = chunk.slice(Buffer.byteLength(streamTagStr));
if (chunk.length) onData(chunk);
socket.on("data", onData);
} catch (err) {
socket.removeListener("error", onError);
socket.removeListener("data", onData);
reject(err);
}
});
try {
callback.next();
} catch (err) {
socket.removeListener("error", onError);
socket.removeListener("data", onData);
reject(err);
}
});
}
const INT_1 = Buffer.from([0, 0, 0, 1]);
const saltedPasswordCache = {
password: "",
iterationCount: 0,
saltBase64: "",
salted: Buffer.allocUnsafe(0),
};
function saltPassword(
password: string,
saltBase64: string,
iteractionCount: number,
): Buffer {
if (
password === saltedPasswordCache.password &&
saltBase64 === saltedPasswordCache.saltBase64 &&
iteractionCount === saltedPasswordCache.iterationCount
)
return saltedPasswordCache.salted;
const hi = createHmac("sha1", password)
.update(Buffer.concat([Buffer.from(saltBase64, "base64"), INT_1]))
.digest();
let hi2: Buffer = hi;
for (let i = 1; i < iteractionCount; ++i) {
hi2 = createHmac("sha1", password).update(hi2).digest();
for (const [j, b] of hi2.entries()) hi[j] ^= b;
}
saltedPasswordCache.saltBase64 = saltBase64;
saltedPasswordCache.password = password;
saltedPasswordCache.iterationCount = iteractionCount;
saltedPasswordCache.salted = hi;
return hi;
}
function* loginPlain(
socket: net.Socket,
username: string,
password: string,
): Generator<void, void, Element> {
socket.write(
`<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="PLAIN">${encodeBase64(
`\x00${username}\x00${password}`,
)}</auth>`,
);
const res1 = yield;
if (
res1.name === "failure" &&
res1.children.some((c) => c.name === "not-authorized")
)
throw new Error("Not authorized");
if (res1.name !== "success")
throw new Error(`Unexpected response ${res1.name}`);
}
function* loginScram(
socket: net.Socket,
username: string,
password: string,
): Generator<void, void, Element> {
const cnonce = randomBytes(8).toString("base64");
const gs2Header = "n,,";
const clientFirstMessageBare = `n=${username},r=${cnonce}`;
const clientFirstMessage = gs2Header + clientFirstMessageBare;
socket.write(
`<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="SCRAM-SHA-1">${encodeBase64(
clientFirstMessage,
)}</auth>`,
);
const res1 = yield;
if (res1.name !== "challenge")
throw new Error(`Unexpected element ${res1.name}`);
const serverFirstMessage = decodeBase64(res1.text);
let iterationCount: number;
let saltBase64: string;
let nonce: string;
for (const s of serverFirstMessage.split(",")) {
if (s.startsWith("i=")) iterationCount = parseInt(s.slice(2));
else if (s.startsWith("s=")) saltBase64 = s.slice(2);
else if (s.startsWith("r=")) nonce = s.slice(2);
}
if (iterationCount == null || isNaN(iterationCount))
throw new Error("Invalid iteration count");
if (saltBase64 == null) throw new Error("Missing salt");
if (nonce == null) throw new Error("Missing nonce");
const saltedPassword = saltPassword(password, saltBase64, iterationCount);
const clientKey = createHmac("sha1", saltedPassword)
.update("Client Key")
.digest();
const storedKey = createHash("sha1").update(clientKey).digest();
const clientFinalMessageWithoutProof = `c=${encodeBase64(
gs2Header,
)},r=${nonce}`;
const authMessage = `${clientFirstMessageBare},${serverFirstMessage},${clientFinalMessageWithoutProof}`;
const clientSignature = createHmac("sha1", storedKey)
.update(authMessage)
.digest();
const clientProof = Buffer.from(clientKey);
for (const [i, b] of clientSignature.entries()) clientProof[i] ^= b;
const clientFinalMessage = `${clientFinalMessageWithoutProof},p=${clientProof.toString(
"base64",
)}`;
socket.write(
`<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">${encodeBase64(
clientFinalMessage,
)}</response>`,
);
const res2 = yield;
if (
res2.name === "failure" &&
res2.children.some((c) => c.name === "not-authorized")
)
throw new Error("Not authorized");
if (res2.name !== "success")
throw new Error(`Unexpected response ${res2.name}`);
const serverKey = createHmac("sha1", saltedPassword)
.update("Server Key")
.digest();
const serverSignature = createHmac("sha1", serverKey)
.update(authMessage)
.digest("base64");
if (!decodeBase64(res2.text).endsWith(serverSignature))
throw new Error("Invalid server signature");
}
function* starttls(socket: net.Socket): Generator<void, void, Element> {
socket.write("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");
const res1 = yield;
if (res1.name !== "proceed") throw new Error("Failed to initiate STARTTLS");
}
function* bind(
socket: net.Socket,
resource: string,
): Generator<void, void, Element> {
const id = randomBytes(8).toString("base64");
socket.write(
`<iq id='${id}' type='set'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>${resource}</resource></bind></iq>`,
);
const res1 = yield;
if (res1.name !== "iq") throw new Error(`Unexpected element ${res1.name}`);
const attrs1 = parseAttrs(res1.attrs);
const idAttr = attrs1.find((a) => a.name === "id");
if (!idAttr || idAttr.value !== id) throw new Error("Invalid ID");
const typeAttr = attrs1.find((a) => a.name === "type");
if (!typeAttr) throw new Error("Missing type attribute");
if (typeAttr.value !== "result") throw new Error("Cannot bind to resource");
}
const STATUS_RESTART_STREAM = 1;
const STATUS_STARTTLS = 2;
function* init(
socket: net.Socket,
host: string,
username: string,
password: string,
resource: string,
): Generator<void, number, Element> {
socket.write(
`<?xml version='1.0'?><stream:stream from='${username}@${host}' to='${host}' version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams'>`,
);
const open = yield;
if (open.name !== "stream:stream")
throw new Error(`Unexpected element ${open.name}`);
const features = yield;
if (features.name !== "stream:features")
throw new Error(`Unexpected element ${features.name}`);
for (const feature of features.children) {
if (feature.name === "starttls") {
if (feature.children.some((c) => c.name === "required")) {
yield* starttls(socket);
return STATUS_STARTTLS;
}
} else if (feature.name === "mechanisms") {
const mechanisms: Set<string> = new Set(
feature.children.map((c) => c.text),
);
if (mechanisms.has("PLAIN")) {
yield* loginPlain(socket, username, password);
return STATUS_RESTART_STREAM;
} else if (mechanisms.has("SCRAM-SHA-1")) {
yield* loginScram(socket, username, password);
return STATUS_RESTART_STREAM;
} else {
throw new Error("No supported SASL method");
}
} else if (feature.name === "bind") {
yield* bind(socket, resource);
return 0;
}
}
return 0;
}
function upgradeTls(socket: net.Socket, host: string): Promise<tls.TLSSocket> {
return new Promise((resolve, reject) => {
socket.on("error", reject);
const newSocket = tls.connect({ socket, host }, () => {
socket.removeListener("error", reject);
resolve(newSocket);
});
});
}
interface XmppClientOptions {
host: string;
port?: number;
username?: string;
password?: string;
resource?: string;
timeout?: number;
}
export default class XmppClient extends EventEmitter {
private _socket: net.Socket;
private _host: string;
private _username: string;
private _resource: string;
private _iqStanzaCallbacks: Map<
string,
(err: Error, r?: { rawRes: string; res: Element }) => void
>;
private constructor() {
super();
this._socket = null;
this._host = null;
this._username = null;
this._resource = null;
this._iqStanzaCallbacks = new Map();
}
static connect(opts: XmppClientOptions): Promise<XmppClient> {
return new Promise((resolve, reject) => {
let socket = new net.Socket();
socket.on("error", reject);
socket.connect(
opts.port || 5222,
opts.host,
void async function () {
socket.removeListener("error", reject);
try {
let status = 1;
while (status) {
if (status === STATUS_STARTTLS)
socket = await upgradeTls(socket, opts.host);
status = await xmppStream(
socket,
init(
socket,
opts.host,
opts.username,
opts.password,
opts.resource,
),
);
}
const client = new XmppClient();
client._socket = socket;
client._host = opts.host;
client._username = opts.username;
client._resource = opts.resource;
socket.on("data", client._onData.bind(client));
socket.on("error", client._onError.bind(client));
if (opts.timeout)
socket.setTimeout(opts.timeout, client.close.bind(client));
resolve(client);
} catch (err) {
socket.destroy();
reject(err);
}
},
);
});
}
close(): void {
this._socket.end("</stream:stream>");
this._socket.removeAllListeners("data");
this._socket.removeAllListeners("error");
this.emit("close");
}
ref(): void {
this._socket.ref();
}
unref(): void {
this._socket.unref();
}
get host(): string {
return this._host;
}
get username(): string {
return this._username;
}
get resource(): string {
return this._resource;
}
private _onData(chunk: Buffer): void {
try {
let close = false;
let str = chunk.toString("utf8");
if (str.endsWith("</stream:stream>")) {
str = str.slice(0, -16);
close = true;
}
const xml = parseXml(str);
const idx = xml.children.map((c) => c.bodyIndex);
for (const [i, c] of xml.children.entries()) {
const s = str.slice(idx[i], idx[i + 1]);
if (c.name === "iq") {
const attrs = parseAttrs(c.attrs);
const id = attrs.find((a) => a.name === "id");
if (id) {
const cb = this._iqStanzaCallbacks.get(id.value);
if (cb) cb(null, { rawRes: s, res: c });
}
}
this.emit("stanza", c, s);
}
if (close) {
this._socket.removeAllListeners("data");
this._socket.removeAllListeners("error");
this.emit("close");
}
} catch (err) {
this._socket.removeAllListeners("data");
this._socket.removeAllListeners("error");
this.emit("error", err);
}
}
private _onError(err: Error): void {
this._socket.end();
this.emit("error", err);
for (const cb of this._iqStanzaCallbacks.values()) cb(err);
}
send(msg: string): void {
this._socket.write(msg);
}
sendIqStanza(
from: string,
to: string,
type: string,
body: string,
timeout = 3000,
): Promise<{ rawReq: string; rawRes: string; res: Element }> {
return new Promise((resolve, reject) => {
const id = randomBytes(8).toString("base64");
const rawReq = `<iq from="${from}" to="${to}" id="${id}" type="${type}">${body}</iq>`;
this.send(rawReq);
const t = setTimeout(() => {
this._iqStanzaCallbacks.delete(id);
reject(
new Error("Did not receive IQ stanza response in a timely manner"),
);
}, timeout);
this._iqStanzaCallbacks.set(id, (err, r) => {
this._iqStanzaCallbacks.delete(id);
clearTimeout(t);
if (err) reject(err);
else resolve(Object.assign(r, { rawReq }));
});
});
}
}
+7752 -9412
View File
File diff suppressed because it is too large. Load diff
+51 -62
View File
@@ -1,6 +1,6 @@
{
"name": "genieacs",
"version": "1.2.0-beta.0",
"version": "1.2.10",
"description": "A TR-069 Auto Configuration Server (ACS)",
"repository": {
"type": "git",
@@ -12,76 +12,65 @@
"CWMP",
"ACS"
],
"author": "Zaid Abdulla <zaid@genieacs.com>",
"author": {
"name": "GenieACS Inc.",
"url": "https://genieacs.com"
},
"license": "AGPL-3.0",
"preferGlobal": true,
"directories": {
"bin": "bin",
"tools": "tools"
"private": true,
"bin": {
"genieacs-cwmp": "bin/genieacs-cwmp",
"genieacs-fs": "bin/genieacs-fs",
"genieacs-nbi": "bin/genieacs-nbi",
"genieacs-ui": "bin/genieacs-ui"
},
"dependencies": {
"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",
"@breejs/later": "^4.2.0",
"bson": "^4.7.2",
"espresso-iisojs": "^1.0.8",
"iconv-lite": "^0.6.3",
"ipaddr.js": "^2.0.1",
"jsonwebtoken": "^9.0.2",
"koa": "^2.15.1",
"koa-bodyparser": "^4.3.0",
"koa-compress": "^5.1.0",
"koa-jwt": "^4.0.3",
"koa-router": "^12.0.0",
"koa-static": "^5.0.0",
"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.16.0",
"parsimmon": "^1.18.1",
"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",
"esm": "^3.2.25",
"mithril": "^2.0.3",
"postcss": "^7.0.17",
"postcss-import": "^12.0.1",
"postcss-preset-env": "^6.7.0",
"prettier": "^1.18.2",
"rollup": "^1.17.0",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.1.1",
"rollup-plugin-typescript": "^1.0.1",
"source-map-loader": "^0.2.4",
"svgo": "^1.3.0",
"ts-node": "^8.3.0",
"typescript": "^3.5.3",
"webpack": "^4.38.0"
"@types/codemirror": "^5.60.15",
"@types/jsonwebtoken": "^9.0.6",
"@types/koa": "^2.15.0",
"@types/koa-compress": "^4.0.6",
"@types/koa-router": "^7.4.8",
"@types/koa-static": "^4.0.4",
"@types/mithril": "^2.2.6",
"@types/node": "^20.11.29",
"@types/parsimmon": "^1.10.9",
"@types/seedrandom": "^3.0.8",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"codemirror": "^5.65.16",
"esbuild": "^0.20.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"mithril": "^2.2.2",
"prettier": "^3.2.5",
"sql.js": "^1.10.2",
"svgo": "^3.2.0",
"typescript": "^5.4.2",
"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-tsc": "tsc --noEmit",
"lint": "npm run lint-ln && npm run lint-eslint && npm run lint-prettier && npm run lint-tsc",
"build": "node -r esm -r ts-node/register/transpile-only build/build"
"test": "esbuild --log-level=warning --bundle --platform=node --target=node18 --packages=external --sourcemap=inline --outdir=test test/*.ts && node --test --enable-source-maps test/*.js && rm test/*.js",
"lint": "esbuild build/lint.ts --bundle --platform=node --target=node16 --packages=external | node",
"build": "esbuild build/build.ts --bundle --platform=node --target=node12 --packages=external | node"
}
}
+10 -11
View File
@@ -1,8 +1,9 @@
import ava from "ava";
import { randomBytes } from "crypto";
import * as auth from "../lib/auth";
import test from "node:test";
import assert from "node:assert";
import { randomBytes } from "node:crypto";
import * as auth from "../lib/auth.ts";
ava("digest", t => {
void test("digest", () => {
const username = "test";
const password = "test";
const uri = "/";
@@ -14,11 +15,9 @@ 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);
for (const challenge of challenges) {
const wwwAuthHeader = auth.parseWwwAuthenticateHeader(challenge);
const solution = auth.solveDigest(
@@ -27,10 +26,10 @@ ava("digest", t => {
uri,
method,
body,
wwwAuthHeader
wwwAuthHeader,
);
const authHeader = auth.parseAuthorizationHeader(solution);
t.is(
assert.strictEqual(
authHeader["response"],
auth.digest(
username,
@@ -42,8 +41,8 @@ ava("digest", t => {
authHeader["qop"],
body,
authHeader["cnonce"],
authHeader["nc"]
)
authHeader["nc"],
),
);
}
});
-68
View File
@@ -1,68 +0,0 @@
import ava from "ava";
import * as common from "../lib/common";
ava("typeOf", t => {
const cases = [
[undefined, common.UNDEFINED_TYPE],
[null, common.NULL_TYPE],
[true, common.BOOLEAN_TYPE],
[false, common.BOOLEAN_TYPE],
[0, common.NUMBER_TYPE],
[1, common.NUMBER_TYPE],
[-1, common.NUMBER_TYPE],
[0.0, common.NUMBER_TYPE],
[1.0, common.NUMBER_TYPE],
[1.1, common.NUMBER_TYPE],
[-1.0, common.NUMBER_TYPE],
[-1.1, common.NUMBER_TYPE],
["", common.STRING_TYPE],
["TEST", common.STRING_TYPE],
[[], common.ARRAY_TYPE],
[[1, 2, 3], common.ARRAY_TYPE],
[{}, common.OBJECT_TYPE],
[{ test: 1 }, common.OBJECT_TYPE],
[/[^A-Za-z0-9_]/g, common.REGEXP_TYPE],
[new RegExp("//"), common.REGEXP_TYPE],
[new Date(), common.DATE_TYPE]
];
t.plan(cases.length);
for (const c of cases) t.is(common.typeOf(c[0]), c[1]);
});
ava("generateDeviceId", t => {
const space = [" ", "%20"];
const special = [";", "%3B"];
const cases = [
[
{
ProductClass: "TestProductClass",
OUI: "TestOUI",
SerialNumber: "TestSerialNumber"
},
"TestOUI-TestProductClass-TestSerialNumber"
],
[
{
OUI: "TestOUI",
SerialNumber: "TestSerialNumber"
},
"TestOUI-TestSerialNumber"
],
[
{
OUI: `TestOUIWith${space[0]}_${special[0]}2912`,
SerialNumber: `TestSerialNumberWith${space[0]}_${special[0]}2912`
},
`TestOUIWith${space[1]}_${special[1]}2912-TestSerialNumberWith${space[1]}_${special[1]}2912`
]
];
t.plan(cases.length);
for (const c of cases) t.is(common.generateDeviceId(c[0]), c[1]);
});
ava("escapeRegExp", t => {
t.is(
common.escapeRegExp("\\ ^ $ * + ? . ( ) | { } [ ]"),
"\\\\ \\^ \\$ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]"
);
});
+168
View File
@@ -0,0 +1,168 @@
import test from "node:test";
import assert from "node:assert";
import { EJSON } from "bson";
import { Filter } from "mongodb";
import { stringify, parse } from "../lib/common/expression/parser.ts";
import { convertOldPrecondition } from "../lib/db/util.ts";
import { toMongoQuery } from "../lib/db/synth.ts";
void test("convertOldPrecondition", () => {
const tests = [
[{}, "true"],
[{ test: "test" }, 'test = "test"'],
[{ test: { $eq: "test" } }, 'test = "test"'],
[{ test: { $ne: "test" } }, 'test <> "test" OR test IS NULL'],
[{ test: { $gte: "test" } }, 'test >= "test"'],
[{ "Tags.test": true }, "Tags.test IS NOT NULL"],
[{ "Tags.test": false }, "Tags.test IS NULL"],
[{ "Tags.test": { $exists: true } }, "Tags.test IS NOT NULL"],
[{ "Tags.test": { $exists: false } }, "Tags.test IS NULL"],
[{ "Tags.test": { $ne: true } }, "Tags.test IS NULL"],
[{ "Tags.test": { $ne: false } }, "Tags.test IS NOT NULL"],
[{ "Tags.test": { $eq: true } }, "Tags.test IS NOT NULL"],
[{ "Tags.test": { $eq: false } }, "Tags.test IS NULL"],
[{ _tags: "test" }, "Tags.test IS NOT NULL"],
[{ _tags: { $ne: "test" } }, "Tags.test IS NULL"],
[{ _tags: { $eq: "test" } }, "Tags.test IS NOT NULL"],
[
{ $and: [{ test: "test" }, { test: { $ne: "test" } }] },
'test = "test" AND (test <> "test" OR test IS NULL)',
],
[{ test: "test", test2: "test2" }, 'test = "test" AND test2 = "test2"'],
[
{ $or: [{ test: "test" }, { test: { $ne: "test" } }] },
'test = "test" OR (test <> "test" OR test IS NULL)',
],
[
{ test: { $gte: "test1", $ne: "test2" } },
'test >= "test1" AND (test <> "test2" OR test IS NULL)',
],
[
{ test: "test", test2: { $ne: "test2" } },
'test = "test" AND (test2 <> "test2" OR test2 IS NULL)',
],
];
const shouldFailTests = [
[{ test: { $gee: "test" } }, "Operator $gee not supported"],
[{ test: [] }, "Invalid type"],
[{ "Tags.test": { $gt: true } }, "Invalid tag query"],
[{ _tags: [] }, "Invalid type"],
[{ _tags: { $gt: "test" } }, "Invalid tag query"],
[{ $nor: [] }, "Operator $nor not supported"],
] as [Filter<unknown>, string][];
for (const t of tests) {
assert.strictEqual(
stringify(convertOldPrecondition(t[0] as Record<string, unknown>)),
t[1],
);
}
for (const t of shouldFailTests) {
const func = (): void => {
convertOldPrecondition(t[0]);
};
assert.throws(func, new Error(t[1]));
}
});
void test("toMongoQuery", async () => {
const queries: [string, Filter<unknown> | false][] = [
["true", {}],
["Tags.tag1 = true", { _tags: { $eq: "tag1" } }],
["Tags.tag1 <> false", { _tags: { $eq: "tag1" } }],
["Tags.tag1 IS NULL", { _tags: { $ne: "tag1" } }],
["Tags.tag1 = 123", false],
["Param1 = 'value1'", { "Param1._value": { $eq: "value1" } }],
[
"Param1 <> 'value1'",
{
"Param1._value": { $ne: "value1" },
$and: [{ "Param1._value": { $ne: null } }],
},
],
[
"Param1 <> 1657844103524",
{
"Param1._value": { $ne: 1657844103524 },
$and: [
{
"Param1._value": { $ne: { $date: "2022-07-15T00:15:03.524Z" } },
},
{ "Param1._value": { $ne: null } },
],
},
],
[
"Param1 = 1657844103524",
{
$or: [
{ "Param1._value": { $eq: { $date: "2022-07-15T00:15:03.524Z" } } },
{ "Param1._value": { $eq: 1657844103524 } },
],
},
],
["Param1 > 'value'", { "Param1._value": { $gt: "value" } }],
["Param1 IS NOT NULL", { "Param1._value": { $ne: null } }],
[
"Param1 LIKE 'value'",
{
"Param1._value": {
$regularExpression: { options: "s", pattern: "^value$" },
},
},
],
[
"LOWER(Param1) LIKE 'value'",
{
"Param1._value": {
$regularExpression: { options: "is", pattern: "^value$" },
},
},
],
[
"Param1 <> 'value2' OR NOT (Param2 = 'value1' OR Param1 < 'value2')",
{
$or: [
{
"Param2._value": { $ne: null },
$and: [{ "Param2._value": { $ne: "value1" } }],
"Param1._value": { $eq: "value2" },
},
{
"Param1._value": { $ne: "value2" },
$and: [{ "Param1._value": { $ne: null } }],
},
],
},
],
[
"Param1 <> 'value2' OR Param1 IS NULL",
{ "Param1._value": { $ne: "value2" } },
],
];
for (const [expStr, expect] of queries) {
const exp = parse(expStr);
let query = toMongoQuery(exp, "devices");
if (query) query = EJSON.serialize(query);
assert.deepStrictEqual(query, expect);
}
const failQueries: [any, string][] = [
["Param1 = Param2", "Right-hand operand must be a literal value"],
["Param1 LIKE Param2", "Right-hand operand of 'LIKE' must be a string"],
["NOW() = 1", "Left-hand operand must be a parameter"],
["param{param2} = 1", "Left-hand operand must be a parameter"],
];
for (const [expStr, err] of failQueries) {
const exp = parse(expStr);
assert.throws(() => toMongoQuery(exp, "devices"), {
message: err,
});
}
});
+38 -38
View File
@@ -1,97 +1,97 @@
import ava from "ava";
import Path from "../lib/common/path";
import * as device from "../lib/device";
import PathSet from "../lib/common/path-set";
import VersionedMap from "../lib/versioned-map";
import { Attributes } from "../lib/types";
import test from "node:test";
import assert from "node:assert";
import Path from "../lib/common/path.ts";
import * as device from "../lib/device.ts";
import PathSet from "../lib/common/path-set.ts";
import VersionedMap from "../lib/versioned-map.ts";
import { Attributes } from "../lib/types.ts";
ava("getAliasDeclarations", t => {
void test("getAliasDeclarations", () => {
const path = Path.parse("a.[aa:10,bb.[aaa:100].cc:1].b");
const decs = device.getAliasDeclarations(path, 99);
const expected = ["a.*.b", "a.*.aa", "a.*.bb.*.cc", "a.*.bb.*.aaa"];
t.plan(24);
for (const [i, d] of decs.entries()) {
t.is(d.path.toString(), expected[i]);
t.is(d.pathGet, 99);
t.is(d.pathSet, null);
t.deepEqual(d.attrGet, i ? { value: 99 } : null);
t.is(d.attrSet, null);
t.is(d.defer, true);
assert.strictEqual(d.path.toString(), expected[i]);
assert.strictEqual(d.pathGet, 99);
assert.strictEqual(d.pathSet, null);
assert.deepStrictEqual(d.attrGet, i ? { value: 99 } : null);
assert.strictEqual(d.attrSet, null);
assert.strictEqual(d.defer, true);
}
});
ava("unpack", t => {
void test("unpack", () => {
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[];
unpacked = device.unpack(
deviceData,
Path.parse("a.[b:b,c:c].a.[b:b1,c:c1].a")
Path.parse("a.[b:b,c:c].a.[b:b1,c:c1].a"),
);
t.is(unpacked.length, 2);
t.is(unpacked[0].toString(), "a.1.a.1.a");
t.is(unpacked[1].toString(), "a.2.a.1.a");
assert.strictEqual(unpacked.length, 2);
assert.strictEqual(unpacked[0].toString(), "a.1.a.1.a");
assert.strictEqual(unpacked[1].toString(), "a.2.a.1.a");
unpacked = device.unpack(deviceData, Path.parse("a.*.a.[b:c1,c:b1].a"));
t.is(unpacked.length, 1);
t.is(unpacked[0].toString(), "a.2.a.2.a");
assert.strictEqual(unpacked.length, 1);
assert.strictEqual(unpacked[0].toString(), "a.2.a.2.a");
});
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff. Show more