1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-10-05 07:33:41 +03:00

289 Commits

Author SHA1 Message Date
Adolfo Gómez García
e7089d9d86 Reverted typo on macos script and updated signatures 2023-04-13 13:23:34 +02:00
Adolfo Gómez García
7a367d9011 Fixed Spice tunneled connections 2023-04-13 13:20:52 +02:00
Adolfo Gómez García
089b8c82dc Updated tunnel scripts for spice and signatures 2023-04-13 12:48:02 +02:00
Adolfo Gómez García
df82c3854c minor fixes for 3.5 2023-03-30 01:20:50 +02:00
Adolfo Gómez García
f158235f16 fixes to allow rgeneration of clients 2023-03-08 13:28:40 +01:00
Adolfo Gómez García
fe704c0ba6 fixed Version to 3.5.1 to force download of UDS client 2023-03-08 11:09:01 +01:00
Adolfo Gómez García
8d635b781b regenerated 2023-03-08 10:57:47 +01:00
Adolfo Gómez García
00c48dff69 security update for UDS client 2023-03-08 10:09:40 +01:00
Adolfo Gómez García
01269e6d3e Ip Detection fix 2023-02-07 17:02:18 +01:00
Adolfo Gómez García
9478a86b02 Ignored "errored" machines on vm list 2023-01-17 12:53:25 +01:00
Adolfo Gómez García
86e8c759ec limited src ip and hostname size 2023-01-17 12:35:41 +01:00
Adolfo Gómez García
3d421ac38c Fixed transport priority on meta pools 2022-12-13 13:13:45 +01:00
Adolfo Gómez García
adec02dc3f fixed stats_counters on system 2022-11-13 03:46:30 +01:00
Adolfo Gómez García
43458cbf99 Reduced stats 2022-11-02 15:02:05 +01:00
Adolfo Gómez García
ec2645b0a2 fixed stats_counters 2022-11-02 02:56:47 +01:00
Adolfo Gómez García
9d0df6cfae small fix for client detecti 2022-10-28 14:42:09 +02:00
Adolfo Gómez García
cfbce5aef5 fixed caching calendars 2022-10-19 14:19:30 +02:00
Adolfo Gómez García
cf6820aa2b Fixed security 2022-09-05 12:48:54 +02:00
Adolfo Gómez García
1a85f60f4f Fixed "Database error" from OpenGnsys to include some more helpfull information 2022-09-02 16:45:11 +02:00
Adolfo Gómez García
83394f0d34 Fixed XEN/XCP-NG network moving on service 2022-08-18 13:56:07 +02:00
Adolfo Gómez García
c34fc41f56 unmanaged fix 2022-08-17 14:55:33 +02:00
Adolfo Gómez García
90aa455586 fixed unmanaged 2022-08-17 14:12:13 +02:00
Adolfo Gómez García
bc2328a239 fixing up sqlite 2022-08-14 21:52:24 +02:00
Adolfo Gómez García
d9d3bc452c fixed login/logout 2022-08-06 20:19:23 +02:00
Adolfo Gómez García
08f14bff57 Fixing up unmanaged actor 2022-08-05 13:33:57 +02:00
Adolfo Gómez García
653bff420f Fixed logout notification 2022-08-05 13:19:47 +02:00
Adolfo Gómez García
73a3c89e04 Fixed logout notification 2022-08-05 13:05:52 +02:00
Adolfo Gómez García
adaabf9d83 Fixing up unmanaged actor 2022-08-04 21:37:33 +02:00
Adolfo Gómez García
3cfbdc86e0 Small cryptomanager typing fix 2022-07-15 10:26:02 +02:00
Adolfo Gómez García
ba759b3652 Fixed Proxmox MAC generation for internal DB (Case sensitive...) 2022-07-14 12:49:14 +02:00
Adolfo Gómez García
1e3478314b Reformating 2022-07-14 12:48:54 +02:00
Adolfo Gómez García
89864b11c2 Fixed window upen 2022-07-06 13:20:10 +02:00
Adolfo Gómez García
c6a40ac182 fixed global logout on federated auth 2022-06-23 12:51:41 +02:00
Adolfo Gómez García
7d9ffca559 Fixex internal db with sqlite 2022-06-23 12:23:22 +02:00
Adolfo Gómez García
f43b9c7bfd Fixed small actor network card check and removed required of network for interface select 2022-06-20 19:42:36 +02:00
Adolfo Gómez García
972c48ddee Merge branch 'v3.5' of github.com:dkmstr/openuds into v3.5 2022-06-17 22:27:37 +02:00
Adolfo Gómez García
118e642700 Fixed frame over buttons ons unmanaged setup 2022-06-17 22:27:23 +02:00
Adolfo Gómez
dfa441871b Fixed logger on Windows (import mistake) 2022-06-17 22:07:25 +02:00
Adolfo Gómez
18c5e3a242 Fixed logger on Windows (import mistake) 2022-06-17 21:59:24 +02:00
Adolfo Gómez García
3a4d571a6c Fixed actor tools changes for typeinfo 2022-06-17 13:54:52 +02:00
Adolfo Gómez García
3cc42e1e73 Adding udsuser to preconnect 2022-06-16 12:52:27 +02:00
Adolfo Gómez García
ffe9baa9a5 Adding udsuser to preconnect 2022-06-16 12:49:28 +02:00
Adolfo Gómez García
0b05009d3f Adding support for several network cards ond unmanaged 2022-06-14 16:51:37 +02:00
Adolfo Gómez García
b34b12ec9f Fixed RDP Transport with RDS Sessions 2022-06-13 11:24:44 +02:00
Adolfo Gómez García
fb70524cb3 fixed sampling points 2022-06-07 21:03:49 +02:00
Adolfo Gómez García
4c66401e4f Fixes to reports 2022-06-07 15:38:57 +02:00
Adolfo Gómez García
364ebd6f3a Fixed several reports 2022-06-06 22:29:42 +02:00
Adolfo Gómez García
493cbbb4e7 fixed samplingPoints 2022-06-06 21:42:04 +02:00
Adolfo Gómez García
5277a74c1c Backport of 4.0 report fixes 2022-06-06 21:26:29 +02:00
Adolfo Gómez García
1e01339b93 Fixed language change on admin 2022-06-06 19:03:47 +02:00
Adolfo Gómez García
9343f7c263 Added CERTIFICATE_BUNDLE_PATH possible variable on environment to check certificates 2022-06-03 13:44:00 +02:00
Adolfo Gómez García
7775964d62 Added never cache to indes 2022-05-26 15:57:14 +02:00
Adolfo Gómez García
a207e8f65f Fixed csrf_field name 2022-05-26 15:36:56 +02:00
Adolfo Gómez García
0a0f2771ae Updated error page logic 2022-05-26 15:00:50 +02:00
Adolfo Gómez García
ceb5fd9bde updated csrf info 2022-05-20 09:03:45 +02:00
Adolfo Gómez García
7bfa6a6c4f Updated admin interface 2022-05-19 09:13:28 +02:00
Adolfo Gómez García
858b79614b Added improved safeHTML method to frontend 2022-05-19 09:11:10 +02:00
Adolfo Gómez García
45b47ce702 Updated user interface 2022-05-17 16:42:06 +02:00
Adolfo Gómez García
dd98ba5653 Remove clear of session on login (nonsense)0 2022-05-10 15:31:29 +02:00
Adolfo Gómez García
0fe5b32224 Fixed RDP usb redir 2022-04-29 14:59:16 +02:00
Adolfo Gómez García
a0adc1ded3 redirect to logout 2022-04-25 14:29:40 +02:00
Adolfo Gómez García
4f5cc505d3 redirect to logout 2022-04-25 14:28:18 +02:00
Adolfo Gómez García
8bac68b55b Fixed locked on machines_multi 2022-04-25 14:23:46 +02:00
Adolfo Gómez García
b5412e70fd Fixed Lock of service multi 2022-04-25 14:18:46 +02:00
Adolfo Gómez García
75cd3c4845 Chanced a couple of declarations 2022-04-24 17:13:38 +02:00
Adolfo Gómez García
e8c45b568d Fixing up some typos 2022-04-24 16:52:45 +02:00
Adolfo Gómez García
540a2b83be Added brand to configjs so we can use it on a future 2022-04-12 22:30:32 +02:00
Adolfo Gómez García
aa4d157c30 Fixed request session timeout 2022-04-12 21:36:17 +02:00
Adolfo Gómez García
69ca93586a Fixed transport & groups deletion 2022-04-12 14:34:06 +02:00
Adolfo Gómez García
cf283bba0f Fixed calendar action delete all groups 2022-04-08 15:29:13 +02:00
Adolfo Gómez García
9abaada7cb Fixed perms 2022-04-06 21:32:16 +02:00
Adolfo Gómez García
b359892454 images 2022-04-06 20:19:11 +02:00
Adolfo Gómez García
927a86c835 Added USB redirection policy for windows 2022-04-06 14:21:52 +02:00
Adolfo Gómez García
2b5aa9c9a4 Fixed address passing to tunnel 2022-04-04 21:12:54 +02:00
Adolfo Gómez García
b3047e366d Fixed 3.5 tunnel DOS attacks tolerance 2022-03-30 15:11:00 +02:00
Adolfo Gómez García
d2ef6e3704 Restored timeout 2022-03-29 22:12:43 +02:00
Adolfo Gómez García
5fb4461934 Remove external timeout 2022-03-29 22:01:10 +02:00
Adolfo Gómez García
2f5f87e122 added timeout to oppened tunnel to avoid possible DOS 2022-03-29 13:08:34 +02:00
Adolfo Gómez García
d9be83863c increaded backlog 2022-03-28 13:59:37 +02:00
Adolfo Gómez García
5fed04d64d Included request on parameters, needed on 3.5 2022-03-24 14:21:20 +01:00
Adolfo Gómez García
8a2e2deaf1 small phisical machine fix 2022-03-23 21:32:29 +01:00
Adolfo Gómez García
86990638dc Added new count method for LIMITED services 2022-03-23 21:28:49 +01:00
Adolfo Gómez García
40b9572233 fixed tunnel to log bad handshake as hex 2022-03-21 15:08:39 +01:00
Adolfo Gómez García
5836b33299 Added new way of counting "active" machines (taking into account the removable and removing services also) 2022-03-17 14:53:32 +01:00
Adolfo Gómez García
282495ce0f Fixed OSS 2022-03-15 20:22:13 +01:00
Adolfo Gómez García
2b33ffc656 Fixed OSS 2022-03-15 20:07:19 +01:00
Adolfo Gómez García
e0149900a7 Added protection on broken pipe to tunnel 2022-03-15 16:28:13 +01:00
Adolfo Gómez García
7bed6ac171 Small tunnel fix and installer info 2022-03-15 13:38:50 +01:00
Adolfo Gómez García
0d77e86af2 small type checking fix 2022-03-14 14:42:40 +01:00
Adolfo Gómez García
a179522f4c Fixed crypto key loading 2022-03-06 15:40:49 +01:00
Adolfo Gómez García
21c2976d82 Fixed copyright for debian 2022-02-27 23:41:50 +01:00
Adolfo Gómez García
ee30ab4604 Fixed authcallbacks 2022-02-23 21:54:23 +01:00
Adolfo Gómez García
1fba4d3f9f Fixed check of ip 2022-02-23 14:17:29 +01:00
Adolfo Gómez García
5084fec43f Fixed SQLITE DB problems 2022-02-22 13:26:24 +01:00
Adolfo Gómez García
04e24d406f Added small fix to allow tempora user redirect 2022-02-21 14:28:49 +01:00
Adolfo Gómez García
f58ef9b6d3 Removed sympy inclusion err 2022-02-15 15:16:04 +01:00
Adolfo Gómez García
18d4147d59 Changed OS Detection system 2022-02-15 15:05:55 +01:00
Adolfo Gómez García
ccd429454e Updated translations due to recent fixes 2022-02-10 14:46:27 +01:00
Adolfo Gómez García
5ce7ddc3a7 Fixed HTML5 transports and advanced tab translation for label 2022-02-10 13:32:05 +01:00
Adolfo Gómez García
3dd73f4723 Vertical label now appears "badly" with waseyprint. Disabled by now 2022-02-07 16:02:16 +01:00
Adolfo Gómez García
c3531f3e7e Fixed saving stats events (field conversion ignored original field name) 2022-02-07 15:54:38 +01:00
Adolfo Gómez García
ba7b1c0198 Fixed 0038 migration to include config moving 2022-02-06 04:01:29 +01:00
Adolfo Gómez García
ba90dae5d6 Fixed Tunnel version 2022-02-05 17:50:34 +01:00
Adolfo Gómez García
f7cd474264 Fixed double open on meta poools 2022-01-30 18:21:32 +01:00
Adolfo Gómez García
a255b52628 added info (apart of prefix to uuid) if pool is meta or not 2022-01-29 21:43:14 +01:00
Adolfo Gómez García
8d93144e24 Fixed meta pools non being correctly checked 2022-01-28 11:54:18 +01:00
Adolfo Gómez García
27d158f514 Fixed metapool admin 2022-01-28 11:14:32 +01:00
Adolfo Gómez García
2b4e771709 Fixed autorun check from api 2022-01-27 12:17:00 +01:00
Adolfo Gómez García
3ebc0dd26f Fixed certs locations for some platforms 2022-01-21 12:04:54 +01:00
Adolfo Gómez García
79739bf9b8 Removed unused .desktop file for thinpro 2022-01-18 14:12:31 +01:00
Adolfo Gómez García
f702c144fc small thinpro fix 2022-01-18 13:34:33 +01:00
Adolfo Gómez García
ce2d2b1c2e added installer for thinpro 7.2 2022-01-18 13:18:24 +01:00
Adolfo Gómez García
790c204b6a fixed uds client actor launching 2022-01-17 13:46:14 +01:00
Adolfo Gómez García
d80cf4052e removed -s 2022-01-17 13:43:51 +01:00
Adolfo Gómez García
6a86b0ff04 Updated translations 2022-01-14 12:37:14 +01:00
Adolfo Gómez García
0d412c4a9a Modified thinpro image tar from bz2 to gz 2022-01-12 13:25:26 +01:00
Adolfo Gómez García
ac9e6dafdf added thinpro installer generator 2022-01-12 13:20:33 +01:00
Adolfo Gómez García
efd0ca3f88 Fixed tunnel stop comms 2022-01-11 15:36:18 +01:00
Adolfo Gómez García
edb4a32496 Updated translations 2022-01-10 14:34:09 +01:00
Adolfo Gómez García
b239ff6cab Removed "harcoded" msrdc path for mac 2022-01-04 10:38:30 +01:00
Adolfo Gómez García
d55d1bc619 Added localized MSRDP as possible path 2022-01-03 14:35:48 +01:00
Adolfo Gómez García
917a201483 Added localized MSRDP as possible path 2022-01-03 14:23:52 +01:00
Adolfo Gómez García
4809252434 Changed concurrent removal to take into account real removals, not removal checks 2022-01-03 14:07:46 +01:00
Adolfo Gómez García
8be0d9702a Fixed sessions providers for html5rdp 2021-12-22 14:04:49 +01:00
Adolfo Gómez García
36acb0b0c0 Fixed transports sorting on metapools 2021-12-22 13:10:07 +01:00
Adolfo Gómez García
420b78d45d Fixed STOP "eating" on application stop 2021-12-21 15:58:46 +01:00
Adolfo Gómez García
e1ccc62dab Fixed minvalue for max services 2021-12-21 15:53:35 +01:00
Adolfo Gómez García
6b0d98d4eb Fixed radius auth not using "appliaction Prefix" for extracting groups from Class Attribute (now accepts group=... and {appPrefix}group=.... as group markers 2021-12-21 11:14:57 +01:00
Adolfo Gómez García
7bec7bd7cc Fixed HTMLRDP for access to RDP session with automanaged users 2021-12-20 12:04:16 +01:00
Adolfo Gómez García
270957fab5 Updated settings sample 2021-12-10 13:04:54 +01:00
Adolfo Gómez García
47c6ca42f1 added Content-Security-Policy to security 2021-11-30 13:54:15 +01:00
Adolfo Gómez García
c1f6ed376b added Content-Security-Policy to security 2021-11-30 13:32:37 +01:00
Adolfo Gómez García
250ade6aee Fixed assignement of new services if pool is at 100% usage 2021-11-30 12:18:04 +01:00
Adolfo Gómez García
bde63f7b4f Added check for database connection problem on config 2021-11-26 11:52:49 +01:00
Adolfo Gómez García
eb4be53508 Fixed cache time and points on system chart info 2021-11-19 14:10:36 +01:00
Adolfo Gómez García
3003066a91 Removed "erroring" machine is it has any exception on connection 2021-11-18 15:35:04 +01:00
Adolfo Gómez García
10805ded7e Removed "erroring" machine is it has any exception on connection 2021-11-18 15:26:23 +01:00
Adolfo Gómez García
21c221a6db Added check for circular connections on Xen when using backup server 2021-11-16 14:19:46 +01:00
Adolfo Gómez García
1857134f42 Fixed admin 2021-11-15 12:15:06 +01:00
Adolfo Gómez García
835dc05e63 Added scheduled action to pool so we can remove "old assigned machines" with a programmed action 2021-11-11 12:07:19 +01:00
Adolfo Gómez García
4cc4af5bd1 Fixed special case for admin form field of numeric fields without limits 2021-11-10 10:56:05 +01:00
Adolfo Gómez García
986a82f225 Fixed special case for admin form field of numeric fields without limits 2021-11-10 10:32:15 +01:00
Adolfo Gómez García
90b64c1721 Changed parameter _USERNAME_ for _USER_ on URL Transport (as in tooltip) 2021-11-08 13:28:09 +01:00
Adolfo Gómez García
f403d4ff3e Fixed Min-Max admin values checking && set proxmox vmid as readonly 2021-11-08 13:18:58 +01:00
Adolfo Gómez García
c5071cf348 Fixed Min-Max admin values checking && set proxmox vmid as readonly 2021-11-08 13:16:44 +01:00
Adolfo Gómez García
679956702b Fixed legacy textx 2021-11-03 14:54:58 +01:00
Adolfo Gómez García
98d7a24656 Fixed check certificate on python 3.6 2021-11-03 14:39:58 +01:00
Adolfo Gómez García
b67771d5f3 Fixed HTMLRDP parameters 2021-11-02 11:56:21 +01:00
Adolfo Gómez García
672c35c903 Fixed admin date && updated translations 2021-11-02 11:05:38 +01:00
Adolfo Gómez García
6df1bc0a50 Removed Legacy client messages from frontend 2021-10-29 11:38:39 +02:00
Adolfo Gómez García
01119d1914 Fixed armhf appimage generation 2021-10-28 11:19:49 +02:00
Adolfo Gómez García
a4d1ecb95f Added call to "notifyReady" on osmanager ready notification 2021-10-27 13:05:41 +02:00
Adolfo Gómez García
237f7e5b77 Added igel port 2021-10-25 14:42:42 +02:00
Adolfo Gómez García
edb74ab9c6 Removed "legacy" 2.7 UDS Client (not working anymore on 3.5)
Fixed igel templates
2021-10-25 14:36:14 +02:00
Adolfo Gómez García
86eb1a9421 Added "cloud marked" icons for tunneled transports 2021-10-25 12:56:18 +02:00
Adolfo Gómez García
c09ea0eb63 Moved security part from request to security middleware 2021-10-23 22:36:12 +02:00
Adolfo Gómez García
ea79ccbee1 Added igel package creation scripts 2021-10-22 14:37:53 +02:00
Adolfo Gómez García
da82a26dd8 Now when we save a service pool, ensures that max_srvs is at leat 1 for services with cache 2021-10-19 18:21:32 +02:00
Adolfo Gómez García
c129c83ca0 Added -s also to udsactor user space 2021-10-18 18:00:05 +02:00
Adolfo Gómez García
d8e6de8c1e Removed unused variable 2021-10-18 17:16:17 +02:00
Adolfo Gómez García
e0d79cb590 Added -s to UDSClient python3 parameter, so local libs does not interfere with package 2021-10-18 17:04:44 +02:00
Adolfo Gómez García
59bd6c1649 Reversed the order for change password on 3.5 UDS 2021-10-18 16:59:59 +02:00
Adolfo Gómez García
564f0e17de added check for "emtpy" usernames or groups on creation 2021-10-18 13:05:53 +02:00
Adolfo Gómez García
842212f186 Removed ssh-tunnel not used on 3.5 release 2021-10-17 01:43:01 +02:00
Adolfo Gómez García
e4b609c4ce Fixed key for debian packages on client appimage recipe 2021-10-15 10:57:43 +02:00
Adolfo Gómez García
741855030f Removed "prints" :) 2021-10-15 10:44:22 +02:00
Adolfo Gómez García
293b7f02ad added small comment for future to actor v3 2021-10-13 11:19:44 +02:00
Adolfo Gómez García
fddd54fa99 Added correcto management of "logout" in case of an unmanaged machine "reboot" 2021-10-08 12:30:00 +02:00
Adolfo Gómez García
cd640af37f Added correcto management of "logout" in case of an unmanaged machine "reboot" 2021-10-08 12:28:37 +02:00
Adolfo Gómez García
6f99b63731 Locales 2021-10-08 00:57:08 +02:00
Adolfo Gómez García
6b3355f819 Added locking multi_ip machines if accessed from outside UDS flag & logic 2021-10-07 13:47:03 +02:00
Adolfo Gómez García
660cfdcd0e Adding console login/logout logic on static machines 2021-10-07 12:49:40 +02:00
Adolfo Gómez García
47df6c58fc Cosmetic chage to actorv3 2021-10-06 15:21:50 +02:00
Adolfo Gómez García
91c90766a3 Updated translations 2021-10-06 15:10:35 +02:00
Adolfo Gómez García
2a834460d1 Fixing up html5rdp 2021-10-06 12:38:45 +02:00
Adolfo Gómez García
5bd77676ca Fixed log of user correctly authenticated, but not belongs to any group 2021-10-05 12:23:13 +02:00
Adolfo Gómez García
8ef97a7773 Fix for client with python 3.6 2021-10-01 12:35:20 +02:00
Adolfo Gómez García
abafa7bfac Added group state "Inactive" 2021-09-29 14:50:40 +02:00
Adolfo Gómez García
dcb7b3e28e Make 3.5 client compatible with python 3.6 2021-09-29 13:42:26 +02:00
Adolfo Gómez García
41aa22fadd Removed optional parameter "transport" from ticket REST api creation. This is due to the fact than the transport needs to be checked on Client browser (user ip, SO, etc...) 2021-09-29 11:04:51 +02:00
Adolfo Gómez García
d02974ad87 Error page was not displayed correctly 2021-09-29 10:46:58 +02:00
Adolfo Gómez García
b2a067300c Added sample ticket auth test 2021-09-29 00:14:02 +02:00
Adolfo Gómez García
afbc75bff0 Added boolean True as valid force value 2021-09-29 00:13:33 +02:00
Adolfo Gómez García
4c453d2b1f Added more info to ticket timedout error on tunnel 2021-09-24 14:42:01 +02:00
Adolfo Gómez García
26f33626c2 Updated translations 2021-09-24 13:52:17 +02:00
Adolfo Gómez García
cb8284d076 Updated RDP scripts (simple cosmetic changes) 2021-09-23 16:53:17 +02:00
Adolfo Gómez García
ef3dd893d9 Added nicedcv protocol && a couple of aliases parameters for user_interface future migration 2021-09-21 16:43:27 +02:00
Adolfo Gómez García
d531a1612a Added "visibleFrom" to authenticators, so we can add custom filters for showing them on login screen 2021-09-16 13:30:38 +02:00
Adolfo Gómez García
de9c06bc2c Fixed "realname overwrite" on internaldb auth 2021-09-15 13:15:55 +02:00
Adolfo Gómez García
2400cc99cd Updated translations 2021-09-15 12:47:08 +02:00
Adolfo Gómez García
7f5c3c3bbd Fixed new remove all groups description & fixed not removing pinbar on tunnel rdp 2021-09-14 11:02:37 +02:00
Adolfo Gómez García
710f2fb0e4 Fixed task manager stop 2021-09-09 13:59:42 +02:00
Adolfo Gómez García
ede23ad793 Improved check of tunneled requests 2021-09-09 12:56:25 +02:00
Adolfo Gómez García
9a3913cc42 Added scheduled action "Remove all transports" and "remove all groups" 2021-09-07 13:55:16 +02:00
Adolfo Gómez García
5bf98782ea Added autocomplete to field types 2021-09-07 13:31:30 +02:00
Adolfo Gómez García
3a69c9205e Removed nonsense security check right now... 2021-09-07 12:15:44 +02:00
Adolfo Gómez García
3615db877e Fix small error on new singleton for taskManager 2021-09-06 13:39:40 +02:00
Adolfo Gómez García
2286ccaca1 Fixed about 2021-09-06 12:36:58 +02:00
Adolfo Gómez García
f90bf3a421 Added sedcurity middleware also 2021-09-04 22:17:41 +02:00
Adolfo Gómez García
df815776da Added asgi from newer model 2021-09-04 21:29:16 +02:00
Adolfo Gómez García
54f7fd21dc Better singleton pattern (more reusable) 2021-09-04 17:16:57 +02:00
Adolfo Gómez García
8e3d90e7f3 Removed "experimental" from AD group on OS Manager and fix on actor runner 2021-09-03 13:38:39 +02:00
Adolfo Gómez García
afa9e0aab6 Upgraded angular version of js 2021-09-03 02:25:01 +02:00
Adolfo Gómez García
77b0c7c8e1 added comment to user interface 2021-09-03 01:31:02 +02:00
Adolfo Gómez García
23afd01004 Fixed log removal 2021-09-02 13:27:27 +02:00
Adolfo Gómez García
c30a67d363 Fixed admin 2021-08-31 14:13:17 +02:00
Adolfo Gómez García
aa2d268453 Fixed admin interface small bug 2021-08-31 13:44:13 +02:00
Adolfo Gómez García
de40c72d9e Fixed "disabled" tag to allow login with only federated auths 2021-08-24 17:02:36 +02:00
Adolfo Gómez García
d0b30b561c Updated cache decorator and updated signatures of modified plugins 2021-08-24 14:07:35 +02:00
Adolfo Gómez García
e485374836 Formating and type fixing 2021-08-24 12:15:10 +02:00
Adolfo Gómez García
3934f2b88d Formating and type fixing all transports 2021-08-24 11:51:56 +02:00
Adolfo Gómez García
c72bcf4200 More formating 2021-08-23 14:59:07 +02:00
Adolfo Gómez García
1b7076e645 Changed "app.exec_" by "app.exec" for future pyqt6 2021-08-21 23:06:19 +02:00
Adolfo Gómez García
e637f208bd Changed app.exec_ by app.exec (future PyQt6) 2021-08-21 23:05:20 +02:00
Adolfo Gómez García
75e54618bb Removed duplicated download 2021-08-19 12:21:39 +02:00
Adolfo Gómez García
04864e3846 Fixed to ensure cache is uptated after template creation 2021-08-19 01:21:09 +02:00
Adolfo Gómez García
a52be141ea Added proxmox connection error check and try to handle y gracefully 2021-08-17 13:04:20 +02:00
Adolfo Gómez García
afcbd058d1 Formating & fixing type checkings 2021-08-14 15:47:21 +02:00
Adolfo Gómez García
8285e2daad More formating & minor typing fixes 2021-08-13 15:11:22 +02:00
Adolfo Gómez García
03bfb3efbb Formating & minor typing fixes 2021-08-13 14:53:23 +02:00
Adolfo Gómez García
8c4b84e7db removed statsManager and used directly "StatsManager.manager()" 2021-08-13 14:09:46 +02:00
Adolfo Gómez García
4f8fe793cc Updated translations 2021-08-13 13:34:38 +02:00
Adolfo Gómez García
286b320257 Updated openstack to look for correct volume api
Updated admin to make optional the "vnc" for user services
2021-08-13 13:33:39 +02:00
Adolfo Gómez García
68411f0726 UDS 3.4 now uses volumev3 for non legacy openstack connections (legacy maintains v2) 2021-08-11 18:59:18 +02:00
Adolfo Gómez García
1be49a6e0e Separated processes manager from main uds_tunnel 2021-08-05 12:53:44 +02:00
Adolfo Gómez García
c21c0b44ce Added guacamole rdp parameter for future suppport 2021-08-04 18:59:51 +02:00
Adolfo Gómez García
46aa9139a0 Fixed Guacamole dict 2021-08-02 13:14:57 +02:00
Adolfo Gómez García
574b19a905 Fixed bug on user services page load and updated translations 2021-07-29 13:13:43 +02:00
Adolfo Gómez García
612646bd1c Fixed userService name on ServiceNotReady exception && small fix to comment 2021-07-29 12:24:24 +02:00
Adolfo Gómez García
10d9279b89 Added default value as TRUE to font smoothing for RDP 2021-07-28 14:08:16 +02:00
Adolfo Gómez García
a8a5063083 Updated Guacamole to only accept authenticated tunnel connections
* Added handshake check BEFORE opening SSL tunnel
2021-07-28 12:57:58 +02:00
Adolfo Gómez García
29b6613c95 Updated space 2021-07-27 12:51:10 +02:00
Adolfo Gómez García
8aa7dc3c6f Added PORT to RDP connections 2021-07-27 12:40:12 +02:00
Adolfo Gómez García
e75d373d03 Service multi is fixed
(Also small tunnel beautify)
2021-07-23 14:00:21 +02:00
Adolfo Gómez García
91d2398ade Fixed multy phisical machines service to add a "custom" maximum duration for assignation 2021-07-21 13:59:12 +02:00
Adolfo Gómez García
f4e953c9c9 Fixed type checkings and detection of client launched when machine not ready 2021-07-20 13:32:28 +02:00
Adolfo Gómez García
f14f36b0d0 Merge remote-tracking branch 'origin/v3.0' 2021-07-19 13:27:00 +02:00
Adolfo Gómez García
d1e51c0103 Upgrading actor for unmanaged && fixed linux operation 2021-07-19 13:26:36 +02:00
Adolfo Gómez García
d38347c534 Fixed ticket for metapools & fixed get interfaces list for python > 3.2 (as is the case) 2021-07-19 13:25:43 +02:00
Adolfo Gómez García
6fd307e86e small fixes (typing) 2021-07-19 12:42:26 +02:00
Adolfo Gómez García
51407b54ee Small spelling fixes 2021-07-19 01:16:18 +02:00
Adolfo Gómez García
91f90c8630 Small sample fix 2021-07-18 15:45:03 +02:00
Adolfo Gómez García
ca5b54c8e2 Added hidden dark theme to administration 2021-07-14 13:49:58 +02:00
Adolfo Gómez García
8d74055357 Added "copy" feature to admin tables 2021-07-13 22:50:55 +02:00
Adolfo Gómez García
8e81d51a43 Fixed Admin tunnel tokens 2021-07-13 15:11:38 +02:00
Adolfo Gómez García
5ff6cdaf69 Fixed tunnel token headers && tunnel proxy typo 2021-07-13 15:00:00 +02:00
Adolfo Gómez García
13cbfe26c7 Fixes (Basically formating & type checking fixes 2021-07-13 13:36:42 +02:00
Adolfo Gómez García
d497235eeb * Added config parameter for "check removal processes hanged" and removed six from RDP client scripts (and regenerated signatures) 2021-07-13 11:53:22 +02:00
Adolfo Gómez García
7d8bcf2168 Fix small admin issue 2021-07-12 16:39:45 +02:00
Adolfo Gómez García
5706f9d681 Fixed drop down menus on mouse over 2021-07-12 15:12:11 +02:00
Adolfo Gómez García
cd06597918 Formatting fixes 2021-07-12 12:58:45 +02:00
Adolfo Gómez García
49ce5622d6 Correctly added Tokens table permissions type 2021-07-12 12:58:26 +02:00
Adolfo Gómez García
de5031febf Fixed memory cache cleanup 2021-07-12 12:57:48 +02:00
Adolfo Gómez García
b29baf2a29 Small fis on service pool 2021-07-10 21:16:33 +02:00
Adolfo Gómez García
aaa909fff0 Added tunnel info to normalize return values & log values 2021-07-10 13:19:45 +02:00
Adolfo Gómez García
99ee0b00fc Added actor token to admin 2021-07-09 13:13:31 +02:00
Adolfo Gómez García
f2643df05f added typos to cryptography 2021-07-08 22:31:25 +02:00
Adolfo Gómez García
2520cce429 Fixed error on status check for "respawneable" services 2021-07-08 17:47:12 +02:00
Adolfo Gómez García
962015c355 Added types to crypto 2021-07-08 17:46:46 +02:00
Adolfo Gómez García
582ba01014 Added minimun number to show "filter" on service list 2021-07-08 14:42:58 +02:00
Adolfo Gómez García
eec8588628 Updated translations 2021-07-08 14:31:44 +02:00
Adolfo Gómez García
37f59e952d Added translated filter string 2021-07-08 14:18:15 +02:00
Adolfo Gómez García
46bab75a92 Added crpytomanager typing 2021-07-08 14:17:59 +02:00
Adolfo Gómez García
8f7421ef9d Updated translations 2021-07-08 13:00:22 +02:00
Adolfo Gómez García
a7584f9e8e Fixed admin 2021-07-08 12:57:36 +02:00
Adolfo Gómez García
fad735bb87 Added ticket compat with 3.0 2021-07-08 12:22:36 +02:00
Adolfo Gómez García
5ba704ac8a Fixed Version number for actor 2021-07-08 10:40:56 +02:00
Adolfo Gómez García
3c5ef5817f Added tooo long machines on removing state as hanged 2021-07-06 14:46:21 +02:00
Adolfo Gómez García
de0db84a5d Added tooo long machines on removing state as hanged 2021-07-06 14:45:01 +02:00
Adolfo Gómez García
548b6e813d Fixed Proxmox concurrencly on vmid assignation problem 2021-07-06 12:39:22 +02:00
Adolfo Gómez García
31b513a7ef Type checking updates 2021-07-06 11:33:04 +02:00
Adolfo Gómez García
fa7ce3de0b Added more info to terminated connection on UDS tunnel 2021-07-05 18:12:46 +02:00
Adolfo Gómez García
3a7e7b8dfc Fixed Client on non standard ports 2021-07-05 18:03:22 +02:00
Adolfo Gómez García
c9488329b9 Fixed Client on non standard ports 2021-07-05 17:54:02 +02:00
Adolfo Gómez García
55c4574021 Added redirect to login on session timeout 2021-07-05 13:48:56 +02:00
Adolfo Gómez García
59179584f2 Fixed tunnel redirect 2021-07-05 10:46:43 +02:00
Adolfo Gómez García
92de3b01dd Removed "plugin download" event, not used 2021-07-04 16:50:42 +02:00
Adolfo Gómez García
c62d62dd65 commented the events generated and logged by UDS 2021-07-04 15:17:51 +02:00
Adolfo Gómez García
e02318e665 Enhacing tunnel data logging info 2021-07-04 13:25:42 +02:00
Adolfo Gómez García
612ae63cf2 Added events to HTML5 connection also (only conneciton event right now) 2021-07-04 13:04:11 +02:00
Adolfo Gómez García
cb44662134 commenting changes on tunnel 2021-07-03 22:01:42 +02:00
Adolfo Gómez García
a359ff2263 Fixing tunnel & client for mac 2021-07-03 21:48:38 +02:00
Adolfo Gómez García
9ca3a7cdeb Fixed proxy sent stats to UDS 2021-07-03 21:16:17 +02:00
Adolfo Gómez García
1736cae1c1 Fixed image upload 2021-07-03 20:59:23 +02:00
Adolfo Gómez García
727ffe0365 Added a basic bot check to request middleware to forbid bots access 2021-07-03 16:25:07 +02:00
Adolfo Gómez García
b031e0aa3c adding fixes on closing tunnel 2021-07-03 13:02:34 +02:00
Adolfo Gómez García
d7886a1281 adding fixes on closing tunnel 2021-07-03 12:58:26 +02:00
Adolfo Gómez García
09e88b60f5 Updated launcher so, if launcher is closed, all tunnels are also closed 2021-07-03 12:30:46 +02:00
Adolfo Gómez García
6af0617c2a Upgrading client for MAC multi open compatibility 2021-07-02 15:18:35 +02:00
427 changed files with 39346 additions and 30929 deletions

View File

@@ -3,4 +3,4 @@
FOLDER=/usr/share/UDSActor
cd $FOLDER
exec python3 actor_client.py -platform xcb $@
exec python3 -s actor_client.py -platform xcb $@

View File

@@ -69,7 +69,7 @@ if __name__ == "__main__":
timer.start(1000)
timer.timeout.connect(lambda *a: None) # type: ignore # timeout can be connected to a callable
qApp.exec_()
qApp.exec()
# On windows, if no window is created, this point will never be reached.
qApp.end()

View File

@@ -187,9 +187,9 @@ if __name__ == "__main__":
app = QApplication(sys.argv)
if udsactor.platform.operations.checkPermissions() is False:
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok)
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok) # type: ignore
sys.exit(1)
myapp = UDSConfigDialog()
myapp.show()
sys.exit(app.exec_())
sys.exit(app.exec())

View File

@@ -40,6 +40,7 @@ import PyQt5 # pylint: disable=unused-import
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
import udsactor
import udsactor.tools
from ui.setup_dialog_unmanaged_ui import Ui_UdsActorSetupDialog
@@ -49,6 +50,7 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger('actor')
class UDSConfigDialog(QDialog):
_host: str = ''
_config: udsactor.types.ActorConfigurationType
@@ -60,65 +62,99 @@ class UDSConfigDialog(QDialog):
self.ui = Ui_UdsActorSetupDialog()
self.ui.setupUi(self)
self.ui.host.setText(self._config.host)
self.ui.validateCertificate.setCurrentIndex(1 if self._config.validateCertificate else 0)
self.ui.validateCertificate.setCurrentIndex(
1 if self._config.validateCertificate else 0
)
self.ui.logLevelComboBox.setCurrentIndex(self._config.log_level)
self.ui.serviceToken.setText(self._config.master_token)
self.ui.serviceToken.setText(self._config.master_token or '')
self.ui.restrictNet.setText(self._config.restrict_net or '')
self.ui.testButton.setEnabled(bool(self._config.master_token and self._config.host))
self.ui.testButton.setEnabled(
bool(self._config.master_token and self._config.host)
)
@property
def api(self) -> udsactor.rest.UDSServerApi:
return udsactor.rest.UDSServerApi(self.ui.host.text(), self.ui.validateCertificate.currentIndex() == 1)
return udsactor.rest.UDSServerApi(
self.ui.host.text(), self.ui.validateCertificate.currentIndex() == 1
)
def finish(self) -> None:
self.close()
def configChanged(self, text: str) -> None:
self.ui.testButton.setEnabled(self.ui.host.text() == self._config.host and self.ui.serviceToken.text() == self._config.master_token)
self.ui.testButton.setEnabled(
self.ui.host.text() == self._config.host
and self.ui.serviceToken.text() == self._config.master_token
and self.ui.restrictNet.text() == self._config.restrict_net
)
def testUDSServer(self) -> None:
if not self._config.master_token or not self._config.host:
self.ui.testButton.setEnabled(False)
return
try:
api = udsactor.rest.UDSServerApi(self._config.host, self._config.validateCertificate)
api = udsactor.rest.UDSServerApi(
self._config.host, self._config.validateCertificate
)
if not api.test(self._config.master_token, udsactor.types.UNMANAGED):
QMessageBox.information(
self,
'UDS Test',
'Service token seems to be invalid . Please, check token validity.',
QMessageBox.Ok
QMessageBox.Ok,
)
else:
QMessageBox.information(
self,
'UDS Test',
'Configuration for {} seems to be correct.'.format(self._config.host),
QMessageBox.Ok
'Configuration for {} seems to be correct.'.format(
self._config.host
),
QMessageBox.Ok,
)
except Exception:
QMessageBox.information(
self,
'UDS Test',
'Configured host {} seems to be inaccesible.'.format(self._config.host),
QMessageBox.Ok
QMessageBox.Ok,
)
def saveConfig(self) -> None:
# Ensure restrict_net is empty or a valid subnet
restrictNet = self.ui.restrictNet.text().strip()
if restrictNet:
try:
subnet = udsactor.tools.strToNoIPV4Network(restrictNet)
if not subnet:
raise Exception('Invalid subnet')
except Exception:
QMessageBox.information(
self,
'Invalid subnet',
'Invalid subnet {}. Please, check it.'.format(restrictNet),
QMessageBox.Ok,
)
return
# Store parameters on register for later use, notify user of registration
self._config = udsactor.types.ActorConfigurationType(
actorType=udsactor.types.UNMANAGED,
host=self.ui.host.text(),
validateCertificate=self.ui.validateCertificate.currentIndex() == 1,
master_token=self.ui.serviceToken.text(),
log_level=self.ui.logLevelComboBox.currentIndex()
master_token=self.ui.serviceToken.text().strip(),
restrict_net=restrictNet,
log_level=self.ui.logLevelComboBox.currentIndex(),
)
udsactor.platform.store.writeConfig(self._config)
# Enables test button
self.ui.testButton.setEnabled(True)
# Informs the user
QMessageBox.information(self, 'UDS Configuration', 'Configuration saved.', QMessageBox.Ok)
QMessageBox.information(
self, 'UDS Configuration', 'Configuration saved.', QMessageBox.Ok
)
if __name__ == "__main__":
@@ -127,9 +163,9 @@ if __name__ == "__main__":
os.environ['QT_X11_NO_MITSHM'] = '1'
app = QApplication(sys.argv)
if udsactor.platform.operations.checkPermissions() is False:
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok)
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok) # type: ignore
sys.exit(1)
if len(sys.argv) > 2:
@@ -153,4 +189,4 @@ if __name__ == "__main__":
myapp = UDSConfigDialog()
myapp.show()
sys.exit(app.exec_())
sys.exit(app.exec())

View File

@@ -10,8 +10,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>595</width>
<height>220</height>
<width>601</width>
<height>243</height>
</rect>
</property>
<property name="sizePolicy">
@@ -55,7 +55,7 @@
<property name="geometry">
<rect>
<x>10</x>
<y>180</y>
<y>210</y>
<width>181</width>
<height>23</height>
</rect>
@@ -83,7 +83,7 @@
<property name="geometry">
<rect>
<x>410</x>
<y>180</y>
<y>210</y>
<width>171</width>
<height>23</height>
</rect>
@@ -117,7 +117,7 @@
<property name="geometry">
<rect>
<x>210</x>
<y>180</y>
<y>210</y>
<width>181</width>
<height>23</height>
</rect>
@@ -144,7 +144,7 @@
<x>10</x>
<y>10</y>
<width>571</width>
<height>161</height>
<height>191</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
@@ -221,14 +221,14 @@
</property>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_loglevel">
<property name="text">
<string>Log Level</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QComboBox" name="logLevelComboBox">
<property name="currentIndex">
<number>1</number>
@@ -258,6 +258,23 @@
</item>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_restrictNet">
<property name="text">
<string>Restrict Net</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="restrictNet">
<property name="toolTip">
<string>UDS user with administration rights (Will not be stored on template)</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Administrator user on UDS Server.&lt;/p&gt;&lt;p&gt;Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
<zorder>label_host</zorder>
<zorder>host</zorder>
@@ -267,6 +284,8 @@
<zorder>label_security</zorder>
<zorder>label_loglevel</zorder>
<zorder>logLevelComboBox</zorder>
<zorder>label_restrictNet</zorder>
<zorder>restrictNet</zorder>
</widget>
</widget>
<resources>
@@ -353,6 +372,22 @@
</hint>
</hints>
</connection>
<connection>
<sender>restrictNet</sender>
<signal>textChanged(QString)</signal>
<receiver>UdsActorSetupDialog</receiver>
<slot>configChanged()</slot>
<hints>
<hint type="sourcelabel">
<x>341</x>
<y>139</y>
</hint>
<hint type="destinationlabel">
<x>295</x>
<y>121</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>finish()</slot>

View File

@@ -185,7 +185,8 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
try:
# Notify loging and mark it
self._loginInfo = self.api.login(platform.operations.getCurrentUser(), platform.operations.getSessionType())
user, sessionType = platform.operations.getCurrentUser(), platform.operations.getSessionType()
self._loginInfo = self.api.login(user, sessionType)
if self._loginInfo.max_idle:
platform.operations.initIdleDuration(self._loginInfo.max_idle)
@@ -197,8 +198,11 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
time.sleep(1.3) # Sleeps between loop iterations
self.api.logout(user + self._extraLogoff, sessionType)
logger.info('Notified logout for %s (%s)', user, sessionType) # Log logout
# Clean up login info
self._loginInfo = None
self.api.logout(platform.operations.getCurrentUser() + self._extraLogoff)
except Exception as e:
logger.error('Error on client loop: %s', e)

View File

@@ -42,7 +42,7 @@ class LocalProvider(handler.Handler):
return result._asdict()
def post_logout(self) -> typing.Any:
self._service.logout(self._params['username'])
self._service.logout(self._params['username'], self._params['session_type'])
return 'ok'
def post_ping(self) -> typing.Any:

View File

@@ -38,6 +38,7 @@ from ..log import logger
if typing.TYPE_CHECKING:
from ..service import CommonService
class PublicProvider(handler.Handler):
def post_logout(self) -> typing.Any:
logger.debug('Sending LOGOFF to clients')
@@ -51,7 +52,9 @@ class PublicProvider(handler.Handler):
logger.debug('Sending MESSAGE to clients')
if 'message' not in self._params:
raise Exception('Invalid message parameters')
self._service._clientsPool.message(self._params['message']) # pylint: disable=protected-access
self._service._clientsPool.message(
self._params['message']
) # pylint: disable=protected-access
return 'ok'
def post_script(self) -> typing.Any:
@@ -60,7 +63,9 @@ class PublicProvider(handler.Handler):
raise Exception('Invalid script parameters')
if self._params.get('user', False):
logger.debug('Sending SCRIPT to client')
self._service._clientsPool.executeScript(self._params['script']) # pylint: disable=protected-access
self._service._clientsPool.executeScript(
self._params['script']
) # pylint: disable=protected-access
else:
# Execute script at server space, that is, here
# as a parallel thread
@@ -72,14 +77,22 @@ class PublicProvider(handler.Handler):
logger.debug('Received Pre connection')
if 'user' not in self._params or 'protocol' not in self._params:
raise Exception('Invalid preConnect parameters')
return self._service.preConnect(self._params['user'], self._params['protocol'], self._params.get('ip', 'unknown'), self._params.get('hostname', 'unknown'))
return self._service.preConnect(
self._params['user'],
self._params['protocol'],
self._params.get('ip', 'unknown'),
self._params.get('hostname', 'unknown'),
self._params.get('udsuser', 'unknown'),
)
def get_information(self) -> typing.Any:
# Return something useful? :)
return 'UDS Actor Secure Server'
def get_screenshot(self) -> typing.Any:
return self._service._clientsPool.screenshot() # pylint: disable=protected-access
return (
self._service._clientsPool.screenshot()
) # pylint: disable=protected-access
def get_uuid(self) -> typing.Any:
if self._service.isManaged():

View File

@@ -159,7 +159,7 @@ class HTTPServerThread(threading.Thread):
# self._server.socket = ssl.wrap_socket(self._server.socket, certfile=self.certFile, server_side=True)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.options = ssl.CERT_NONE
# context.options = ssl.CERT_NONE
context.load_cert_chain(certfile=self._certFile, password=password)
self._server.socket = context.wrap_socket(self._server.socket, server_side=True)

View File

@@ -1 +0,0 @@
VERSION = '3.0.0'

View File

@@ -37,6 +37,7 @@ import typing
class LocalLogger: # pylint: disable=too-few-public-methods
linux = False
windows = True
serviceLogger = False
logger: typing.Optional[logging.Logger]

View File

@@ -91,12 +91,12 @@ def _getInterfaces() -> typing.List[str]:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
names = array.array(str('B'), b'\0' * space)
outbytes = struct.unpack(str('iL'), fcntl.ioctl(
outbytes = struct.unpack('iL', fcntl.ioctl(
s.fileno(),
0x8912, # SIOCGIFCONF
struct.pack(str('iL'), space, names.buffer_info()[0])
struct.pack('iL', space, names.buffer_info()[0])
))[0]
namestr = names.tostring()
namestr = names.tobytes()
# return namestr, outbytes
return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)]
@@ -155,7 +155,7 @@ def renameComputer(newName: str) -> bool:
Returns True if reboot needed
'''
rename(newName)
return True # Always reboot right now. Not much slower but much more better
return True # Always reboot right now. Not much slower but much more convenient
def joinDomain(domain: str, ou: str, account: str, password: str, executeInOneStep: bool = False):

View File

@@ -56,6 +56,7 @@ def readConfig() -> types.ActorConfigurationType:
validateCertificate=uds.getboolean('validate', fallback=False),
master_token=uds.get('master_token', None),
own_token=uds.get('own_token', None),
restrict_net=uds.get('restrict_net', None),
pre_command=uds.get('pre_command', None),
runonce_command=uds.get('runonce_command', None),
post_command=uds.get('post_command', None),
@@ -78,6 +79,7 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
writeIfValue(config.actorType, 'type')
writeIfValue(config.master_token, 'master_token')
writeIfValue(config.own_token, 'own_token')
writeIfValue(config.restrict_net, 'restrict_net')
writeIfValue(config.pre_command, 'pre_command')
writeIfValue(config.post_command, 'post_command')
writeIfValue(config.runonce_command, 'runonce_command')

View File

@@ -37,41 +37,51 @@ import typing
import requests
from . import types
from .info import VERSION
from .version import VERSION
# Default public listen port
LISTEN_PORT = 43910
# Default timeout
TIMEOUT = 5 # 5 seconds is more than enought
TIMEOUT = 5 # 5 seconds is more than enought
# Constants
UNKNOWN = 'unknown'
class RESTError(Exception):
ERRCODE = 0
class RESTConnectionError(RESTError):
ERRCODE = -1
# Errors ""raised"" from broker
class RESTInvalidKeyError(RESTError):
ERRCODE = 1
class RESTUnmanagedHostError(RESTError):
ERRCODE = 2
class RESTUserServiceNotFoundError(RESTError):
ERRCODE = 3
class RESTOsManagerError(RESTError):
ERRCODE = 4
# For avoid proxy on localhost connections
NO_PROXY = {
'http': None,
'https': None,
}
UDS_BASE_URL = 'https://{}/uds/rest/'
#
# Basic UDS Api
#
@@ -79,6 +89,7 @@ class UDSApi: # pylint: disable=too-few-public-methods
"""
Base for remote api accesses
"""
_host: str
_validateCert: bool
_url: str
@@ -86,12 +97,12 @@ class UDSApi: # pylint: disable=too-few-public-methods
def __init__(self, host: str, validateCert: bool) -> None:
self._host = host
self._validateCert = validateCert
self._url = "https://{}/uds/rest/".format(self._host)
self._url = UDS_BASE_URL.format(self._host)
# Disable logging requests messages except for errors, ...
logging.getLogger("requests").setLevel(logging.CRITICAL)
logging.getLogger("urllib3").setLevel(logging.ERROR)
logging.getLogger('request').setLevel(logging.CRITICAL)
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
warnings.simplefilter("ignore") # Disables all warnings
warnings.simplefilter('ignore') # Disables all warnings
except Exception:
pass
@@ -99,19 +110,19 @@ class UDSApi: # pylint: disable=too-few-public-methods
def _headers(self) -> typing.MutableMapping[str, str]:
return {
'Content-Type': 'application/json',
'User-Agent': 'UDS Actor v{}'.format(VERSION)
'User-Agent': 'UDS Actor v{}'.format(VERSION),
}
def _apiURL(self, method: str) -> str:
raise NotImplementedError
def _doPost(
self,
method: str, # i.e. 'initialize', 'ready', ....
payLoad: typing.MutableMapping[str, typing.Any],
headers: typing.Optional[typing.MutableMapping[str, str]] = None,
disableProxy: bool = False
) -> typing.Any:
self,
method: str, # i.e. 'initialize', 'ready', ....
payLoad: typing.MutableMapping[str, typing.Any],
headers: typing.Optional[typing.MutableMapping[str, str]] = None,
disableProxy: bool = False,
) -> typing.Any:
headers = headers or self._headers
try:
result = requests.post(
@@ -120,7 +131,9 @@ class UDSApi: # pylint: disable=too-few-public-methods
headers=headers,
verify=self._validateCert,
timeout=TIMEOUT,
proxies=NO_PROXY if disableProxy else None # if not proxies wanted, enforce it
proxies=NO_PROXY # type: ignore
if disableProxy
else None, # if not proxies wanted, enforce it
)
if result.ok:
@@ -139,6 +152,7 @@ class UDSApi: # pylint: disable=too-few-public-methods
raise RESTError(data)
#
# UDS Broker API access
#
@@ -148,7 +162,12 @@ class UDSServerApi(UDSApi):
def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
try:
result = requests.get(self._url + 'auth/auths', headers=self._headers, verify=self._validateCert, timeout=4)
result = requests.get(
self._url + 'auth/auths',
headers=self._headers,
verify=self._validateCert,
timeout=4,
)
if result.ok:
for v in sorted(result.json(), key=lambda x: x['priority']):
yield types.AuthenticatorType(
@@ -157,7 +176,7 @@ class UDSServerApi(UDSApi):
auth=v['auth'],
type=v['type'],
priority=v['priority'],
isCustom=v['isCustom']
isCustom=v['isCustom'],
)
except Exception:
pass
@@ -173,7 +192,7 @@ class UDSServerApi(UDSApi):
preCommand: str,
runOnceCommand: str,
postCommand: str,
logLevel: int
logLevel: int,
) -> str:
"""
Raises an exception if could not register, or registers and returns the "authorization token"
@@ -186,7 +205,7 @@ class UDSServerApi(UDSApi):
'pre_command': preCommand,
'run_once_command': runOnceCommand,
'post_command': postCommand,
'log_level': logLevel
'log_level': logLevel,
}
# First, try to login to REST api
@@ -194,13 +213,23 @@ class UDSServerApi(UDSApi):
# First, try to login
authInfo = {'auth': auth, 'username': username, 'password': password}
headers = self._headers
result = requests.post(self._url + 'auth/login', data=json.dumps(authInfo), headers=headers, verify=self._validateCert)
result = requests.post(
self._url + 'auth/login',
data=json.dumps(authInfo),
headers=headers,
verify=self._validateCert,
)
if not result.ok or result.json()['result'] == 'error':
raise Exception() # Invalid credentials
headers['X-Auth-Token'] = result.json()['token']
result = requests.post(self._apiURL('register'), data=json.dumps(data), headers=headers, verify=self._validateCert)
result = requests.post(
self._apiURL('register'),
data=json.dumps(data),
headers=headers,
verify=self._validateCert,
)
if result.ok:
return result.json()['result']
except requests.ConnectionError as e:
@@ -212,13 +241,18 @@ class UDSServerApi(UDSApi):
raise RESTError(result.content.decode())
def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType], actor_type: typing.Optional[str]) -> types.InitializationResultType:
def initialize(
self,
token: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
actor_type: typing.Optional[str],
) -> types.InitializationResultType:
# Generate id list from netork cards
payload = {
'type': actor_type or types.MANAGED,
'token': token,
'version': VERSION,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces]
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
}
r = self._doPost('initialize', payload)
os = r['os']
@@ -232,53 +266,55 @@ class UDSServerApi(UDSApi):
password=os.get('password'),
new_password=os.get('new_password'),
ad=os.get('ad'),
ou=os.get('ou')
) if r['os'] else None
ou=os.get('ou'),
)
if r['os']
else None,
)
def ready(self, own_token: str, secret: str, ip: str, port: int) -> types.CertificateInfoType:
payload = {
'token': own_token,
'secret': secret,
'ip': ip,
'port': port
}
def ready(
self, own_token: str, secret: str, ip: str, port: int
) -> types.CertificateInfoType:
payload = {'token': own_token, 'secret': secret, 'ip': ip, 'port': port}
result = self._doPost('ready', payload)
return types.CertificateInfoType(
private_key=result['private_key'],
server_certificate=result['server_certificate'],
password=result['password']
password=result['password'],
)
def notifyIpChange(self, own_token: str, secret: str, ip: str, port: int) -> types.CertificateInfoType:
payload = {
'token': own_token,
'secret': secret,
'ip': ip,
'port': port
}
def notifyIpChange(
self, own_token: str, secret: str, ip: str, port: int
) -> types.CertificateInfoType:
payload = {'token': own_token, 'secret': secret, 'ip': ip, 'port': port}
result = self._doPost('ipchange', payload)
return types.CertificateInfoType(
private_key=result['private_key'],
server_certificate=result['server_certificate'],
password=result['password']
password=result['password'],
)
def notifyUnmanagedCallback(self, master_token: str, secret: str, interfaces: typing.Iterable[types.InterfaceInfoType], port: int) -> types.CertificateInfoType:
def notifyUnmanagedCallback(
self,
master_token: str,
secret: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
port: int,
) -> types.CertificateInfoType:
payload = {
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
'token': master_token,
'secret': secret,
'port': port
'port': port,
}
result = self._doPost('unmanaged', payload)
return types.CertificateInfoType(
private_key=result['private_key'],
server_certificate=result['server_certificate'],
password=result['password']
password=result['password'],
)
def login(
@@ -288,14 +324,11 @@ class UDSServerApi(UDSApi):
username: str,
sessionType: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
secret: typing.Optional[str]
secret: typing.Optional[str],
) -> types.LoginResultInfoType:
if not token:
return types.LoginResultInfoType(
ip='0.0.0.0',
hostname=UNKNOWN,
dead_line=None,
max_idle=None
ip='0.0.0.0', hostname=UNKNOWN, dead_line=None, max_idle=None
)
payload = {
'type': actor_type or types.MANAGED,
@@ -310,7 +343,7 @@ class UDSServerApi(UDSApi):
ip=result['ip'],
hostname=result['hostname'],
dead_line=result['dead_line'],
max_idle=result['max_idle']
max_idle=result['max_idle'],
)
def logout(
@@ -318,29 +351,26 @@ class UDSServerApi(UDSApi):
actor_type: typing.Optional[str],
token: str,
username: str,
sessionType: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
secret: typing.Optional[str]
) -> None:
secret: typing.Optional[str],
) -> typing.Optional[str]:
if not token:
return
return None
payload = {
'type': actor_type or types.MANAGED,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
'token': token,
'username': username,
'secret': secret or ''
'session_type': sessionType,
'secret': secret or '',
}
self._doPost('logout', payload)
return self._doPost('logout', payload) # Can be 'ok' or 'notified'
def log(self, own_token: str, level: int, message: str) -> None:
if not own_token:
return
payLoad = {
'token': own_token,
'level': level,
'message': message
}
payLoad = {'token': own_token, 'level': level, 'message': message}
self._doPost('log', payLoad) # Ignores result...
def test(self, master_token: str, actorType: typing.Optional[str]) -> bool:
@@ -359,26 +389,25 @@ class UDSClientApi(UDSApi):
def _apiURL(self, method: str) -> str:
return self._url + method
def post(
self,
method: str, # i.e. 'initialize', 'ready', ....
payLoad: typing.MutableMapping[str, typing.Any]
) -> typing.Any:
self,
method: str, # i.e. 'initialize', 'ready', ....
payLoad: typing.MutableMapping[str, typing.Any],
) -> typing.Any:
return self._doPost(method=method, payLoad=payLoad, disableProxy=True)
def register(self, callbackUrl: str) -> None:
payLoad = {
'callback_url': callbackUrl
}
payLoad = {'callback_url': callbackUrl}
self.post('register', payLoad)
def unregister(self, callbackUrl: str) -> None:
payLoad = {
'callback_url': callbackUrl
}
payLoad = {'callback_url': callbackUrl}
self.post('unregister', payLoad)
def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
def login(
self, username: str, sessionType: typing.Optional[str] = None
) -> types.LoginResultInfoType:
payLoad = {
'username': username,
'session_type': sessionType or UNKNOWN,
@@ -388,12 +417,13 @@ class UDSClientApi(UDSApi):
ip=result['ip'],
hostname=result['hostname'],
dead_line=result['dead_line'],
max_idle=result['max_idle']
max_idle=result['max_idle'],
)
def logout(self, username: str) -> None:
def logout(self, username: str, sessionType: typing.Optional[str]) -> None:
payLoad = {
'username': username
'username': username,
'session_type': sessionType or UNKNOWN
}
self.post('logout', payLoad)

View File

@@ -39,6 +39,7 @@ import typing
from . import platform
from . import rest
from . import types
from . import tools
from .log import logger, DEBUG, INFO, ERROR, FATAL
from .http import clients_pool, server, cert
@@ -55,6 +56,7 @@ from .http import clients_pool, server, cert
# else:
# logger.setLevel(20000)
class CommonService: # pylint: disable=too-many-instance-attributes
_isAlive: bool = True
_rebootRequested: bool = False
@@ -75,7 +77,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.debug('Executing command on {}: {}'.format(section, cmdLine))
res = subprocess.check_call(cmdLine, shell=True)
except Exception as e:
logger.error('Got exception executing: {} - {} - {}'.format(section, cmdLine, e))
logger.error(
'Got exception executing: {} - {} - {}'.format(section, cmdLine, e)
)
return False
logger.debug('Result of executing cmd for {} was {}'.format(section, res))
return True
@@ -86,7 +90,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._api = rest.UDSServerApi(self._cfg.host, self._cfg.validateCertificate)
self._secret = secrets.token_urlsafe(33)
self._clientsPool = clients_pool.UDSActorClientPool()
self._certificate = cert.defaultCertificate # For being used on "unmanaged" hosts only
self._certificate = (
cert.defaultCertificate
) # For being used on "unmanaged" hosts only
self._http = None
# Initialzies loglevel and serviceLogger
@@ -112,16 +118,24 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._http.start()
def isManaged(self) -> bool:
return self._cfg.actorType != types.UNMANAGED # Only "unmanaged" hosts are unmanaged, the rest are "managed"
return (
self._cfg.actorType != types.UNMANAGED
) # Only "unmanaged" hosts are unmanaged, the rest are "managed"
def serviceInterfaceInfo(self, interfaces: typing.Optional[typing.List[types.InterfaceInfoType]] = None) -> typing.Optional[types.InterfaceInfoType]:
def serviceInterfaceInfo(
self, interfaces: typing.Optional[typing.List[types.InterfaceInfoType]] = None
) -> typing.Optional[types.InterfaceInfoType]:
"""
returns the inteface with unique_id mac or first interface or None if no interfaces...
"""
interfaces = interfaces or self._interfaces # Emty interfaces is like "no ip change" because cannot be notified
interfaces = (
interfaces or self._interfaces
) # Emty interfaces is like "no ip change" because cannot be notified
if self._cfg.config and interfaces:
try:
return next(x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id)
return next(
x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id
)
except StopIteration:
return interfaces[0]
@@ -152,7 +166,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes
while self._isAlive:
counter -= 1
try:
self._certificate = self._api.ready(self._cfg.own_token, self._secret, srvInterface.ip, rest.LISTEN_PORT)
self._certificate = self._api.ready(
self._cfg.own_token,
self._secret,
srvInterface.ip,
rest.LISTEN_PORT,
)
except rest.RESTConnectionError as e:
if not logged: # Only log connection problems ONCE
logged = True
@@ -168,7 +187,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Success or any error that is not recoverable (retunerd by UDS). if Error, service will be cleaned in a while.
break
else:
logger.error('Could not locate IP address!!!. (Not registered with UDS)')
logger.error(
'Could not locate IP address!!!. (Not registered with UDS)'
)
# Do not continue if not alive...
if not self._isAlive:
@@ -176,7 +197,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Cleans sensible data
if self._cfg.config:
self._cfg = self._cfg._replace(config=self._cfg.config._replace(os=None), data=None)
self._cfg = self._cfg._replace(
config=self._cfg.config._replace(os=None), data=None
)
platform.store.writeConfig(self._cfg)
logger.info('Service ready')
@@ -195,10 +218,10 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._cfg = self._cfg._replace(runonce_command=None)
platform.store.writeConfig(self._cfg)
if self.execute(runOnce, "runOnce"):
# If runonce is present, will not do anythin more
# So we have to ensure that, when runonce command is finished, reboots the machine.
# That is, the COMMAND itself has to restart the machine!
return False # If the command fails, continue with the rest of the operations...
# If runonce is present, will not do anythin more
# So we have to ensure that, when runonce command is finished, reboots the machine.
# That is, the COMMAND itself has to restart the machine!
return False # If the command fails, continue with the rest of the operations...
# Retry configuration while not stop service, config in case of error 10 times, reboot vm
counter = 10
@@ -208,9 +231,20 @@ class CommonService: # pylint: disable=too-many-instance-attributes
if self._cfg.config and self._cfg.config.os:
osData = self._cfg.config.os
if osData.action == 'rename':
self.rename(osData.name, osData.username, osData.password, osData.new_password)
self.rename(
osData.name,
osData.username,
osData.password,
osData.new_password,
)
elif osData.action == 'rename_ad':
self.joinDomain(osData.name, osData.ad or '', osData.ou or '', osData.username or '', osData.password or '')
self.joinDomain(
osData.name,
osData.ad or '',
osData.ou or '',
osData.username or '',
osData.password or '',
)
if self._rebootRequested:
try:
@@ -234,7 +268,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self.getInterfaces() # Ensure we have interfaces
if self._cfg.master_token:
try:
self._certificate = self._api.notifyUnmanagedCallback(self._cfg.master_token, self._secret, self._interfaces, rest.LISTEN_PORT)
self._certificate = self._api.notifyUnmanagedCallback(
self._cfg.master_token,
self._secret,
self._interfaces,
rest.LISTEN_PORT,
)
except Exception as e:
logger.error('Couuld not notify unmanaged callback: %s', e)
@@ -245,13 +284,17 @@ class CommonService: # pylint: disable=too-many-instance-attributes
return
while self._isAlive:
self._interfaces = list(platform.operations.getNetworkInfo())
self._interfaces = tools.validNetworkCards(
self._cfg.restrict_net, platform.operations.getNetworkInfo()
)
if self._interfaces:
break
self.doWait(5000)
def initialize(self) -> bool:
if self._initialized or not self._cfg.host or not self._isAlive: # Not configured or not running
if (
self._initialized or not self._cfg.host or not self._isAlive
): # Not configured or not running
return False
self._initialized = True
@@ -268,9 +311,15 @@ class CommonService: # pylint: disable=too-many-instance-attributes
try:
# If master token is present, initialize and get configuration data
if self._cfg.master_token:
initResult: types.InitializationResultType = self._api.initialize(self._cfg.master_token, self._interfaces, self._cfg.actorType)
initResult: types.InitializationResultType = self._api.initialize(
self._cfg.master_token, self._interfaces, self._cfg.actorType
)
if not initResult.own_token: # Not managed
logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces))
logger.debug(
'This host is not managed by UDS Broker (ids: {})'.format(
self._interfaces
)
)
return False
# Only removes master token for managed machines (will need it on next client execution)
@@ -279,9 +328,8 @@ class CommonService: # pylint: disable=too-many-instance-attributes
master_token=master_token,
own_token=initResult.own_token,
config=types.ActorDataConfigurationType(
unique_id=initResult.unique_id,
os=initResult.os
)
unique_id=initResult.unique_id, os=initResult.os
),
)
# On first successfull initialization request, master token will dissapear for managed hosts so it will be no more available (not needed anyway)
@@ -294,10 +342,16 @@ class CommonService: # pylint: disable=too-many-instance-attributes
break # Initial configuration done..
except rest.RESTConnectionError as e:
logger.info('Trying to inititialize connection with broker (last error: {})'.format(e))
logger.info(
'Trying to inititialize connection with broker (last error: {})'.format(
e
)
)
self.doWait(5000) # Wait a bit and retry
except rest.RESTError as e: # Invalid key?
logger.error('Error validating with broker. (Invalid token?): {}'.format(e))
except rest.RESTError as e: # Invalid key?
logger.error(
'Error validating with broker. (Invalid token?): {}'.format(e)
)
return False
except Exception:
logger.exception()
@@ -307,7 +361,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
def uninitialize(self):
self._initialized = False
self._cfg = self._cfg._replace(own_token=None) # Ensures assigned token is cleared
self._cfg = self._cfg._replace(
own_token=None
) # Ensures assigned token is cleared
def finish(self) -> None:
if self._http:
@@ -321,8 +377,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._cfg.actorType,
self._cfg.own_token,
'',
'',
self._interfaces,
self._secret
self._secret,
)
except Exception as e:
logger.error('Error notifying final logout to UDS: %s', e)
@@ -334,19 +391,33 @@ class CommonService: # pylint: disable=too-many-instance-attributes
return # Unamanaged hosts does not changes ips. (The full initialize-login-logout process is done in a row, so at login the IP is correct)
try:
if not self._cfg.own_token or not self._cfg.config or not self._cfg.config.unique_id:
if (
not self._cfg.own_token
or not self._cfg.config
or not self._cfg.config.unique_id
):
# Not enouth data do check
return
currentInterfaces = list(platform.operations.getNetworkInfo())
currentInterfaces = tools.validNetworkCards(
self._cfg.restrict_net, platform.operations.getNetworkInfo()
)
old = self.serviceInterfaceInfo()
new = self.serviceInterfaceInfo(currentInterfaces)
if not new or not old:
raise Exception('No ip currently available for {}'.format(self._cfg.config.unique_id))
raise Exception(
'No ip currently available for {}'.format(
self._cfg.config.unique_id
)
)
if old.ip != new.ip:
self._certificate = self._api.notifyIpChange(self._cfg.own_token, self._secret, new.ip, rest.LISTEN_PORT)
self._certificate = self._api.notifyIpChange(
self._cfg.own_token, self._secret, new.ip, rest.LISTEN_PORT
)
# Now store new addresses & interfaces...
self._interfaces = currentInterfaces
logger.info('Ip changed from {} to {}. Notified to UDS'.format(old.ip, new.ip))
logger.info(
'Ip changed from {} to {}. Notified to UDS'.format(old.ip, new.ip)
)
# Stop the running HTTP Thread and start a new one, with new generated cert
self.startHttpServer()
except Exception as e:
@@ -354,29 +425,34 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.warn('Checking ips failed: {}'.format(e))
def rename(
self,
name: str,
userName: typing.Optional[str] = None,
oldPassword: typing.Optional[str] = None,
newPassword: typing.Optional[str] = None
) -> None:
self,
name: str,
userName: typing.Optional[str] = None,
oldPassword: typing.Optional[str] = None,
newPassword: typing.Optional[str] = None,
) -> None:
'''
Invoked when broker requests a rename action
default does nothing
'''
hostName = platform.operations.getComputerName()
if hostName.lower() == name.lower():
logger.info('Computer name is already {}'.format(hostName))
return
# Check for password change request for an user
if userName and newPassword:
logger.info('Setting password for configured user')
try:
platform.operations.changeUserPassword(userName, oldPassword or '', newPassword)
platform.operations.changeUserPassword(
userName, oldPassword or '', newPassword
)
except Exception as e:
raise Exception('Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(userName, str(e)))
# Logs error, but continue renaming computer
logger.error(
'Could not change password for user {}: {}'.format(userName, e)
)
if hostName.lower() == name.lower():
logger.info('Computer name is already {}'.format(hostName))
return
if platform.operations.renameComputer(name):
self.reboot()
@@ -389,7 +465,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Now check if every registered client is already there (if logged in OFC)
if self._loggedIn and not self._clientsPool.ping():
self.logout('client_unavailable')
self.logout('client_unavailable', '')
except Exception as e:
logger.error('Exception on main service loop: %s', e)
@@ -397,13 +473,8 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Methods that can be overriden by linux & windows Actor
# ******************************************************
def joinDomain( # pylint: disable=unused-argument, too-many-arguments
self,
name: str,
domain: str,
ou: str,
account: str,
password: str
) -> None:
self, name: str, domain: str, ou: str, account: str, password: str
) -> None:
'''
Invoked when broker requests a "domain" action
default does nothing
@@ -411,19 +482,24 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.debug('Base join invoked: {} on {}, {}'.format(name, domain, ou))
# Client notifications
def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
result = types.LoginResultInfoType(ip='', hostname='', dead_line=None, max_idle=None)
self._loggedIn = True
def login(
self, username: str, sessionType: typing.Optional[str] = None
) -> types.LoginResultInfoType:
result = types.LoginResultInfoType(
ip='', hostname='', dead_line=None, max_idle=None
)
master_token = None
secret = None
# If unmanaged, do initialization now, because we don't know before this
# Also, even if not initialized, get a "login" notification token
if not self.isManaged():
self.initialize()
self._initialized = (
self.initialize()
) # Maybe it's a local login by an unmanaged host.... On real login, will execute initilize again
# Unamanaged, need the master token
master_token = self._cfg.master_token
secret = self._secret
# Own token will not be set if UDS did not assigned the initialized VM to an user
# In that case, take master token (if machine is Unamanaged version)
token = self._cfg.own_token or master_token
@@ -434,33 +510,43 @@ class CommonService: # pylint: disable=too-many-instance-attributes
username,
sessionType or '',
self._interfaces,
secret
secret,
)
script = platform.store.invokeScriptOnLogin()
if script:
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
self.execute(script, 'Logon')
if result.logged_in:
logger.debug('Login successful')
self._loggedIn = True
script = platform.store.invokeScriptOnLogin()
if script:
logger.info('Executing script on login: {}'.format(script))
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
self.execute(script, 'Logon')
return result
def logout(self, username: str) -> None:
self._loggedIn = False
master_token = self._cfg.master_token if self.isManaged() else None
def logout(self, username: str, sessionType: typing.Optional[str]) -> None:
master_token = self._cfg.master_token
# Own token will not be set if UDS did not assigned the initialized VM to an user
# In that case, take master token (if machine is Unamanaged version)
token = self._cfg.own_token or master_token
if token:
self._api.logout(
self._cfg.actorType,
token,
username,
self._interfaces,
self._secret
)
# If logout is not processed (that is, not ok result), the logout has not been processed
if (
self._api.logout(
self._cfg.actorType,
token,
username,
sessionType or '',
self._interfaces,
self._secret,
)
!= 'ok'
):
logger.info('Logout from %s ignored as required by uds broker', username)
return
self._loggedIn = False
self.onLogout(username)
if not self.isManaged():
@@ -487,13 +573,25 @@ class CommonService: # pylint: disable=too-many-instance-attributes
'''
logger.info('Service stopped')
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str: # pylint: disable=unused-argument
def preConnect(
self, userName: str, protocol: str, ip: str, hostname: str, udsUserName: str
) -> str:
'''
Invoked when received a PRE Connection request via REST
Base preconnect executes the preconnect command
'''
if self._cfg.pre_command:
self.execute(self._cfg.pre_command + ' {} {} {} {}'.format(userName.replace('"', '%22'), protocol, ip, hostname), 'preConnect')
self.execute(
self._cfg.pre_command
+ ' {} {} {} {} {}'.format(
userName.replace('"', '%22'),
protocol,
ip,
hostname,
udsUserName.replace('"', '%22'),
),
'preConnect',
)
return 'ok'

View File

@@ -28,20 +28,58 @@
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
# pylint: disable=invalid-name
import threading
import ipaddress
import typing
if typing.TYPE_CHECKING:
from udsactor.types import InterfaceInfoType
from udsactor.log import logger
class ScriptExecutorThread(threading.Thread):
def __init__(self, script: str) -> None:
super(ScriptExecutorThread, self).__init__()
self.script = script
def run(self) -> None:
from udsactor.log import logger
try:
logger.debug('Executing script: {}'.format(self.script))
exec(self.script, globals(), None) # pylint: disable=exec-used
except Exception as e:
logger.error('Error executing script: {}'.format(e))
logger.exception()
# Convert "X.X.X.X/X" to ipaddress.IPv4Network
def strToNoIPV4Network(net: typing.Optional[str]) -> typing.Optional[ipaddress.IPv4Network]:
if not net: # Empty or None
return None
try:
return ipaddress.IPv4Interface(net).network
except Exception:
return None
def validNetworkCards(
net: typing.Optional[str], cards: typing.Iterable['InterfaceInfoType']
) -> typing.List['InterfaceInfoType']:
try:
subnet = strToNoIPV4Network(net)
except Exception as e:
subnet = None
if subnet is None:
return list(cards)
def isValid(ip: str, subnet: ipaddress.IPv4Network) -> bool:
if not ip:
return False
try:
return ipaddress.IPv4Address(ip) in subnet
except Exception:
return False
return [c for c in cards if isValid(c.ip, subnet)]

View File

@@ -35,6 +35,7 @@ class ActorConfigurationType(typing.NamedTuple):
actorType: typing.Optional[str] = None
master_token: typing.Optional[str] = None
own_token: typing.Optional[str] = None
restrict_net: typing.Optional[str] = None
pre_command: typing.Optional[str] = None
runonce_command: typing.Optional[str] = None
@@ -57,6 +58,10 @@ class LoginResultInfoType(typing.NamedTuple):
dead_line: typing.Optional[int]
max_idle: typing.Optional[int] # Not provided by broker
@property
def logged_in(self) -> bool:
return self.hostname != '' or self.ip != ''
class CertificateInfoType(typing.NamedTuple):
private_key: str
server_certificate: str

View File

@@ -0,0 +1 @@
VERSION = '3.5.0'

View File

@@ -34,7 +34,7 @@ import os
import tempfile
import typing
import servicemanager # pylint: disable=import-error
import servicemanager
# Valid logging levels, from UDS Broker (uds.core.utils.log).
from .. import loglevel
@@ -42,6 +42,7 @@ from .. import loglevel
class LocalLogger: # pylint: disable=too-few-public-methods
linux = False
windows = True
serviceLogger = False
logger: typing.Optional[logging.Logger]

View File

@@ -41,6 +41,8 @@ from .service import UDSActorSvc
def setupRecoverService():
svc_name = UDSActorSvc._svc_name_ # pylint: disable=protected-access
hs = None
hscm = None
try:
hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
@@ -57,9 +59,11 @@ def setupRecoverService():
}
win32service.ChangeServiceConfig2(hs, win32service.SERVICE_CONFIG_FAILURE_ACTIONS, service_failure_actions)
finally:
win32service.CloseServiceHandle(hs)
if hs:
win32service.CloseServiceHandle(hs)
finally:
win32service.CloseServiceHandle(hscm)
if hscm:
win32service.CloseServiceHandle(hscm)
def run() -> None:

View File

@@ -139,7 +139,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
logger.info('Using multiple step join because configuration requests to do so')
self.multiStepJoin(name, domain, ou, account, password)
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str:
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str, udsUserName: str) -> str:
logger.debug('Pre connect invoked')
if protocol == 'rdp': # If connection is not using rdp, skip adding user
@@ -168,7 +168,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
self._user = None
logger.debug('User {} already in group'.format(userName))
return super().preConnect(userName, protocol, ip, hostname)
return super().preConnect(userName, protocol, ip, hostname, udsUserName)
def ovLogon(self, username: str, password: str) -> str:
"""

View File

@@ -2,9 +2,10 @@
# Form implementation generated from reading ui file 'setup-dialog.ui'
#
# Created by: PyQt5 UI code generator 5.13.2
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets

View File

@@ -2,9 +2,10 @@
# Form implementation generated from reading ui file 'setup-dialog-unmanaged.ui'
#
# Created by: PyQt5 UI code generator 5.13.2
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
@@ -14,7 +15,7 @@ class Ui_UdsActorSetupDialog(object):
def setupUi(self, UdsActorSetupDialog):
UdsActorSetupDialog.setObjectName("UdsActorSetupDialog")
UdsActorSetupDialog.setWindowModality(QtCore.Qt.WindowModal)
UdsActorSetupDialog.resize(595, 220)
UdsActorSetupDialog.resize(601, 243)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -34,12 +35,12 @@ class Ui_UdsActorSetupDialog(object):
UdsActorSetupDialog.setModal(True)
self.saveButton = QtWidgets.QPushButton(UdsActorSetupDialog)
self.saveButton.setEnabled(True)
self.saveButton.setGeometry(QtCore.QRect(10, 180, 181, 23))
self.saveButton.setGeometry(QtCore.QRect(10, 210, 181, 23))
self.saveButton.setMinimumSize(QtCore.QSize(181, 0))
self.saveButton.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
self.saveButton.setObjectName("saveButton")
self.closeButton = QtWidgets.QPushButton(UdsActorSetupDialog)
self.closeButton.setGeometry(QtCore.QRect(410, 180, 171, 23))
self.closeButton.setGeometry(QtCore.QRect(410, 210, 171, 23))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -49,11 +50,11 @@ class Ui_UdsActorSetupDialog(object):
self.closeButton.setObjectName("closeButton")
self.testButton = QtWidgets.QPushButton(UdsActorSetupDialog)
self.testButton.setEnabled(False)
self.testButton.setGeometry(QtCore.QRect(210, 180, 181, 23))
self.testButton.setGeometry(QtCore.QRect(210, 210, 181, 23))
self.testButton.setMinimumSize(QtCore.QSize(181, 0))
self.testButton.setObjectName("testButton")
self.layoutWidget = QtWidgets.QWidget(UdsActorSetupDialog)
self.layoutWidget.setGeometry(QtCore.QRect(10, 10, 571, 161))
self.layoutWidget.setGeometry(QtCore.QRect(10, 10, 571, 191))
self.layoutWidget.setObjectName("layoutWidget")
self.formLayout = QtWidgets.QFormLayout(self.layoutWidget)
self.formLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
@@ -84,7 +85,7 @@ class Ui_UdsActorSetupDialog(object):
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.serviceToken)
self.label_loglevel = QtWidgets.QLabel(self.layoutWidget)
self.label_loglevel.setObjectName("label_loglevel")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_loglevel)
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_loglevel)
self.logLevelComboBox = QtWidgets.QComboBox(self.layoutWidget)
self.logLevelComboBox.setFrame(True)
self.logLevelComboBox.setObjectName("logLevelComboBox")
@@ -96,7 +97,13 @@ class Ui_UdsActorSetupDialog(object):
self.logLevelComboBox.setItemText(2, "ERROR")
self.logLevelComboBox.addItem("")
self.logLevelComboBox.setItemText(3, "FATAL")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.logLevelComboBox)
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.logLevelComboBox)
self.label_restrictNet = QtWidgets.QLabel(self.layoutWidget)
self.label_restrictNet.setObjectName("label_restrictNet")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_restrictNet)
self.restrictNet = QtWidgets.QLineEdit(self.layoutWidget)
self.restrictNet.setObjectName("restrictNet")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.restrictNet)
self.label_host.raise_()
self.host.raise_()
self.label_serviceToken.raise_()
@@ -105,6 +112,8 @@ class Ui_UdsActorSetupDialog(object):
self.label_security.raise_()
self.label_loglevel.raise_()
self.logLevelComboBox.raise_()
self.label_restrictNet.raise_()
self.restrictNet.raise_()
self.retranslateUi(UdsActorSetupDialog)
self.logLevelComboBox.setCurrentIndex(1)
@@ -113,6 +122,7 @@ class Ui_UdsActorSetupDialog(object):
self.saveButton.clicked.connect(UdsActorSetupDialog.saveConfig)
self.host.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
self.serviceToken.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
self.restrictNet.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
QtCore.QMetaObject.connectSlotsByName(UdsActorSetupDialog)
def retranslateUi(self, UdsActorSetupDialog):
@@ -139,4 +149,7 @@ class Ui_UdsActorSetupDialog(object):
self.serviceToken.setToolTip(_translate("UdsActorSetupDialog", "UDS user with administration rights (Will not be stored on template)"))
self.serviceToken.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Administrator user on UDS Server.</p><p>Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.</p></body></html>"))
self.label_loglevel.setText(_translate("UdsActorSetupDialog", "Log Level"))
self.label_restrictNet.setText(_translate("UdsActorSetupDialog", "Restrict Net"))
self.restrictNet.setToolTip(_translate("UdsActorSetupDialog", "UDS user with administration rights (Will not be stored on template)"))
self.restrictNet.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Administrator user on UDS Server.</p><p>Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.</p></body></html>"))
from ui import uds_rc

View File

@@ -2,7 +2,7 @@
# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.13.2)
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
#
# WARNING! All changes made in this file will be lost!

View File

@@ -14,6 +14,8 @@ APPSDIR := $(DESTDIR)/usr/share/applications
PYC := $(shell find $(SOURCEDIR) -name '*.py[co]')
CACHES := $(shell find $(SOURCEDIR) -name '__pycache__')
clean:
rm -rf $(PYC) $(CACHES) $(DESTDIR)
install:
@@ -55,16 +57,16 @@ build-appimage:
ifeq ($(DISTRO),x86_64)
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g > appimage.recipe
endif
ifeq ($(DISTRO),armf)
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64\\\|x86_64/armhf/g > appimage.recipe
ifeq ($(DISTRO),armhf)
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64/armhf/g | sed -e s/x86_64/armhf/g > appimage.recipe
endif
ifeq ($(DISTRO),i686)
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64/i386/g | sed -e s/x86_64/i686/g > appimage.recipe
endif
# Ensure all working folders are "clean"
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir appimage-build AppDir
appimage-builder --recipe appimage.recipe
appimage-builder --recipe appimage.recipe --appdir /tmp/UDSClientDir
# Now create dist and move appimage
rm -rf $(DESTDIR)
mkdir -p $(DESTDIR)
@@ -77,4 +79,29 @@ endif
tar czvf ../udsclient3-$(DISTRO)-$(VERSION).tar.gz -C $(DESTDIR) .
# cleanup
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir appimage-build AppDir
build-igel:
rm -rf $(DESTDIR)
mkdir -p $(DESTDIR)
# Calculate the size of the custom partition (15 megas more than the appimage size)
@$(eval APPIMAGE_SIZE=$(shell du -sm UDSClient-$(VERSION)-x86_64.AppImage | cut -f1))
@$(eval APPIMAGE_SIZE=$(shell expr $(APPIMAGE_SIZE) + 15))
cat igel/UDSClient-Profile-template.xml | sed -e s/"_SIZE_"/"$(APPIMAGE_SIZE)M"/g > $(DESTDIR)/UDSClient-Profile.xml
cat igel/UDSClient-template.inf | sed -e s/"_SIZE_"/"$(APPIMAGE_SIZE)M"/g > $(DESTDIR)/UDSClient.inf
cp UDSClient-$(VERSION)-x86_64.AppImage $(DESTDIR)/UDSClient
cp igel/UDSClient.desktop $(DESTDIR)/UDSClient.desktop
cp igel/init.sh $(DESTDIR)/init.sh
tar cjvf $(DESTDIR)/UDSClient.tar.bz2 -C $(DESTDIR) UDSClient UDSClient.desktop init.sh
zip -j ../udsclient3-$(VERSION)-igel.zip $(DESTDIR)/UDSClient-Profile.xml $(DESTDIR)/UDSClient.inf $(DESTDIR)/UDSClient.tar.bz2
cd ..
rm -rf $(DESTDIR)
build-thinpro:
rm -rf $(DESTDIR)
mkdir -p $(DESTDIR)
cp -r thinpro/* $(DESTDIR)
cp UDSClient-$(VERSION)-x86_64.AppImage $(DESTDIR)/UDSClient
tar czvf ../udsclient3-$(VERSION)-thinpro.tar.gz -C $(DESTDIR) .
rm -rf $(DESTDIR)

View File

@@ -43,5 +43,11 @@ make DESTDIR=appimage DISTRO=x86_64 VERSION=${VERSION} build-appimage
make DESTDIR=appimage DISTRO=armhf VERSION=${VERSION} build-appimage
make DESTDIR=appimage DISTRO=i686 VERSION=${VERSION} build-appimage
# Now create igel version
# we need first to create the Appimage for x86_64
make DESTDIR=igelimage DISTRO=x86_64 VERSION=${VERSION} build-igel
# Create the thinpro version
make DESTDIR=thinproimage DISTRO=x86_64 VERSION=${VERSION} build-thinpro
rpm --addsign ../*rpm

View File

@@ -1,26 +1,38 @@
Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
Name: udsclient3
Maintainer: Adolfo Gómez García
Source: http://www.udsenterprise.com/
Source: http://github.com/dkmstr/openuds/client-py3
Copyright: 2014 Virtual Cable S.L.U.
License: BSD-3-clause
Files: *
Copyright: (c) 2014-2022, Virtual Cable S.L.U.
License: 3-BSD
License: GPL-2+
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
.
This program 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 General Public License for more details.
.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
.
On Debian systems, the full text of the GNU General Public
License version 2 can be found in the file
`/usr/share/common-licenses/GPL-2'.
License: 3-BSD
All rights reserved.
.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
.
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
.
* Neither the name of pg_query nor the names of its contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<profile>
<profile_id>1126</profile_id>
<profilename>UDSClient</profilename>
<firmware>
<model>IGEL OS 11</model>
<version>11.05.120.01</version>
</firmware>
<description></description>
<overwritesessions>false</overwritesessions>
<is_master_profile>false</is_master_profile>
<is_igel_os>true</is_igel_os>
<settings>
<pclass name="custom_partition.enabled">
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">true</pvalue>
<variableSubstitutionActive>false</variableSubstitutionActive>
</pclass>
<pclass name="system.security.apparmor">
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">false</pvalue>
<variableSubstitutionActive>false</variableSubstitutionActive>
</pclass>
<pclass name="custom_partition.mountpoint">
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">/UDSClient</pvalue>
<variableSubstitutionActive>false</variableSubstitutionActive>
</pclass>
<pclass name="custom_partition.size">
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">_SIZE_</pvalue>
<variableSubstitutionActive>false</variableSubstitutionActive>
</pclass>
</settings>
<instancesettings>
<instance classprefix="custom_partition.source%" serialnumber="-719cadfe:17ca470644a:-7fff127.0.1.1">
<ivalue classname="custom_partition.source%.autoupdate" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="custom_partition.source%.crypt_password" variableExpression="" variableSubstitutionActive="false">000d4317311f2c0031133c4d3e4c3d</ivalue>
<ivalue classname="custom_partition.source%.final_action" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="custom_partition.source%.init_action" variableExpression="" variableSubstitutionActive="false">/UDSClient/init.sh</ivalue>
<ivalue classname="custom_partition.source%.password" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="custom_partition.source%.url" variableExpression="" variableSubstitutionActive="false">https://[UMS_SERVER]:8443/ums_filetransfer/UDSClient-igel.inf</ivalue>
<ivalue classname="custom_partition.source%.username" variableExpression="" variableSubstitutionActive="false">[UMS_USERNAME]</ivalue>
</instance>
<instance classprefix="sessions.chromium%" serialnumber="-6b5264e9:17ca6f65505:-8000127.0.1.1">
<ivalue classname="sessions.chromium%.app.browser_startup_page" variableExpression="" variableSubstitutionActive="false">1</ivalue>
<ivalue classname="sessions.chromium%.app.homepage" variableExpression="" variableSubstitutionActive="false">https://demo.udsenterprise.com</ivalue>
<ivalue classname="sessions.chromium%.applaunch" variableExpression="" variableSubstitutionActive="false">true</ivalue>
<ivalue classname="sessions.chromium%.applaunch_path" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="sessions.chromium%.applaunch_system" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.autostart" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.autostartnotify" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.desktop" variableExpression="" variableSubstitutionActive="false">true</ivalue>
<ivalue classname="sessions.chromium%.desktop_path" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="sessions.chromium%.hotkey" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="sessions.chromium%.hotkeymodifier" variableExpression="" variableSubstitutionActive="false">None</ivalue>
<ivalue classname="sessions.chromium%.icon" variableExpression="" variableSubstitutionActive="false">chromium</ivalue>
<ivalue classname="sessions.chromium%.menu_path" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="sessions.chromium%.name" variableExpression="UDS" variableSubstitutionActive="true">###LOC_DEFAULT###</ivalue>
<ivalue classname="sessions.chromium%.position" variableExpression="" variableSubstitutionActive="false">0</ivalue>
<ivalue classname="sessions.chromium%.pulldown" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.pwprotected" variableExpression="" variableSubstitutionActive="false">none</ivalue>
<ivalue classname="sessions.chromium%.quick_start" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.scardautostart" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.snotify" variableExpression="" variableSubstitutionActive="false">true</ivalue>
<ivalue classname="sessions.chromium%.startmenu" variableExpression="" variableSubstitutionActive="false">true</ivalue>
<ivalue classname="sessions.chromium%.startmenu_system" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.usehotkey" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.waittime2autostart" variableExpression="" variableSubstitutionActive="false">0</ivalue>
<ivalue classname="sessions.chromium%.waittime2restart" variableExpression="" variableSubstitutionActive="false">0</ivalue>
</instance>
</instancesettings>
</profile>

View File

@@ -0,0 +1,7 @@
[INFO]
[PART]
file="UDSClient.tar.bz2"
version="1.1_igel1"
size="_SIZE_"
name="UDSClient"
minfw="11.05.120"

View File

@@ -0,0 +1,11 @@
[Desktop Entry]
Name=UDSClient
Comment=UDS Helper
Keywords=uds;client;vdi;
Exec=/UDSClient/UDSClient %u
Icon=help-browser
StartupNotify=true
Terminal=false
Type=Application
Categories=Utility;
MimeType=x-scheme-handler/uds;x-scheme-handler/udss;

View File

@@ -0,0 +1,2 @@
#!/bin/sh
cp /UDSClient/UDSClient.desktop /usr/share/applications.mime

View File

@@ -8,6 +8,8 @@ echo "Installation process done."
echo "Remember that the following packages must be installed on system:"
echo "* Python3 paramiko"
echo "* Python3 PyQt5"
echo "* Python3 six"
echo "* Python3 requests"
echo "* Python3 cryptography"
echo "Theese packages (as their names), are dependent on your platform, so you must locate and install them"
echo "Also, ensure that a /media folder exists on your machine, that will be redirected on RDP connections"

View File

@@ -0,0 +1,4 @@
# UDS handlers.json
cp "/lib/UDSClient/firefox/handlers.json" "$FIREFOX_PROFILE_HANDLERS"
ffset "network.protocol-handler.external.uds" "true"
ffset "network.protocol-handler.external.udss" "true"

View File

@@ -0,0 +1,98 @@
{
"defaultHandlersVersion": {
"en-US": 4
},
"mimeTypes": {
"application/pdf": {
"action": 3,
"extensions": [
"pdf"
]
},
"application/x-ica": {
"action": 2,
"extensions": [
"ica"
],
"handlers": [
{
"name": "wfica",
"path": "/usr/share/hptc-firefox-mgr/handlers/citrix"
}
]
},
"application/x-rdp": {
"action": 2,
"extensions": [
"rdp"
],
"handlers": [
{
"name": "HP xfreerdp",
"path": "/usr/share/hptc-firefox-mgr/handlers/rdp"
}
]
},
"text/lic": {
"action": 2,
"extensions": [
"lic"
],
"handlers": [
{
"name": "Copy license to ThinPro",
"path": "/usr/share/hptc-firefox-mgr/handlers/copy_lic"
}
]
},
"text/xml": {
"action": 3,
"extensions": [
"xml"
]
},
"image/svg+xml": {
"action": 3,
"extensions": [
"svg"
]
},
"image/webp": {
"action": 3,
"extensions": [
"webp"
]
}
},
"schemes": {
"vmware-view": {
"action": 2,
"handlers": [
{
"name": "VMWare Horizon View",
"path": "/usr/share/hptc-firefox-mgr/handlers/vmware"
}
]
},
"uds": {
"action": 2,
"handlers": [
{
"name": "UDS Client for ThinPro (SSL)",
"path": "/usr/share/hptc-firefox-mgr/handlers/uds"
}
]
},
"udss": {
"action": 2,
"handlers": [
{
"name": "UDS Client for ThinPro",
"path": "/usr/share/hptc-firefox-mgr/handlers/uds"
}
]
}
}
}

View File

@@ -0,0 +1,5 @@
#!/bin/sh
export LD_PRELOAD=""
/bin/udsclient $*
exit 0

View File

@@ -0,0 +1,2 @@
# UDS handlers.json
restore "/lib/UDSClient/firefox/handlers.json" "$FIREFOX_PROFILE_HANDLERS"

View File

@@ -0,0 +1,50 @@
{
"defaultHandlersVersion":{
"en-US":4
},
"mimeTypes":{
"application/pdf":{
"action":3,
"extensions":["pdf"]
},
"application/x-ica":{
"action":2,
"handlers":[{
"name":"wfica",
"path":"/usr/bin/hptc-firefox-run-wfica.sh"
}],
"extensions":["ica"]
},
"application/x-rdp":{
"action":2,
"handlers":[{
"name":"HP xfreerdp",
"path":"/usr/bin/hptc-run-rdp-file-freerdp.sh"
}],
"extensions":["rdp"]
}
},
"schemes":{
"vmware-view":{
"action":2,
"handlers":[{
"name":"VMWare Horizon View",
"path":"/usr/bin/vmware-view"
}]
},
"udss":{
"action":2,
"handlers":[{
"name":"UDS Client",
"path":"/bin/udsclient"
}]
},
"uds":{
"action":2,
"handlers":[{
"name":"UDS Client",
"path":"/bin/udsclient"
}]
}
}
}

View File

@@ -0,0 +1,37 @@
// This file can be used to configure global preferences for Firefox
// Example: Homepage
//pref("browser.startup.homepage", "http://www.weebls-stuff.com/wab/");
pref("plugin.default.state", 2);
pref("xpinstall.signatures.required", false, locked);
pref("extensions.autoDisableScopes", 0, locked);
pref("extensions.pocket.enabled", false, locked);
pref("extensions.screenshots.disabled", true, locked);
pref("datareporting.policy.dataSubmissionEnabled", false, locked);
pref("datareporting.policy.dataSubmissionEnabled.v2", false, locked);
pref("app.update.auto", false, locked);
pref("app.update.enabled", false, locked);
pref("browser.download.manager.closeWhenDone", true, locked);
pref("browser.helperApps.neverAsk.openFile", "application/x-rdp, application/x-java-jnlp-file", locked);
pref("browser.EULA.3.accepted", true, locked);
pref("browser.rights.3.shown", true, locked);
pref("browser.safebrowsing.enabled", false, locked);
pref("browser.search.update", false, locked);
pref("browser.sessionstore.enabled", false, locked);
pref("browser.sessionhistory.cache_subframes", false, locked);
pref("datareporting.healthreport.service.enabled", false, locked);
pref("datareporting.healthreport.uploadEnabled", false, locked);
pref("devtools.toolbox.host", "none", locked);
pref("extensions.autoDisableScopes", 14, locked);
pref("extensions.blocklist.enabled", false, locked);
pref("extensions.update.enabled", false, locked);
pref("intl.charsetmenu.browser.cache", "UTF-8", locked);
pref("network.protocol-handler.external.mailto", false, locked);
pref("network.protocol-handler.external.news", false, locked);
pref("network.protocol-handler.external.snews", false, locked);
pref("network.protocol-handler.external.nntp", false, locked);
pref("network.protocol-handler.external-default", false, locked);
pref("network.protocol-handler.external.vmware-view", true, locked);
pref("network.protocol-handler.external.uds", true, locked);
pref("network.protocol-handler.external.udss", true, locked);

View File

@@ -0,0 +1,38 @@
#!/bin/sh
# Common part
# unlocks so we can write on TC
fsunlock
cp UDSClient /bin/udsclient
chmod 755 /bin/udsclient
# RDP Script for UDSClient. Launchs udsclient using the "Template_UDS" profile
cp udsrdp /usr/bin
INSTALLED=0
# Installation for 7.1.x version
grep -q "7.1" /etc/issue
if [ $? -eq 0 ]; then
echo "Installing for thinpro version 7.1"
# Allow UDS apps without asking
cp firefox7.1/syspref.js /etc/firefox
# Copy handlers.json for firefox
mkdir -p /lib/UDSClient/firefox/ > /dev/null 2>&1
cp firefox7.1/handlers.json /lib/UDSClient/firefox/
# and runner
cp firefox7.1/45-uds /etc/hptc-firefox-mgr/prestart
else
echo "Installing for thinpro version 7.2 or later"
# Copy handlers for firefox
mkdir -p /lib/UDSClient/firefox/ > /dev/null 2>&1
# Copy handlers.json for firefox
cp firefox/handlers.json /lib/UDSClient/firefox/
cp firefox/45-uds /etc/hptc-firefox-mgr/prestart
# copy uds handler for firefox
cp firefox/uds /usr/share/hptc-firefox-mgr/handlers/uds
chmod 755 /usr/share/hptc-firefox-mgr/handlers/uds
fi
# Common part
fslock

View File

@@ -0,0 +1,390 @@
#!/bin/bash
function clearParams {
mclient set $REGKEY/address ""
mclient set $REGKEY/username ""
mclient set $REGKEY/password ""
mclient set $REGKEY/domain ""
mclient set $REGKEY/authorizations/user/execution 0
mclient commit
}
function getRegKey {
# Get Template_UDS
for key in `mclient get root/ConnectionType/freerdp/connections | sed "s/dir //g"`; do
val=`mclient get $key/label | sed "s/value //g"`
if [ "$val" == "Template_UDS" ]; then
REGKEY=$key
fi
done
}
function createUDSConnectionTemplate {
TMPFILE=$(mktemp /tmp/udsexport.XXXXXX)
cat > $TMPFILE << EOF
<Profile>
<ProfileSettings>
<Name>UDS Template Profile</Name>
<RegistryRoot>root/ConnectionType/freerdp/connections/{ff064bd9-047a-45ec-b70f-04ab218186ff}</RegistryRoot>
<Target>
<Hardware>t420</Hardware>
<ImageId>T7X62022</ImageId>
<Version>6.2.0</Version>
<Config>standard</Config>
</Target>
</ProfileSettings>
<ProfileRegistry>
<NodeDir name="{ff064bd9-047a-45ec-b70f-04ab218186ff}">
<NodeDir name="rdWebFeed">
<NodeKey name="keepResourcesWindowOpened">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="autoStartSingleResource">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="autoDisconnectTimeout">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
</NodeDir>
<NodeDir name="loginfields">
<NodeKey name="username">
<NodeParam name="value">3</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="rememberme">
<NodeParam name="value">2</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="password">
<NodeParam name="value">3</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="domain">
<NodeParam name="value">3</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
</NodeDir>
<NodeDir name="authorizations">
<NodeDir name="user">
<NodeKey name="execution">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="edit">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
</NodeDir>
</NodeDir>
<NodeKey name="address">
<NodeParam name="value"/>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="username">
<NodeParam name="value"/>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="password">
<NodeParam name="value">NLCR.1</NodeParam>
<NodeParam name="type">rc4</NodeParam>
</NodeKey>
<NodeKey name="domain">
<NodeParam name="value"/>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="label">
<NodeParam name="value">Template_UDS</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="credentialsType">
<NodeParam name="value">password</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="gatewayEnabled">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="gatewayPort">
<NodeParam name="value">443</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="gatewayUsesSameCredentials">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="gatewayCredentialsType">
<NodeParam name="value">password</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="remoteDesktopService">
<NodeParam name="value">Remote Computer</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="windowMode">
<NodeParam name="value">Remote Application</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="seamlessWindow">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="windowType">
<NodeParam name="value">full</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="windowSizePercentage">
<NodeParam name="value">70</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="windowSizeWidth">
<NodeParam name="value">1024</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="windowSizeHeight">
<NodeParam name="value">768</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="mouseMotionEvents">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="compression">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="rdpEncryption">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="offScreenBitmaps">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="attachToConsole">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="clipboardExtension">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="rdp6Buffering">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="rdpProgressiveCodec">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="securityLevel">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="tlsVersion">
<NodeParam name="value">auto</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="sound">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="printerMapping">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="portMapping">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="usbStorageRedirection">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="localPartitionRedirection">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="scRedirection">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="usbMiscRedirection">
<NodeParam name="value">2</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagNoWallpaper">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagFontSmoothing">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagDesktopComposition">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagNoWindowDrag">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagNoMenuAnimations">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagNoTheming">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="timeoutsEnabled">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="timeoutWarning">
<NodeParam name="value">6000</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="timeoutWarningDialog">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="timeoutRecovery">
<NodeParam name="value">30000</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="timeoutError">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="showRDPDashboard">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="showConnectionGraph">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="x11Synchronous">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="x11Logging">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="x11LogAutoflush">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="x11Capture">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="SingleSignOn">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="autostart">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="waitForNetwork">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="hasDesktopIcon">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="autoReconnect">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
</NodeDir>
</ProfileRegistry>
<ProfileFiles/>
</Profile>
EOF
mclient import $TMPFILE
rm $TMPFILE
}
ADDRESS=
USERNAME=
PASSWORD=
DOMAIN=
REGKEY=
CLEAR=0
# Try to locate registry key for UDS Template
getRegKey
if [ "$REGKEY" == "" ]; then
# Not found, create on based on our template
createUDSConnectionTemplate
getRegKey
fi
for param in $@; do
if [ "/u:" == "${param:0:3}" ]; then
USERNAME=${param:3}
CLEAR=1
fi
if [ "/p:" == "${param:0:3}" ]; then
PASSWORD=${param:3}
CLEAR=1
fi
if [ "/d:" == "${param:0:3}" ]; then
DOMAIN=${param:3}
CLEAR=1
fi
if [ "/v:" == "${param:0:3}" ]; then
ADDRESS=${param:3}
CLEAR=1
fi
done
if [ "$CLEAR" -eq 1 ]; then
clearParams
fi
ID=`basename $REGKEY`
RESPAWN=0
if [ "" != "$ADDRESS" ]; then
mclient set $REGKEY/address "${ADDRESS}"
RESPAWN=1
fi
if [ "" != "$USERNAME" ]; then
mclient set $REGKEY/username "${USERNAME}"
RESPAWN=1
fi
if [ "" != "$PASSWORD" ]; then
mclient set $REGKEY/password "${PASSWORD}"
RESPAWN=1
fi
if [ "" != "$DOMAIN" ]; then
mclient set $REGKEY/domain "${DOMAIN}"
RESPAWN=1
fi
if [ "$RESPAWN" -eq 1 ]; then
mclient set $REGKEY/authorizations/user/execution 1
mclient commit
exec $0 # Restart without command line
fi
process-connection $ID
clearParams

View File

@@ -1,18 +1,17 @@
version: 1
script:
# Remove any previous build
- rm -rf /tmp/UDSClientDir | true
- rm -rf $TARGET_APPDIR | true
# Make usr and icons dirs
- mkdir -p /tmp/UDSClientDir/usr/src
- mkdir -p $TARGET_APPDIR/usr/src
# Copy the python application code into the UDSClientDir
- cp ../src/UDS*.py /tmp/UDSClientDir/usr/src
- cp -r ../src/uds /tmp/UDSClientDir/usr/src
- cp ../src/UDS*.py $TARGET_APPDIR/usr/src
- cp -r ../src/uds $TARGET_APPDIR/usr/src
# Remove __pycache__ and .mypy if exists
- rm /tmp/UDSClientDir/usr/src/.mypy_cache -rf 2>&1 > /dev/null
- rm /tmp/UDSClientDir/usr/src/uds/.mypy_cache -rf 2>&1 > /dev/null
- rm /tmp/UDSClientDir/usr/src/__pycache__ -rf 2>&1 > /dev/null
- rm /tmp/UDSClientDir/usr/src/uds/__pycache__ -rf 2>&1 > /dev/null
- rm $TARGET_APPDIR/usr/src/.mypy_cache -rf 2>&1 > /dev/null
- rm $TARGET_APPDIR/usr/src/uds/.mypy_cache -rf 2>&1 > /dev/null
- rm $TARGET_APPDIR/usr/src/__pycache__ -rf 2>&1 > /dev/null
- rm $TARGET_APPDIR/usr/src/uds/__pycache__ -rf 2>&1 > /dev/null
AppDir:
# On /tmp, that is an ext4 filesystem. On btrfs squashfs complains with "Unrecognised xattr prefix btrfs.compression"
path: /tmp/UDSClientDir
@@ -31,7 +30,7 @@ AppDir:
arch: amd64
sources:
- sourceline: 'deb [arch=amd64] http://ftp.de.debian.org/debian/ bullseye main contrib non-free'
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x04EE7237B7D453EC'
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x648ACFD622F3D138'
include:
- python3
@@ -57,6 +56,6 @@ AppDir:
PYTHONPATH: '${APPDIR}/usr/lib/python3.9/site-packages'
AppImage:
update-information: None
sign-key: None
# update-information: None
sign-key: 592AF43A64B8559137FA2458AA4ECFEE784E6BA7
arch: x86_64

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env -S python3 -s
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2021 Virtual Cable S.L.U.
@@ -44,10 +44,10 @@ from PyQt5.QtCore import QSettings
from uds.rest import RestApi, RetryException, InvalidVersion, UDSException
# Just to ensure there are available on runtime
from uds.forward import forward # type: ignore
from uds.tunnel import forward as f2 # type: ignore
from uds.forward import forward as ssh_forward # type: ignore
from uds.tunnel import forward as tunnel_forwards # type: ignore
from uds.log import logger, DEBUG
from uds.log import logger
from uds import tools
from uds import VERSION
@@ -151,6 +151,8 @@ class UDSClient(QtWidgets.QMainWindow):
return
except Exception as e:
self.showError(e)
self.closeWindow()
return
self.getTransportData()
@@ -176,8 +178,6 @@ class UDSClient(QtWidgets.QMainWindow):
# Retry operation in ten seconds
QtCore.QTimer.singleShot(10000, self.getTransportData)
except Exception as e:
if DEBUG:
logger.exception('Got exception on getTransportData')
self.showError(e)
def start(self):
@@ -316,12 +316,11 @@ def minimal(api: RestApi, ticket: str, scrambler: str):
return 0
if __name__ == "__main__":
def main(args: typing.List[str]):
app = QtWidgets.QApplication(sys.argv)
logger.debug('Initializing connector for %s(%s)', sys.platform, platform.machine())
# Initialize app
app = QtWidgets.QApplication(sys.argv)
logger.debug('Arguments: %s', args)
# Set several info for settings
QtCore.QCoreApplication.setOrganizationName('Virtual Cable S.L.U.')
QtCore.QCoreApplication.setApplicationName('UDS Connector')
@@ -343,11 +342,11 @@ if __name__ == "__main__":
# First parameter must be url
useMinimal = False
try:
uri = sys.argv[1]
uri = args[1]
if uri == '--minimal':
useMinimal = True
uri = sys.argv[2] # And get URI
uri = args[2] # And get URI
if uri == '--test':
sys.exit(0)
@@ -362,8 +361,8 @@ if __name__ == "__main__":
'ssl:%s, host:%s, ticket:%s, scrambler:%s',
ssl,
host,
UDSClient.ticket,
UDSClient.scrambler,
ticket,
scrambler,
)
except Exception:
logger.debug('Detected execution without valid URI, exiting')
@@ -392,7 +391,7 @@ if __name__ == "__main__":
win.start()
exitVal = app.exec_()
exitVal = app.exec()
logger.debug('Execution finished correctly')
except Exception as e:
@@ -404,3 +403,6 @@ if __name__ == "__main__":
logger.debug('Exiting')
sys.exit(exitVal)
if __name__ == "__main__":
main(sys.argv)

View File

@@ -0,0 +1,75 @@
import sys
import os.path
import subprocess
import typing
from uds.log import logger
import UDSClient
from UDSLauncherMac import Ui_MacLauncher
from PyQt5 import QtCore, QtWidgets, QtGui
SCRIPT_NAME = 'UDSClientLauncher'
class UdsApplication(QtWidgets.QApplication):
path: str
tunnels: typing.List[subprocess.Popen]
def __init__(self, argv: typing.List[str]) -> None:
super().__init__(argv)
self.path = os.path.join(os.path.dirname(sys.argv[0]).replace('Resources', 'MacOS'), SCRIPT_NAME)
self.tunnels = []
self.lastWindowClosed.connect(self.closeTunnels) # type: ignore
def cleanTunnels(self) -> None:
def isRunning(p: subprocess.Popen):
try:
if p.poll() is None:
return True
except Exception as e:
logger.debug('Got error polling subprocess: %s', e)
return False
for k in [i for i, tunnel in enumerate(self.tunnels) if not isRunning(tunnel)]:
try:
del self.tunnels[k]
except Exception as e:
logger.debug('Error closing tunnel: %s', e)
def closeTunnels(self) -> None:
logger.debug('Closing remaining tunnels')
for tunnel in self.tunnels:
logger.debug('Checking %s - "%s"', tunnel, tunnel.poll())
if tunnel.poll() is None: # Running
logger.info('Found running tunnel %s, closing it', tunnel.pid)
tunnel.kill()
def event(self, evnt: QtCore.QEvent) -> bool:
if evnt.type() == QtCore.QEvent.FileOpen:
fe = typing.cast(QtGui.QFileOpenEvent, evnt)
logger.debug('Got url: %s', fe.url().url())
fe.accept()
logger.debug('Spawning %s', self.path)
# First, remove all finished tunnel processed from check queue
self.cleanTunnels()
# And now add a new one
self.tunnels.append(subprocess.Popen([self.path, fe.url().url()]))
return super().event(evnt)
def main(args: typing.List[str]):
if len(args) > 1:
UDSClient.main(args)
else:
app = UdsApplication(sys.argv)
window = QtWidgets.QMainWindow()
Ui_MacLauncher().setupUi(window)
window.showMinimized()
sys.exit(app.exec())
if __name__ == "__main__":
main(args=sys.argv)

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'UDSLauncherMac.ui'
#
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MacLauncher(object):
def setupUi(self, MacLauncher):
MacLauncher.setObjectName("MacLauncher")
MacLauncher.setWindowModality(QtCore.Qt.NonModal)
MacLauncher.resize(235, 120)
MacLauncher.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/images/logo-uds-small"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
MacLauncher.setWindowIcon(icon)
MacLauncher.setWindowOpacity(1.0)
self.centralwidget = QtWidgets.QWidget(MacLauncher)
self.centralwidget.setAutoFillBackground(True)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout_2.setContentsMargins(4, 4, 4, 4)
self.verticalLayout_2.setSpacing(4)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.frame = QtWidgets.QFrame(self.centralwidget)
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
self.frame.setObjectName("frame")
self.verticalLayout = QtWidgets.QVBoxLayout(self.frame)
self.verticalLayout.setObjectName("verticalLayout")
self.topLabel = QtWidgets.QLabel(self.frame)
self.topLabel.setTextFormat(QtCore.Qt.RichText)
self.topLabel.setObjectName("topLabel")
self.verticalLayout.addWidget(self.topLabel)
self.image = QtWidgets.QLabel(self.frame)
self.image.setMinimumSize(QtCore.QSize(0, 32))
self.image.setAutoFillBackground(True)
self.image.setText("")
self.image.setPixmap(QtGui.QPixmap(":/images/logo-uds-small"))
self.image.setScaledContents(False)
self.image.setAlignment(QtCore.Qt.AlignCenter)
self.image.setObjectName("image")
self.verticalLayout.addWidget(self.image)
self.label_2 = QtWidgets.QLabel(self.frame)
self.label_2.setTextFormat(QtCore.Qt.RichText)
self.label_2.setObjectName("label_2")
self.verticalLayout.addWidget(self.label_2)
self.verticalLayout_2.addWidget(self.frame)
MacLauncher.setCentralWidget(self.centralwidget)
self.retranslateUi(MacLauncher)
QtCore.QMetaObject.connectSlotsByName(MacLauncher)
def retranslateUi(self, MacLauncher):
_translate = QtCore.QCoreApplication.translate
MacLauncher.setWindowTitle(_translate("MacLauncher", "UDS Launcher"))
self.topLabel.setText(_translate("MacLauncher", "<html><head/><body><p align=\"center\"><span style=\" font-size:12pt; font-weight:600;\">UDS Launcher</span></p></body></html>"))
self.label_2.setText(_translate("MacLauncher", "<html><head/><body><p align=\"center\"><span style=\" font-size:6pt;\">Closing this window will end all UDS tunnels</span></p></body></html>"))
import UDSResources_rc
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MacLauncher = QtWidgets.QMainWindow()
ui = Ui_MacLauncher()
ui.setupUi(MacLauncher)
MacLauncher.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MacLauncher</class>
<widget class="QMainWindow" name="MacLauncher">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>235</width>
<height>120</height>
</rect>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="windowTitle">
<string>UDS Launcher</string>
</property>
<property name="windowIcon">
<iconset resource="UDSResources.qrc">
<normaloff>:/images/logo-uds-small</normaloff>:/images/logo-uds-small</iconset>
</property>
<property name="windowOpacity">
<double>1.000000000000000</double>
</property>
<widget class="QWidget" name="centralwidget">
<property name="autoFillBackground">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="topLabel">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-size:12pt; font-weight:600;&quot;&gt;UDS Launcher&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="image">
<property name="minimumSize">
<size>
<width>0</width>
<height>32</height>
</size>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="pixmap">
<pixmap resource="UDSResources.qrc">:/images/logo-uds-small</pixmap>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-size:6pt;&quot;&gt;Closing this window will end all UDS tunnels&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="UDSResources.qrc"/>
</resources>
<connections/>
</ui>

View File

@@ -2,158 +2,13 @@
# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.13.2)
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore
qt_resource_data = b"\
\x00\x00\x08\xed\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x25\x00\x00\x00\x30\x08\x06\x00\x00\x00\x96\x85\xb3\x2b\
\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\
\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\
\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\
\xdf\x03\x1b\x0e\x10\x3b\x1e\x53\x78\x6b\x00\x00\x08\x7a\x49\x44\
\x41\x54\x58\xc3\xed\x98\x7b\x70\x54\xf5\x15\xc7\x3f\xbf\xfb\xd8\
\x7b\xb3\x79\x42\x70\x09\x08\xa8\xa5\x20\x54\x4a\xaa\xa5\x6b\xad\
\x81\xfa\x18\xf1\x3a\xad\x53\x67\xaa\x16\xad\xd1\x5a\x75\xc4\x19\
\xa7\x63\xeb\xab\x1a\x94\x91\x91\xaa\xb5\x76\x3a\xd3\x97\xa9\x75\
\x98\x96\xd2\x86\x19\x5b\x47\x85\x61\xd5\x1a\x35\xc1\x16\x57\xa8\
\xe1\xfd\x0e\x04\x03\x92\x9b\x40\x4c\x76\x37\xd9\xec\x7d\xfc\xfa\
\xc7\xde\xd0\x4d\xd8\x4d\x42\xa1\xd3\x3f\xf4\xcc\xec\x24\xfb\xdb\
\x73\xcf\xef\xfb\x3b\xe7\x7b\xce\x3d\xbf\x03\x9f\xc9\x67\xf2\x7f\
\x94\x58\x3c\x72\x5a\xcf\x5f\xb1\xc8\xe4\x8a\x45\xe6\xa8\x7a\x62\
\x2c\x40\xac\xa8\x9d\xfb\xdd\x00\xa6\x07\x9f\xb3\x81\xb3\x1c\x47\
\x96\xef\xde\xe3\x86\xf7\xb7\xba\x66\x67\x97\x27\x92\x29\xc9\xc0\
\x80\x74\x84\xa0\x17\xf8\x18\x68\x03\xb6\x34\x36\xa4\xf7\xe4\x02\
\x6c\x6c\x48\x8f\x1d\xd4\x20\x90\xc0\x33\xa5\xc0\xc5\xc0\x0d\xc0\
\x95\x01\x98\xec\xc3\x02\x76\xed\x76\x59\xf7\x16\x74\xa7\x2a\xf1\
\xb4\x08\x52\x14\x81\x00\x55\xf1\xd0\x45\x0f\xaa\xdf\x83\xf0\xd3\
\x08\xd9\x8f\xe2\xf7\xf5\x80\xf7\x5b\x50\x7f\xd7\xd8\x90\x3e\x50\
\x08\xd8\x88\x9e\x7a\x73\xd3\xc4\x99\x9e\x27\x7f\x13\x80\x39\x49\
\x14\x05\x7e\xf9\x82\x4a\x4b\xea\x01\x12\xe6\xe5\xf8\x98\x20\x3d\
\x94\xa2\x32\xb4\x8a\x89\x08\xc3\x40\x75\x93\x18\x99\x7d\x14\xa5\
\x37\x52\x92\x7a\x9b\xe2\x54\x13\x9a\xdb\x71\x50\x0a\xf5\xa6\xc6\
\x86\xf4\x86\x7c\xc0\x46\x02\x25\x5e\x6e\x9a\xb0\x24\x5c\xa4\x2c\
\x93\xb2\x80\x82\x80\xf8\xf6\x49\xfc\xb4\x79\x15\xaa\xec\x47\xe0\
\xa2\x16\x8f\x43\x1b\x37\x29\x8b\x58\x4a\x40\x20\x85\x0a\x68\x80\
\x47\x79\xe2\x35\xaa\x3a\x1e\x46\x73\x0f\xef\x01\x65\x4e\x63\x43\
\xda\x19\x6e\x57\x2d\x84\x68\x42\x0d\xea\x79\x53\x94\xab\xcb\x4a\
\xd4\x1a\xd3\x28\x8c\x7d\x62\xe5\x00\x42\xfa\x6c\x6d\xaf\x46\x33\
\x34\xb4\xca\x29\x08\x45\x19\x76\x72\x1f\x81\x8b\x40\x92\x36\x2e\
\x20\xe4\xb4\x53\x94\x6e\xa9\x04\xd9\x74\x60\x9b\xdb\x7a\x52\x04\
\x0a\x6d\xd6\xb5\x1e\x6f\xdb\x9e\xf4\xe6\xdd\xad\x03\x8e\xeb\x15\
\x76\xa7\xa6\x38\x7c\xb3\xfa\xaf\x5c\x32\xbd\x09\x69\x54\x9c\x04\
\x68\xa8\x64\x3d\x97\x36\x2f\xc0\x57\x0c\x80\x0b\xf3\xd2\x62\x24\
\x4e\xe9\x21\x5e\xde\xba\xab\x7f\xed\xc1\xf6\xcc\x88\x19\x5a\x56\
\xd4\x43\xed\x25\x2f\x32\x6d\xdc\xfe\xec\xbe\xa3\xa5\xbc\xf4\x72\
\x51\x8e\x1d\x54\x6d\x9d\xce\xaa\xe5\xee\x80\xa6\xb1\xe4\x9f\x9b\
\x52\x6d\x76\x97\x4b\x21\x27\x78\xbe\xca\xb4\xca\x83\xdc\xf1\x95\
\x67\xa8\x30\xbb\xf0\x65\xe1\xb3\x4a\xa1\xa0\xbb\xed\x08\x99\x01\
\xd8\x7d\x4a\xa0\x56\x2e\x77\xb2\xc0\x9e\x72\xb7\xfb\x52\xd6\x35\
\x6f\x4c\xd2\xdd\xe3\xa1\xaa\xf9\xf9\x35\xe0\x9a\x54\x4f\xfe\x07\
\xb7\xcf\x5d\x42\x99\xd1\x8d\xe3\x87\x0a\x96\x46\xdd\x69\x47\x48\
\x07\x60\xc3\x29\x87\x6f\x50\xfe\xfc\xb4\xbb\xaa\xab\x3b\xb3\x74\
\xfd\xc6\x24\x7d\x7d\x3e\x4a\x01\xde\xf7\xbb\xc5\xcc\xab\x5a\xc7\
\x3d\x17\xde\xc7\xb9\xe5\x3b\xba\xd3\x5e\x78\xf7\x70\x40\x8a\x9f\
\x44\x75\x6d\x84\xf4\xed\xc6\x86\x74\x67\xbe\x0a\x5f\x10\xd4\x8f\
\x9e\x1b\xcf\xca\xe5\xd9\x6c\x3d\xd2\x7d\x83\x59\x77\xcf\x42\x31\
\xbe\xac\x94\x0f\xb6\x26\xf0\xfc\xc2\x07\x70\xfc\x10\x73\x26\x34\
\xf1\xf0\x57\xbf\x3b\xf0\xc3\x79\x77\x3d\x74\xf4\x58\xe9\xd9\xc0\
\x75\xc0\x8b\x20\xfa\x34\xaf\x0b\xd5\xeb\x06\x78\xaf\x90\x8d\x82\
\xa0\x7e\x7e\xff\x71\x00\xe5\xef\x1b\x6b\x96\x1c\xef\x49\x1d\x0b\
\x1b\xda\xd2\x19\xb3\x6e\x61\xdf\xe1\x89\xec\x6b\xeb\x1f\xd1\xb3\
\x12\x41\x59\xe8\x58\xd5\xc5\x93\xd7\x2e\x8d\xdd\x3a\xd1\x3c\xfa\
\x44\xd9\x2b\xc0\x5d\x8e\x56\x3c\xc3\xc8\xec\x7b\x5b\xf5\x93\x80\
\x7c\x17\x38\xf5\x8a\x7e\xfd\x83\x28\xba\xc2\x9a\xd9\xb3\x6a\xae\
\x39\xcc\x6a\x8e\xf4\x4c\x26\xec\xac\xa2\x38\x75\x27\x57\x5e\x12\
\xa6\x72\x9c\x36\x96\xe8\xaf\x05\x6e\xb5\xa2\xf6\x71\x80\x4b\x6f\
\x9b\x7b\xa3\xee\x7c\xbc\x5a\xf1\x7b\x2e\x6c\x6c\xc8\xb4\x9c\x72\
\xf6\xbd\xf4\x2c\xbe\xa6\xd2\xba\xa3\xd5\xc7\x4e\x4e\x26\xa4\x81\
\x6b\x5e\x4f\x52\xb9\x8d\x0d\x2d\x7d\xa4\x07\x24\x42\x8c\xe2\x34\
\xf8\x06\xf0\xc4\xe0\x82\x99\xde\x76\x4c\xf1\x7b\x5e\x02\xf5\x50\
\xa1\x8e\x61\x74\xa2\x0b\xde\xd3\xfd\x96\xa4\x22\x82\xda\xa2\x18\
\xf8\xe5\x4b\xb1\x13\xb3\x89\x6f\x4e\x8d\xb5\x0b\xb9\x37\x16\x8f\
\xdc\x92\xad\x7d\xc6\x3b\xa0\xdc\x02\xb2\xfb\x94\xba\x84\xa1\x1e\
\x0b\x85\x91\x99\x5d\x99\xca\x75\x53\x5d\xc3\x3a\xf1\x94\x9a\xd9\
\x48\xa8\x73\x3e\x73\x66\xea\x44\xab\xc3\x64\x9c\x51\xab\xe6\xc7\
\x52\x32\xe5\x9a\x8b\x6d\x7f\x34\xc5\x51\x3d\xb5\x72\x79\xa6\x0f\
\xf8\x83\x9e\xa8\xe3\x44\x4d\x94\xe0\x85\xe6\xe1\x54\xfc\x9a\xbd\
\xad\x09\x5a\x3f\xca\x14\xac\x5f\xb9\xaf\x53\x21\xb8\x6a\x2c\x24\
\x1c\x53\x9d\x42\x29\x5d\x2a\x9c\x2d\x5d\x7a\x6a\xc5\x7f\x7c\x2b\
\xc1\x2d\xfe\x3e\xfd\x7a\x2d\x5b\x76\x7e\x42\x22\xe9\x8d\xc6\x2f\
\x01\x4c\x3a\x23\xa0\x6a\xeb\x74\x56\x3e\x99\xf0\x41\x7c\x4f\xef\
\x7d\x0c\xc5\xd9\x3d\x04\x58\xa6\xfc\x57\x74\x25\xe7\xb2\x63\x6f\
\x02\xcf\x1b\xd1\x94\x0b\x6c\x1a\x0b\x28\x75\x34\x85\x2d\xcd\x7e\
\x16\xd8\x72\x67\x6f\xf5\xfc\xbe\x22\xc5\xdd\x5e\xe3\x19\x57\x83\
\x28\x09\xce\x6f\x20\xf5\xd9\x24\x3a\xd7\x30\xa1\x22\x4d\x69\x49\
\xc1\x32\xf1\x82\x15\xb5\x57\x9c\xb9\xf0\x9d\x48\x70\xef\x49\x25\
\xf3\xce\x9f\x8c\x9e\x7b\x10\xb2\xf7\xc4\xb2\x6f\x7c\x9d\xfe\xd0\
\xdd\xfc\x6b\x87\x87\xeb\xe5\x25\xfc\x0b\xc0\x03\x63\xbd\x7c\xa8\
\x63\xc1\xb2\xa5\xd9\x0f\xfe\x92\xa9\x9e\x2f\x9b\x84\xb7\xf7\x7c\
\x35\xf3\xde\x2c\xd7\xfc\x36\x08\x23\xeb\x30\x7d\x3a\xe9\xee\x35\
\x14\x85\xba\x99\x58\xa9\xe5\xf6\x24\xcf\x00\x8f\x59\x51\x3b\x39\
\xfc\x12\x72\xda\x9e\xaa\xad\xd3\xb3\xd9\xf8\x13\x8e\x01\x37\x2a\
\x99\xf5\x7f\x2b\xea\x9c\x85\xe2\xbc\x0f\x02\x3c\x7d\x1a\x8a\x3e\
\x89\x9d\x7b\xd3\x88\xec\x1b\x3b\x03\x3c\x02\xd4\x59\x51\xbb\x77\
\xac\x80\xc6\x54\xa7\xf2\x12\x3f\x78\x51\xd7\xd6\xa9\x8f\x81\xff\
\xa0\x1b\xbe\xa3\xd4\xd7\x2f\x45\x4b\x2c\xc3\x50\xdb\xb9\xf9\xda\
\xf1\x47\x5d\x4f\x3e\x60\x45\xed\x55\xf9\xae\x69\x67\x1c\xd4\x10\
\xaf\x2d\x77\xa8\xad\x53\xbe\x84\xf4\x1f\x15\x92\x8b\x7c\xa1\x98\
\xe7\x4e\x29\xda\xb6\xb0\xa6\xf4\xfe\x05\x73\x8f\x6e\x1f\xe4\xcf\
\xa9\x00\x2a\x78\xfb\x1d\x8d\x8c\xb1\x78\xe4\x24\x9d\x9b\x1f\xd1\
\xab\x6f\x7f\x3c\xb4\x60\x73\x6b\x44\xcb\x67\xa3\x90\xcd\x11\xf7\
\x3a\x5d\x50\x23\xd9\x1c\x66\x7b\x7c\x2c\x1e\xf9\x62\x2c\x1e\x39\
\xa7\xd0\xfe\x62\xd8\x0f\xe7\x02\x1b\xac\xa8\x5d\x55\x60\x83\x1f\
\x00\x33\xad\xa8\x7d\x6f\xb0\x76\x14\xd8\x09\xf4\x07\x49\x53\x01\
\x84\x81\x57\x81\xa7\xac\xa8\x9d\xca\xb9\x6d\xcf\xc8\x36\x7a\xf4\
\x02\xbb\x80\x09\x40\x0d\x50\x6f\x45\xed\x67\x73\xb9\xa7\xe5\xc9\
\xc6\xca\x11\x0e\x6f\x02\xc5\x39\xdf\xcf\x02\x2e\xb5\xa2\xf6\xfe\
\x1c\xf0\x4a\x00\xfe\xfd\x58\x3c\x72\xbd\x15\xb5\x77\x05\x3f\x6d\
\x08\xfa\xaa\xb5\x39\xba\x26\xf0\x7a\x2c\x1e\x29\xb5\xa2\xf6\xe3\
\x83\xdc\x53\x38\x7d\xd1\x86\xcd\x20\x7c\x2b\x6a\xff\x02\x58\x01\
\xd4\x05\xeb\x93\x80\x72\x60\x48\x53\x67\x45\xed\x34\xb0\x18\xe8\
\x0d\x0e\x33\xd4\xe0\x99\x90\x61\x59\xd6\x0c\x58\xb1\x78\x64\x8a\
\x15\xb5\xdb\x63\xf1\xc8\xcf\x80\xa7\x63\xf1\x48\x0b\x90\x00\x8e\
\x00\x6d\x56\xd4\xde\x0a\xec\x1c\xe4\x69\xbe\xf0\x89\x42\x23\xa0\
\xff\xa2\x8c\x78\xc1\x4b\x58\x0f\x00\xff\x38\x16\x8f\x7c\x39\x98\
\xda\x8c\x03\x66\x03\x97\xc5\xe2\x91\x69\xc0\x8b\x56\xd4\x7e\xbd\
\x90\xa7\xbc\x02\xa7\x3e\x71\x69\x06\xfc\x31\xce\xb3\xce\x0a\x0e\
\xd0\x11\x8b\x47\xae\x02\x34\x2b\x6a\xaf\x1b\xec\x14\x62\xf1\x48\
\x51\x30\x66\x8a\x02\x8f\xc6\xe2\x91\xb6\x41\xfe\x29\xc3\xdc\x7f\
\x10\xb0\x63\xf1\xc8\x75\x05\x42\x73\x25\xf0\xfe\x68\x21\x8c\xc5\
\x23\xa5\xc0\x43\xc0\xab\x56\xd4\xee\x03\xce\x07\x9e\x1b\xa6\xd7\
\x6f\x45\x6d\x1b\xd8\x1c\x80\x2f\x39\x29\x14\xaf\x35\xcd\xe1\xda\
\x05\xdb\x08\x5c\xbc\x1a\x78\x12\x78\x27\x48\xf7\x69\x41\x46\xe9\
\x56\xd4\x5e\x94\xe3\x95\x04\x70\x6f\x30\xa9\x53\x03\xc3\x17\x00\
\xdf\x01\xfe\x68\x45\xed\xe7\x72\x74\xd7\x01\x4e\x00\x6e\x7f\x70\
\xa9\xa8\x06\xee\x03\x5a\xac\xa8\xfd\x50\x5e\x7e\xac\x58\xf9\xb8\
\x72\x7b\xed\x32\x7f\xcd\xfa\x99\xc5\xba\x71\xec\x56\x29\xb5\xb9\
\x42\x78\x2a\xc8\x0e\x21\xbc\x37\x16\xce\xfb\xa4\x39\x57\x7f\x4d\
\xf3\xac\x07\x75\xb3\xcb\x43\xf8\x02\x29\x84\x94\x6a\x1a\xe4\x81\
\xe2\xf1\x9d\x6f\xce\xff\x3c\x27\xa6\x22\xcf\x3f\xff\xfb\xb2\xc5\
\x8b\xef\xec\x7d\x63\x53\xf9\x65\xd2\xd7\xae\x90\x52\x9d\x0c\xd2\
\x45\xc8\x43\x8a\x9a\x5e\xb7\xf0\xa2\xe4\x87\x00\x0d\xaf\xdc\xc4\
\xa2\x6f\xfd\x65\x28\xa8\xfa\xfa\xfa\xf3\x84\x92\x69\xf3\xdd\x70\
\xcd\x40\xdf\x94\x23\xbd\x1d\x0b\xba\x8c\xe2\x43\x45\xba\x69\x9b\
\xba\xd9\xa9\xeb\x66\xa7\x16\x64\xce\x78\x84\xff\x51\x26\x35\xf5\
\x6c\x29\x55\x0f\x29\xfc\xfe\xc4\xf4\xce\x70\xc5\xf6\x73\x14\x35\
\x83\xa2\xa6\x3b\x15\xb5\xcf\x00\xe1\x03\xdd\xd9\x22\x29\x76\x3a\
\xe9\x09\x5a\xdf\x27\x5f\x38\xae\x86\x7a\x8b\x9d\xfe\xaa\x94\xef\
\x99\x99\xf2\xaa\xc6\xa9\x5a\xa8\xc7\x01\x79\xe8\xee\xbb\x17\xf7\
\xe5\x23\xba\x2f\xfd\xd0\xd7\x84\xe2\xce\x30\x4b\x0e\x9a\x66\xc9\
\xc1\xcf\x01\x21\x20\x02\x1c\x00\xa6\x06\xc5\xb5\x03\xa9\x74\x84\
\xc2\x87\xcf\x0f\x6c\x54\x19\x25\x6d\xfb\x80\x99\xc0\xd1\x6c\x2f\
\x3e\xd8\x9a\xb2\x39\x1b\x2a\x79\xb9\x6e\x76\x66\xca\xab\xde\x3d\
\x14\x84\xd9\x05\x7a\x82\xaa\x7e\x18\x84\x07\xec\xcd\x07\xea\xa3\
\x60\x9a\xfb\x41\x90\x65\x22\xa7\xd2\xbb\x01\x6f\xd4\x20\x4b\x1d\
\x60\x4d\xa0\x37\xb8\xf6\x56\x4e\x06\x1b\xc1\xe7\x78\xc0\x21\x2d\
\xd0\xcd\xb5\x2b\x81\x0f\x83\x35\xf7\x7f\x3a\x6f\xaf\xaf\xaf\xcf\
\xfb\xff\x67\xf2\xa9\x90\x7f\x03\xbb\x9c\x9c\x9c\x32\xd8\x63\xca\
\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x8e\x8c\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
@@ -2437,6 +2292,151 @@ qt_resource_data = b"\
\x00\x80\x10\x0c\x00\x00\x00\x10\x82\x01\x00\x00\x00\x42\x30\x00\
\x00\x00\xe0\x96\xff\x7f\x00\x27\x97\xdb\xb5\x4d\x29\xcb\x9d\x00\
\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x08\xed\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x25\x00\x00\x00\x30\x08\x06\x00\x00\x00\x96\x85\xb3\x2b\
\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\
\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\
\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\
\xdf\x03\x1b\x0e\x10\x3b\x1e\x53\x78\x6b\x00\x00\x08\x7a\x49\x44\
\x41\x54\x58\xc3\xed\x98\x7b\x70\x54\xf5\x15\xc7\x3f\xbf\xfb\xd8\
\x7b\xb3\x79\x42\x70\x09\x08\xa8\xa5\x20\x54\x4a\xaa\xa5\x6b\xad\
\x81\xfa\x18\xf1\x3a\xad\x53\x67\xaa\x16\xad\xd1\x5a\x75\xc4\x19\
\xa7\x63\xeb\xab\x1a\x94\x91\x91\xaa\xb5\x76\x3a\xd3\x97\xa9\x75\
\x98\x96\xd2\x86\x19\x5b\x47\x85\x61\xd5\x1a\x35\xc1\x16\x57\xa8\
\xe1\xfd\x0e\x04\x03\x92\x9b\x40\x4c\x76\x37\xd9\xec\x7d\xfc\xfa\
\xc7\xde\xd0\x4d\xd8\x4d\x42\xa1\xd3\x3f\xf4\xcc\xec\x24\xfb\xdb\
\x73\xcf\xef\xfb\x3b\xe7\x7b\xce\x3d\xbf\x03\x9f\xc9\x67\xf2\x7f\
\x94\x58\x3c\x72\x5a\xcf\x5f\xb1\xc8\xe4\x8a\x45\xe6\xa8\x7a\x62\
\x2c\x40\xac\xa8\x9d\xfb\xdd\x00\xa6\x07\x9f\xb3\x81\xb3\x1c\x47\
\x96\xef\xde\xe3\x86\xf7\xb7\xba\x66\x67\x97\x27\x92\x29\xc9\xc0\
\x80\x74\x84\xa0\x17\xf8\x18\x68\x03\xb6\x34\x36\xa4\xf7\xe4\x02\
\x6c\x6c\x48\x8f\x1d\xd4\x20\x90\xc0\x33\xa5\xc0\xc5\xc0\x0d\xc0\
\x95\x01\x98\xec\xc3\x02\x76\xed\x76\x59\xf7\x16\x74\xa7\x2a\xf1\
\xb4\x08\x52\x14\x81\x00\x55\xf1\xd0\x45\x0f\xaa\xdf\x83\xf0\xd3\
\x08\xd9\x8f\xe2\xf7\xf5\x80\xf7\x5b\x50\x7f\xd7\xd8\x90\x3e\x50\
\x08\xd8\x88\x9e\x7a\x73\xd3\xc4\x99\x9e\x27\x7f\x13\x80\x39\x49\
\x14\x05\x7e\xf9\x82\x4a\x4b\xea\x01\x12\xe6\xe5\xf8\x98\x20\x3d\
\x94\xa2\x32\xb4\x8a\x89\x08\xc3\x40\x75\x93\x18\x99\x7d\x14\xa5\
\x37\x52\x92\x7a\x9b\xe2\x54\x13\x9a\xdb\x71\x50\x0a\xf5\xa6\xc6\
\x86\xf4\x86\x7c\xc0\x46\x02\x25\x5e\x6e\x9a\xb0\x24\x5c\xa4\x2c\
\x93\xb2\x80\x82\x80\xf8\xf6\x49\xfc\xb4\x79\x15\xaa\xec\x47\xe0\
\xa2\x16\x8f\x43\x1b\x37\x29\x8b\x58\x4a\x40\x20\x85\x0a\x68\x80\
\x47\x79\xe2\x35\xaa\x3a\x1e\x46\x73\x0f\xef\x01\x65\x4e\x63\x43\
\xda\x19\x6e\x57\x2d\x84\x68\x42\x0d\xea\x79\x53\x94\xab\xcb\x4a\
\xd4\x1a\xd3\x28\x8c\x7d\x62\xe5\x00\x42\xfa\x6c\x6d\xaf\x46\x33\
\x34\xb4\xca\x29\x08\x45\x19\x76\x72\x1f\x81\x8b\x40\x92\x36\x2e\
\x20\xe4\xb4\x53\x94\x6e\xa9\x04\xd9\x74\x60\x9b\xdb\x7a\x52\x04\
\x0a\x6d\xd6\xb5\x1e\x6f\xdb\x9e\xf4\xe6\xdd\xad\x03\x8e\xeb\x15\
\x76\xa7\xa6\x38\x7c\xb3\xfa\xaf\x5c\x32\xbd\x09\x69\x54\x9c\x04\
\x68\xa8\x64\x3d\x97\x36\x2f\xc0\x57\x0c\x80\x0b\xf3\xd2\x62\x24\
\x4e\xe9\x21\x5e\xde\xba\xab\x7f\xed\xc1\xf6\xcc\x88\x19\x5a\x56\
\xd4\x43\xed\x25\x2f\x32\x6d\xdc\xfe\xec\xbe\xa3\xa5\xbc\xf4\x72\
\x51\x8e\x1d\x54\x6d\x9d\xce\xaa\xe5\xee\x80\xa6\xb1\xe4\x9f\x9b\
\x52\x6d\x76\x97\x4b\x21\x27\x78\xbe\xca\xb4\xca\x83\xdc\xf1\x95\
\x67\xa8\x30\xbb\xf0\x65\xe1\xb3\x4a\xa1\xa0\xbb\xed\x08\x99\x01\
\xd8\x7d\x4a\xa0\x56\x2e\x77\xb2\xc0\x9e\x72\xb7\xfb\x52\xd6\x35\
\x6f\x4c\xd2\xdd\xe3\xa1\xaa\xf9\xf9\x35\xe0\x9a\x54\x4f\xfe\x07\
\xb7\xcf\x5d\x42\x99\xd1\x8d\xe3\x87\x0a\x96\x46\xdd\x69\x47\x48\
\x07\x60\xc3\x29\x87\x6f\x50\xfe\xfc\xb4\xbb\xaa\xab\x3b\xb3\x74\
\xfd\xc6\x24\x7d\x7d\x3e\x4a\x01\xde\xf7\xbb\xc5\xcc\xab\x5a\xc7\
\x3d\x17\xde\xc7\xb9\xe5\x3b\xba\xd3\x5e\x78\xf7\x70\x40\x8a\x9f\
\x44\x75\x6d\x84\xf4\xed\xc6\x86\x74\x67\xbe\x0a\x5f\x10\xd4\x8f\
\x9e\x1b\xcf\xca\xe5\xd9\x6c\x3d\xd2\x7d\x83\x59\x77\xcf\x42\x31\
\xbe\xac\x94\x0f\xb6\x26\xf0\xfc\xc2\x07\x70\xfc\x10\x73\x26\x34\
\xf1\xf0\x57\xbf\x3b\xf0\xc3\x79\x77\x3d\x74\xf4\x58\xe9\xd9\xc0\
\x75\xc0\x8b\x20\xfa\x34\xaf\x0b\xd5\xeb\x06\x78\xaf\x90\x8d\x82\
\xa0\x7e\x7e\xff\x71\x00\xe5\xef\x1b\x6b\x96\x1c\xef\x49\x1d\x0b\
\x1b\xda\xd2\x19\xb3\x6e\x61\xdf\xe1\x89\xec\x6b\xeb\x1f\xd1\xb3\
\x12\x41\x59\xe8\x58\xd5\xc5\x93\xd7\x2e\x8d\xdd\x3a\xd1\x3c\xfa\
\x44\xd9\x2b\xc0\x5d\x8e\x56\x3c\xc3\xc8\xec\x7b\x5b\xf5\x93\x80\
\x7c\x17\x38\xf5\x8a\x7e\xfd\x83\x28\xba\xc2\x9a\xd9\xb3\x6a\xae\
\x39\xcc\x6a\x8e\xf4\x4c\x26\xec\xac\xa2\x38\x75\x27\x57\x5e\x12\
\xa6\x72\x9c\x36\x96\xe8\xaf\x05\x6e\xb5\xa2\xf6\x71\x80\x4b\x6f\
\x9b\x7b\xa3\xee\x7c\xbc\x5a\xf1\x7b\x2e\x6c\x6c\xc8\xb4\x9c\x72\
\xf6\xbd\xf4\x2c\xbe\xa6\xd2\xba\xa3\xd5\xc7\x4e\x4e\x26\xa4\x81\
\x6b\x5e\x4f\x52\xb9\x8d\x0d\x2d\x7d\xa4\x07\x24\x42\x8c\xe2\x34\
\xf8\x06\xf0\xc4\xe0\x82\x99\xde\x76\x4c\xf1\x7b\x5e\x02\xf5\x50\
\xa1\x8e\x61\x74\xa2\x0b\xde\xd3\xfd\x96\xa4\x22\x82\xda\xa2\x18\
\xf8\xe5\x4b\xb1\x13\xb3\x89\x6f\x4e\x8d\xb5\x0b\xb9\x37\x16\x8f\
\xdc\x92\xad\x7d\xc6\x3b\xa0\xdc\x02\xb2\xfb\x94\xba\x84\xa1\x1e\
\x0b\x85\x91\x99\x5d\x99\xca\x75\x53\x5d\xc3\x3a\xf1\x94\x9a\xd9\
\x48\xa8\x73\x3e\x73\x66\xea\x44\xab\xc3\x64\x9c\x51\xab\xe6\xc7\
\x52\x32\xe5\x9a\x8b\x6d\x7f\x34\xc5\x51\x3d\xb5\x72\x79\xa6\x0f\
\xf8\x83\x9e\xa8\xe3\x44\x4d\x94\xe0\x85\xe6\xe1\x54\xfc\x9a\xbd\
\xad\x09\x5a\x3f\xca\x14\xac\x5f\xb9\xaf\x53\x21\xb8\x6a\x2c\x24\
\x1c\x53\x9d\x42\x29\x5d\x2a\x9c\x2d\x5d\x7a\x6a\xc5\x7f\x7c\x2b\
\xc1\x2d\xfe\x3e\xfd\x7a\x2d\x5b\x76\x7e\x42\x22\xe9\x8d\xc6\x2f\
\x01\x4c\x3a\x23\xa0\x6a\xeb\x74\x56\x3e\x99\xf0\x41\x7c\x4f\xef\
\x7d\x0c\xc5\xd9\x3d\x04\x58\xa6\xfc\x57\x74\x25\xe7\xb2\x63\x6f\
\x02\xcf\x1b\xd1\x94\x0b\x6c\x1a\x0b\x28\x75\x34\x85\x2d\xcd\x7e\
\x16\xd8\x72\x67\x6f\xf5\xfc\xbe\x22\xc5\xdd\x5e\xe3\x19\x57\x83\
\x28\x09\xce\x6f\x20\xf5\xd9\x24\x3a\xd7\x30\xa1\x22\x4d\x69\x49\
\xc1\x32\xf1\x82\x15\xb5\x57\x9c\xb9\xf0\x9d\x48\x70\xef\x49\x25\
\xf3\xce\x9f\x8c\x9e\x7b\x10\xb2\xf7\xc4\xb2\x6f\x7c\x9d\xfe\xd0\
\xdd\xfc\x6b\x87\x87\xeb\xe5\x25\xfc\x0b\xc0\x03\x63\xbd\x7c\xa8\
\x63\xc1\xb2\xa5\xd9\x0f\xfe\x92\xa9\x9e\x2f\x9b\x84\xb7\xf7\x7c\
\x35\xf3\xde\x2c\xd7\xfc\x36\x08\x23\xeb\x30\x7d\x3a\xe9\xee\x35\
\x14\x85\xba\x99\x58\xa9\xe5\xf6\x24\xcf\x00\x8f\x59\x51\x3b\x39\
\xfc\x12\x72\xda\x9e\xaa\xad\xd3\xb3\xd9\xf8\x13\x8e\x01\x37\x2a\
\x99\xf5\x7f\x2b\xea\x9c\x85\xe2\xbc\x0f\x02\x3c\x7d\x1a\x8a\x3e\
\x89\x9d\x7b\xd3\x88\xec\x1b\x3b\x03\x3c\x02\xd4\x59\x51\xbb\x77\
\xac\x80\xc6\x54\xa7\xf2\x12\x3f\x78\x51\xd7\xd6\xa9\x8f\x81\xff\
\xa0\x1b\xbe\xa3\xd4\xd7\x2f\x45\x4b\x2c\xc3\x50\xdb\xb9\xf9\xda\
\xf1\x47\x5d\x4f\x3e\x60\x45\xed\x55\xf9\xae\x69\x67\x1c\xd4\x10\
\xaf\x2d\x77\xa8\xad\x53\xbe\x84\xf4\x1f\x15\x92\x8b\x7c\xa1\x98\
\xe7\x4e\x29\xda\xb6\xb0\xa6\xf4\xfe\x05\x73\x8f\x6e\x1f\xe4\xcf\
\xa9\x00\x2a\x78\xfb\x1d\x8d\x8c\xb1\x78\xe4\x24\x9d\x9b\x1f\xd1\
\xab\x6f\x7f\x3c\xb4\x60\x73\x6b\x44\xcb\x67\xa3\x90\xcd\x11\xf7\
\x3a\x5d\x50\x23\xd9\x1c\x66\x7b\x7c\x2c\x1e\xf9\x62\x2c\x1e\x39\
\xa7\xd0\xfe\x62\xd8\x0f\xe7\x02\x1b\xac\xa8\x5d\x55\x60\x83\x1f\
\x00\x33\xad\xa8\x7d\x6f\xb0\x76\x14\xd8\x09\xf4\x07\x49\x53\x01\
\x84\x81\x57\x81\xa7\xac\xa8\x9d\xca\xb9\x6d\xcf\xc8\x36\x7a\xf4\
\x02\xbb\x80\x09\x40\x0d\x50\x6f\x45\xed\x67\x73\xb9\xa7\xe5\xc9\
\xc6\xca\x11\x0e\x6f\x02\xc5\x39\xdf\xcf\x02\x2e\xb5\xa2\xf6\xfe\
\x1c\xf0\x4a\x00\xfe\xfd\x58\x3c\x72\xbd\x15\xb5\x77\x05\x3f\x6d\
\x08\xfa\xaa\xb5\x39\xba\x26\xf0\x7a\x2c\x1e\x29\xb5\xa2\xf6\xe3\
\x83\xdc\x53\x38\x7d\xd1\x86\xcd\x20\x7c\x2b\x6a\xff\x02\x58\x01\
\xd4\x05\xeb\x93\x80\x72\x60\x48\x53\x67\x45\xed\x34\xb0\x18\xe8\
\x0d\x0e\x33\xd4\xe0\x99\x90\x61\x59\xd6\x0c\x58\xb1\x78\x64\x8a\
\x15\xb5\xdb\x63\xf1\xc8\xcf\x80\xa7\x63\xf1\x48\x0b\x90\x00\x8e\
\x00\x6d\x56\xd4\xde\x0a\xec\x1c\xe4\x69\xbe\xf0\x89\x42\x23\xa0\
\xff\xa2\x8c\x78\xc1\x4b\x58\x0f\x00\xff\x38\x16\x8f\x7c\x39\x98\
\xda\x8c\x03\x66\x03\x97\xc5\xe2\x91\x69\xc0\x8b\x56\xd4\x7e\xbd\
\x90\xa7\xbc\x02\xa7\x3e\x71\x69\x06\xfc\x31\xce\xb3\xce\x0a\x0e\
\xd0\x11\x8b\x47\xae\x02\x34\x2b\x6a\xaf\x1b\xec\x14\x62\xf1\x48\
\x51\x30\x66\x8a\x02\x8f\xc6\xe2\x91\xb6\x41\xfe\x29\xc3\xdc\x7f\
\x10\xb0\x63\xf1\xc8\x75\x05\x42\x73\x25\xf0\xfe\x68\x21\x8c\xc5\
\x23\xa5\xc0\x43\xc0\xab\x56\xd4\xee\x03\xce\x07\x9e\x1b\xa6\xd7\
\x6f\x45\x6d\x1b\xd8\x1c\x80\x2f\x39\x29\x14\xaf\x35\xcd\xe1\xda\
\x05\xdb\x08\x5c\xbc\x1a\x78\x12\x78\x27\x48\xf7\x69\x41\x46\xe9\
\x56\xd4\x5e\x94\xe3\x95\x04\x70\x6f\x30\xa9\x53\x03\xc3\x17\x00\
\xdf\x01\xfe\x68\x45\xed\xe7\x72\x74\xd7\x01\x4e\x00\x6e\x7f\x70\
\xa9\xa8\x06\xee\x03\x5a\xac\xa8\xfd\x50\x5e\x7e\xac\x58\xf9\xb8\
\x72\x7b\xed\x32\x7f\xcd\xfa\x99\xc5\xba\x71\xec\x56\x29\xb5\xb9\
\x42\x78\x2a\xc8\x0e\x21\xbc\x37\x16\xce\xfb\xa4\x39\x57\x7f\x4d\
\xf3\xac\x07\x75\xb3\xcb\x43\xf8\x02\x29\x84\x94\x6a\x1a\xe4\x81\
\xe2\xf1\x9d\x6f\xce\xff\x3c\x27\xa6\x22\xcf\x3f\xff\xfb\xb2\xc5\
\x8b\xef\xec\x7d\x63\x53\xf9\x65\xd2\xd7\xae\x90\x52\x9d\x0c\xd2\
\x45\xc8\x43\x8a\x9a\x5e\xb7\xf0\xa2\xe4\x87\x00\x0d\xaf\xdc\xc4\
\xa2\x6f\xfd\x65\x28\xa8\xfa\xfa\xfa\xf3\x84\x92\x69\xf3\xdd\x70\
\xcd\x40\xdf\x94\x23\xbd\x1d\x0b\xba\x8c\xe2\x43\x45\xba\x69\x9b\
\xba\xd9\xa9\xeb\x66\xa7\x16\x64\xce\x78\x84\xff\x51\x26\x35\xf5\
\x6c\x29\x55\x0f\x29\xfc\xfe\xc4\xf4\xce\x70\xc5\xf6\x73\x14\x35\
\x83\xa2\xa6\x3b\x15\xb5\xcf\x00\xe1\x03\xdd\xd9\x22\x29\x76\x3a\
\xe9\x09\x5a\xdf\x27\x5f\x38\xae\x86\x7a\x8b\x9d\xfe\xaa\x94\xef\
\x99\x99\xf2\xaa\xc6\xa9\x5a\xa8\xc7\x01\x79\xe8\xee\xbb\x17\xf7\
\xe5\x23\xba\x2f\xfd\xd0\xd7\x84\xe2\xce\x30\x4b\x0e\x9a\x66\xc9\
\xc1\xcf\x01\x21\x20\x02\x1c\x00\xa6\x06\xc5\xb5\x03\xa9\x74\x84\
\xc2\x87\xcf\x0f\x6c\x54\x19\x25\x6d\xfb\x80\x99\xc0\xd1\x6c\x2f\
\x3e\xd8\x9a\xb2\x39\x1b\x2a\x79\xb9\x6e\x76\x66\xca\xab\xde\x3d\
\x14\x84\xd9\x05\x7a\x82\xaa\x7e\x18\x84\x07\xec\xcd\x07\xea\xa3\
\x60\x9a\xfb\x41\x90\x65\x22\xa7\xd2\xbb\x01\x6f\xd4\x20\x4b\x1d\
\x60\x4d\xa0\x37\xb8\xf6\x56\x4e\x06\x1b\xc1\xe7\x78\xc0\x21\x2d\
\xd0\xcd\xb5\x2b\x81\x0f\x83\x35\xf7\x7f\x3a\x6f\xaf\xaf\xaf\xcf\
\xfb\xff\x67\xf2\xa9\x90\x7f\x03\xbb\x9c\x9c\x9c\x32\xd8\x63\xca\
\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
"
qt_resource_name = b"\
@@ -2444,21 +2444,21 @@ qt_resource_name = b"\
\x07\x03\x7d\xc3\
\x00\x69\
\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\
\x00\x0e\
\x01\x8e\xbf\xec\
\x00\x6c\
\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x75\x00\x64\x00\x73\x00\x2d\x00\x73\x00\x6d\x00\x61\x00\x6c\x00\x6c\
\x00\x0c\
\x05\xe1\xfc\x77\
\x00\x6c\
\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x75\x00\x64\x00\x73\x00\x2d\x00\x62\x00\x69\x00\x67\
\x00\x0e\
\x01\x8e\xbf\xec\
\x00\x6c\
\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x75\x00\x64\x00\x73\x00\x2d\x00\x73\x00\x6d\x00\x61\x00\x6c\x00\x6c\
"
qt_resource_struct_v1 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x8e\x90\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x08\xf1\
"
qt_resource_struct_v2 = b"\
@@ -2466,9 +2466,9 @@ qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x8e\x90\
\x00\x00\x01\x70\xc4\x82\x24\xd0\
\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x08\xf1\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x70\xc4\x82\x24\xd0\
"

View File

@@ -2,9 +2,10 @@
# Form implementation generated from reading ui file 'UDSWindow.ui'
#
# Created by: PyQt5 UI code generator 5.13.2
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets

View File

@@ -31,7 +31,7 @@
'''
from __future__ import unicode_literals
VERSION = '3.5.0'
VERSION = '3.5.1'
__title__ = 'udclient'
__version__ = VERSION

View File

@@ -25,7 +25,11 @@ class CheckfingerPrints(paramiko.MissingHostKeyPolicy):
if self.fingerPrints:
remotefingerPrints = hexlify(key.get_fingerprint()).decode().lower()
if remotefingerPrints not in self.fingerPrints.split(','):
logger.error("Server {!r} has invalid fingerPrints. ({} vs {})".format(hostname, remotefingerPrints, self.fingerPrints))
logger.error(
"Server {!r} has invalid fingerPrints. ({} vs {})".format(
hostname, remotefingerPrints, self.fingerPrints
)
)
raise paramiko.SSHException(
"Server {!r} has invalid fingerPrints".format(hostname)
)
@@ -47,21 +51,39 @@ class Handler(socketserver.BaseRequestHandler):
self.thread.currentConnections += 1
try:
chan = self.ssh_transport.open_channel('direct-tcpip',
(self.chain_host, self.chain_port),
self.request.getpeername())
chan = self.ssh_transport.open_channel(
'direct-tcpip',
(self.chain_host, self.chain_port),
self.request.getpeername(),
)
except Exception as e:
logger.exception('Incoming request to %s:%d failed: %s', self.chain_host, self.chain_port, repr(e))
logger.exception(
'Incoming request to %s:%d failed: %s',
self.chain_host,
self.chain_port,
repr(e),
)
return
if chan is None:
logger.error('Incoming request to %s:%d was rejected by the SSH server.', self.chain_host, self.chain_port)
logger.error(
'Incoming request to %s:%d was rejected by the SSH server.',
self.chain_host,
self.chain_port,
)
return
logger.debug('Connected! Tunnel open %r -> %r -> %r', self.request.getpeername(), chan.getpeername(), (self.chain_host, self.chain_port))
logger.debug(
'Connected! Tunnel open %r -> %r -> %r',
self.request.getpeername(),
chan.getpeername(),
(self.chain_host, self.chain_port),
)
# self.ssh_transport.set_keepalive(10) # Keep alive every 10 seconds...
try:
while self.event.is_set() is False:
r, _w, _x = select.select([self.request, chan], [], [], 1) # pylint: disable=unused-variable
r, _w, _x = select.select(
[self.request, chan], [], [], 1
) # pylint: disable=unused-variable
if self.request in r:
data = self.request.recv(1024)
@@ -80,7 +102,10 @@ class Handler(socketserver.BaseRequestHandler):
peername = self.request.getpeername()
chan.close()
self.request.close()
logger.debug('Tunnel closed from %r', peername,)
logger.debug(
'Tunnel closed from %r',
peername,
)
except Exception:
pass
@@ -95,7 +120,18 @@ class ForwardThread(threading.Thread):
client: typing.Optional[paramiko.SSHClient]
fs: typing.Optional[ForwardServer]
def __init__(self, server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints):
def __init__(
self,
server,
port,
username,
password,
localPort,
redirectHost,
redirectPort,
waitTime,
fingerPrints,
):
threading.Thread.__init__(self)
self.client = None
self.fs = None
@@ -110,7 +146,7 @@ class ForwardThread(threading.Thread):
self.redirectPort = redirectPort
self.waitTime = waitTime
self.fingerPrints = fingerPrints
self.stopEvent = threading.Event()
@@ -124,7 +160,17 @@ class ForwardThread(threading.Thread):
if localPort is None:
localPort = random.randrange(33000, 53000)
ft = ForwardThread(self.server, self.port, self.username, self.password, localPort, redirectHost, redirectPort, self.waitTime, self.fingerPrints)
ft = ForwardThread(
self.server,
self.port,
self.username,
self.password,
localPort,
redirectHost,
redirectPort,
self.waitTime,
self.fingerPrints,
)
ft.client = self.client
self.client.useCount += 1 # type: ignore
ft.start()
@@ -134,7 +180,6 @@ class ForwardThread(threading.Thread):
return (ft, localPort)
def _timerFnc(self):
self.timer = None
logger.debug('Timer fnc: %s', self.currentConnections)
@@ -148,12 +193,21 @@ class ForwardThread(threading.Thread):
self.client = paramiko.SSHClient()
self.client.useCount = 1 # type: ignore
self.client.load_system_host_keys()
self.client.set_missing_host_key_policy(CheckfingerPrints(self.fingerPrints))
self.client.set_missing_host_key_policy(
CheckfingerPrints(self.fingerPrints)
)
logger.debug('Connecting to ssh host %s:%d ...', self.server, self.port)
# To disable ssh-ageng asking for passwords: allow_agent=False
self.client.connect(self.server, self.port, username=self.username, password=self.password, timeout=5, allow_agent=False)
self.client.connect(
self.server,
self.port,
username=self.username,
password=self.password,
timeout=5,
allow_agent=False,
)
except Exception:
logger.exception('Exception connecting: ')
self.status = 2 # Error
@@ -194,7 +248,17 @@ class ForwardThread(threading.Thread):
logger.exception('Exception stopping')
def forward(server, port, username, password, redirectHost, redirectPort, localPort=None, waitTime=10, fingerPrints=None):
def forward(
server,
port,
username,
password,
redirectHost,
redirectPort,
localPort=None,
waitTime=10,
fingerPrints=None,
):
'''
Instantiates an ssh connection to server:port
Returns the Thread created and the local redirected port as a list: (thread, port)
@@ -204,10 +268,28 @@ def forward(server, port, username, password, redirectHost, redirectPort, localP
if localPort is None:
localPort = random.randrange(40000, 50000)
logger.debug('Connecting to %s:%s using %s/%s redirecting to %s:%s, listening on 127.0.0.1:%s',
server, port, username, password, redirectHost, redirectPort, localPort)
logger.debug(
'Connecting to %s:%s using %s/%s redirecting to %s:%s, listening on 127.0.0.1:%s',
server,
port,
username,
password,
redirectHost,
redirectPort,
localPort,
)
ft = ForwardThread(server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints)
ft = ForwardThread(
server,
port,
username,
password,
localPort,
redirectHost,
redirectPort,
waitTime,
fingerPrints,
)
ft.start()

View File

@@ -29,8 +29,6 @@
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
import logging
import os
import os.path
@@ -57,7 +55,7 @@ try:
filename=logFile,
filemode='a',
format='%(levelname)s %(asctime)s %(message)s',
level=LOGLEVEL
level=LOGLEVEL,
)
except Exception:
logging.basicConfig(format='%(levelname)s %(asctime)s %(message)s', level=LOGLEVEL)

View File

@@ -30,14 +30,13 @@
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
import sys
LINUX = 'Linux'
WINDOWS = 'Windows'
MAC_OS_X = 'Mac os x'
def getOs():
if sys.platform.startswith('linux'):
return LINUX

View File

@@ -29,8 +29,6 @@
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
# pylint: disable=c-extension-no-member,no-name-in-module
import json
import bz2
import base64
@@ -42,7 +40,6 @@ import ssl
import socket
import typing
import certifi
from cryptography import x509
from cryptography.hazmat.backends import default_backend
@@ -63,9 +60,11 @@ CertCallbackType = typing.Callable[[str, str], bool]
class UDSException(Exception):
pass
class RetryException(UDSException):
pass
class InvalidVersion(UDSException):
downloadUrl: str
@@ -73,9 +72,10 @@ class InvalidVersion(UDSException):
super().__init__(downloadUrl)
self.downloadUrl = downloadUrl
class RestApi:
_restApiUrl: str # base Rest API URL
_restApiUrl: str # base Rest API URL
_callbackInvalidCert: typing.Optional[CertCallbackType]
_serverVersion: str
@@ -90,14 +90,18 @@ class RestApi:
self._callbackInvalidCert = callbackInvalidCert
self._serverVersion = ''
def get(self, url: str, params: typing.Optional[typing.Mapping[str, str]] = None) -> typing.Any:
def get(
self, url: str, params: typing.Optional[typing.Mapping[str, str]] = None
) -> typing.Any:
if params:
url += '?' + '&'.join(
'{}={}'.format(k, urllib.parse.quote(str(v).encode('utf8')))
for k, v in params.items()
)
return json.loads(RestApi.getUrl(self._restApiUrl + url, self._callbackInvalidCert))
return json.loads(
RestApi.getUrl(self._restApiUrl + url, self._callbackInvalidCert)
)
def processError(self, data: typing.Any) -> None:
if 'error' in data:
@@ -106,7 +110,6 @@ class RestApi:
raise UDSException(data['error'])
def getVersion(self) -> str:
'''Gets and stores the serverVersion.
Also checks that the version is valid for us. If not,
@@ -122,12 +125,16 @@ class RestApi:
try:
if self._serverVersion > VERSION:
raise InvalidVersion(downloadUrl)
return self._serverVersion
except InvalidVersion:
raise
except Exception as e:
raise UDSException(e)
def getScriptAndParams(self, ticket: str, scrambler: str) -> typing.Tuple[str, typing.Any]:
def getScriptAndParams(
self, ticket: str, scrambler: str
) -> typing.Tuple[str, typing.Any]:
'''Gets the transport script, validates it if necesary
and returns it'''
try:
@@ -145,10 +152,7 @@ class RestApi:
params = None
if self._serverVersion <= OLD_METHOD_VERSION:
script = bz2.decompress(base64.b64decode(data['result']))
# This fixes uds 2.2 "write" string on binary streams on some transport
script = script.replace(b'stdin.write("', b'stdin.write(b"')
script = script.replace(b'version)', b'version.decode("utf-8"))')
raise Exception('Server version is too old. Please, update it.')
else:
res = data['result']
# We have three elements on result:
@@ -173,7 +177,6 @@ class RestApi:
# exec(script.decode("utf-8"), globals(), {'parent': self, 'sp': params})
@staticmethod
def _open(
url: str, certErrorCallback: typing.Optional[CertCallbackType] = None
@@ -181,15 +184,23 @@ class RestApi:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.load_verify_locations(certifi.where())
# If we have the certificates file, we use it
if tools.getCaCertsFile() is not None:
ctx.load_verify_locations(tools.getCaCertsFile())
hostname = urllib.parse.urlparse(url)[1]
serial = ''
port = ''
if ':' in hostname:
hostname, port = hostname.split(':')
if url.startswith('https'):
port = port or '443'
with ctx.wrap_socket(
socket.socket(socket.AF_INET, socket.SOCK_STREAM), server_hostname=hostname
socket.socket(socket.AF_INET, socket.SOCK_STREAM),
server_hostname=hostname,
) as s:
s.connect((hostname, 443))
s.connect((hostname, int(port)))
# Get binary certificate
binCert = s.getpeercert(True)
if binCert:
@@ -200,14 +211,17 @@ class RestApi:
serial = hex(cert.serial_number)[2:]
response = None
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.check_hostname = True
def urlopen(url: str):
# Generate the request with the headers
req = urllib.request.Request(url, headers={
'User-Agent': os_detector.getOs() + " - UDS Connector " + VERSION
})
req = urllib.request.Request(
url,
headers={
'User-Agent': os_detector.getOs() + " - UDS Connector " + VERSION
},
)
return urllib.request.urlopen(req, context=ctx)
try:

View File

@@ -33,6 +33,8 @@ import tempfile
import string
import random
import os
import os.path
import sys
import socket
import stat
import sys
@@ -40,6 +42,8 @@ import time
import base64
import typing
import certifi
try:
import psutil
except ImportError:
@@ -161,7 +165,9 @@ def unlinkFiles(early: bool = False) -> None:
def addTaskToWait(task: typing.Any, includeSubprocess: bool = False) -> None:
logger.debug(
'Added task %s to wait %s', task, 'with subprocesses' if includeSubprocess else ''
'Added task %s to wait %s',
task,
'with subprocesses' if includeSubprocess else '',
)
_tasksToWait.append((task, includeSubprocess))
@@ -176,12 +182,22 @@ def waitForTasks() -> None:
elif hasattr(task, 'wait'):
task.wait()
# If wait for spanwed process (look for process with task pid) and we can look for them...
logger.debug('Psutil: %s, waitForSubp: %s, hasattr: %s', psutil, waitForSubp, hasattr(task, 'pid'))
logger.debug(
'Psutil: %s, waitForSubp: %s, hasattr: %s',
psutil,
waitForSubp,
hasattr(task, 'pid'),
)
if psutil and waitForSubp and hasattr(task, 'pid'):
subProcesses = list(filter(
lambda x: x.ppid() == task.pid, psutil.process_iter(attrs=('ppid',))
))
logger.debug('Waiting for subprocesses... %s, %s', task.pid, subProcesses)
subProcesses = list(
filter(
lambda x: x.ppid() == task.pid, # type: ignore
psutil.process_iter(attrs=('ppid',)),
)
)
logger.debug(
'Waiting for subprocesses... %s, %s', task.pid, subProcesses
)
for i in subProcesses:
logger.debug('Found %s', i)
i.wait()
@@ -218,11 +234,36 @@ def verifySignature(script: bytes, signature: bytes) -> bool:
)
try:
public_key.verify(
base64.b64decode(signature), script, padding.PKCS1v15(), hashes.SHA256()
public_key.verify( # type: ignore
base64.b64decode(signature), script, padding.PKCS1v15(), hashes.SHA256() # type: ignore
)
except Exception: # InvalidSignature
return False
# If no exception, the script was fine...
return True
def getCaCertsFile() -> typing.Optional[str]:
# First, try certifi...
# If environment contains CERTIFICATE_BUNDLE_PATH, use it
if 'CERTIFICATE_BUNDLE_PATH' in os.environ:
return os.environ['CERTIFICATE_BUNDLE_PATH']
try:
if os.path.exists(certifi.where()):
return certifi.where()
except Exception:
pass
logger.info('Certifi file does not exists: %s', certifi.where())
# Check if "standard" paths are valid for linux systems
if 'linux' in sys.platform:
for path in ('/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/certs/ca-certificates.crt', '/etc/ssl/ca-bundle.pem'):
if os.path.exists(path):
logger.info('Found certifi path: %s', path)
return path
return None

View File

@@ -39,7 +39,7 @@ import select
import typing
import logging
import certifi
from . import tools
HANDSHAKE_V1 = b'\x5AMGB\xA5\x01\x00'
BUFFER_SIZE = 1024 * 16 # Max buffer length
@@ -114,11 +114,16 @@ class ForwardServer(socketserver.ThreadingTCPServer):
rsocket.connect(self.remote)
rsocket.sendall(HANDSHAKE_V1) # No response expected, just the handshake
context = ssl.create_default_context()
# Do not "recompress" data, use only "base protocol" compression
context.options |= ssl.OP_NO_COMPRESSION
context.load_verify_locations(certifi.where()) # Load certifi certificates
if tools.getCaCertsFile() is not None:
context.load_verify_locations(
tools.getCaCertsFile()
) # Load certifi certificates
# If ignore remote certificate
if self.check_certificate is False:
@@ -136,7 +141,7 @@ class ForwardServer(socketserver.ThreadingTCPServer):
try:
with self.connect() as ssl_socket:
ssl_socket.sendall(HANDSHAKE_V1 + b'TEST')
ssl_socket.sendall(b'TEST')
resp = ssl_socket.recv(2)
if resp != b'OK':
raise Exception({'Invalid tunnelresponse: {resp}'})
@@ -184,7 +189,7 @@ class Handler(socketserver.BaseRequestHandler):
logger.debug('Ticket %s', self.server.ticket)
with self.server.connect() as ssl_socket:
# Send handhshake + command + ticket
ssl_socket.sendall(HANDSHAKE_V1 + b'OPEN' + self.server.ticket.encode())
ssl_socket.sendall(b'OPEN' + self.server.ticket.encode())
# Check response is OK
data = ssl_socket.recv(2)
if data != b'OK':

159
server/samples/REST4.py Normal file
View File

@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import json
import sys
import typing
import requests
rest_url = 'http://172.27.0.1:8000/uds/rest/'
session = requests.Session()
session.headers.update({'Content-Type': 'application/json'})
class RESTException(Exception):
pass
class AuthException(RESTException):
pass
class LogoutException(RESTException):
pass
# Hace login con el root, puede usarse cualquier autenticador y cualquier usuario, pero en la 1.5 solo está implementado poder hacer
# este tipo de login con el usuario "root"
def login():
# parameters = '{ "auth": "admin", "username": "root", "password": "temporal" }'
# parameters = '{ "auth": "interna", "username": "admin", "password": "temporal" }'
parameters = {'auth': 'interna', 'username': 'admin', 'password': 'temporal'}
response = session.post(rest_url + 'auth/login', json=parameters)
if not response.ok:
raise AuthException('Error logging in')
# resp contiene las cabeceras, content el contenido de la respuesta (que es json), pero aún está en formato texto
res = response.json()
print(res)
if res['result'] != 'ok': # Authentication error
raise AuthException('Authentication error')
session.headers.update({'X-Auth-Token': res['token']})
def logout():
response = session.get(rest_url + 'auth/logout')
if not response.ok:
raise LogoutException('Error logging out')
# Sample response from request_pools
# [
# {
# u'initial_srvs': 0,
# u'name': u'WinAdolfo',
# u'max_srvs': 0,
# u'comments': u'',
# u'id': 6,
# u'state': u'A',
# u'user_services_count': 3,
# u'cache_l2_srvs': 0,
# u'service_id': 9,
# u'provider_id': 2,
# u'cache_l1_srvs': 0,
# u'restrained': False}
# ]
def request_pools() -> typing.List[typing.MutableMapping[str, typing.Any]]:
response = session.get(rest_url + 'servicespools/overview')
if not response.ok:
raise RESTException('Error requesting pools')
return response.json()
def request_ticket(
username: str,
authSmallName: str,
groups: typing.Union[typing.List[str], str],
servicePool: str,
realName: typing.Optional[str] = None,
transport: typing.Optional[str] = None,
force: bool = False
) -> typing.MutableMapping[str, typing.Any]:
data = {
'username': username,
'authSmallName': authSmallName,
'groups': groups,
'servicePool': servicePool,
'force': 'true' if force else 'false'
}
if realName:
data['realname'] = realName
if transport:
data['transport'] = transport
response = session.put(
rest_url + 'tickets/create',
json=data
)
if not response.ok:
raise RESTException('Error requesting ticket')
return response.json()
if __name__ == '__main__':
# request_pools() # Not logged in, this will generate an error
login() # Will raise an exception if error
#pools = request_pools()
#for i in pools:
# print(i['id'], i['name'])
ticket = request_ticket(
username='adolfo',
authSmallName='172.27.0.1:8000',
groups=['adolfo', 'dkmaster'],
servicePool='5d045a19-54b5-541b-ba56-447b0622191c',
realName='Adolfo Gómez',
force=True
)
print(ticket)
logout()

199
server/src/requirements.txt Normal file
View File

@@ -0,0 +1,199 @@
altgraph==0.17.2
appdirs==1.4.4
asgiref==3.5.0
asttokens==2.0.5
Babel==2.9.1
backcall==0.2.0
backports.entry-points-selectable==1.1.1
bcrypt==3.2.0
beautifulsoup4==4.10.0
bitarray==2.6.0
black==22.1.0
boto3==1.21.27
botocore==1.24.27
Brotli==1.0.9
CacheControl==0.12.11
cachetools==5.0.0
cairocffi==1.3.0
CairoSVG==2.5.2
certifi==2021.10.8
cffi==1.15.0
chardet==4.0.0
charset-normalizer==2.0.12
click==8.0.4
commonmark==0.9.1
contextlib2==21.6.0
coverage==6.4.4
cryptography==36.0.2
cssselect2==0.5.0
curio==1.5
cycler==0.11.0
cyclonedx-python-lib==3.1.0
Cython==0.29.28
decorator==5.1.1
defusedxml==0.7.1
Delorean==1.0.0
distlib==0.3.4
Django==3.2.16
django-extensions==3.1.5
dnspython==2.2.1
docker==5.0.3
emrichen==0.2.3
et-xmlfile==1.1.0
executing==0.8.3
filelock==3.6.0
fonttools==4.31.2
gdown==4.4.0
gitdb==4.0.9
GitPython==3.1.27
google-api-core==2.7.1
google-auth==2.6.2
google-cloud-core==2.2.3
google-cloud-translate==3.7.2
googleapis-common-protos==1.56.0
grpcio==1.44.0
grpcio-status==1.44.0
gunicorn==20.1.0
h11==0.13.0
html5lib==1.1
huggingface-hub==0.4.0
humanize==4.0.0
idna==3.3
ipython==8.2.0
ipython-genutils==0.2.0
isodate==0.6.1
jedi==0.18.1
jmespath==1.0.0
joblib==1.2.0
jsonpath-rw==1.4.0
kiwisolver==1.4.1
ldap3==2.9.1
libvirt-python==8.1.0
lockfile==0.12.2
lxml==4.6.5
matplotlib==3.5.1
matplotlib-inline==0.1.3
mpmath==1.2.1
msgpack==1.0.4
mypy==0.942
mypy-extensions==0.4.3
mysqlclient==2.1.0
netaddr==0.8.0
ntlm-auth==1.5.0
numpy==1.22.3
openpyxl==3.0.9
ovirt-engine-sdk-python==4.5.0
packageurl-python==0.10.4
packaging==21.3
paramiko==2.10.3
parso==0.8.3
pathspec==0.9.0
pexpect==4.8.0
pickleshare==0.7.5
Pillow==9.0.1
pip-api==0.0.30
pip-requirements-parser==31.2.0
pip_audit==2.4.5
pkg_resources==0.0.0
platformdirs==2.5.1
ply==3.11
prompt-toolkit==3.0.28
proto-plus==1.20.3
protobuf==3.19.4
psutil==5.9.0
ptyprocess==0.7.0
pure-eval==0.2.2
pyaml==21.10.1
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.21
pycryptodome==3.14.1
pycurl==7.45.1
pydyf==0.1.2
Pygments==2.11.2
pyinstaller==4.10
pyinstaller-hooks-contrib==2022.3
PyJWT==2.4.0
PyNaCl==1.5.0
pyOpenSSL==22.0.0
pyparsing==3.0.7
pyphen==0.12.0
PyQt5==5.15.6
PyQt5-Qt5==5.15.2
PyQt5-sip==12.9.1
pyrad==2.4
PySide2==5.15.2.1
PySocks==1.7.1
pyspnego==0.5.1
python-dateutil==2.8.2
python-geoip-geolite2-coex==2018.1113
python-geoip-python3==1.3
python-ldap==3.4.0
python-memcached==1.59
python3-saml==1.14.0
pytz==2022.1
pytz-deprecation-shim==0.1.0.post0
pyvmomi==7.0.3
pywinrm==0.4.2
PyYAML==6.0
questionary==1.10.0
regex==2022.3.15
requests==2.27.1
requests-credssp==2.0.0
requests-ntlm==1.1.0
resolvelib==0.8.1
rich==12.6.0
roam==0.3.1
rsa==4.8
ruamel.yaml==0.17.21
ruamel.yaml.clib==0.2.6
s3transfer==0.5.2
sacremoses==0.0.49
schema==0.7.5
Send2Trash==1.8.0
sentencepiece==0.1.96
setproctitle==1.2.2
shiboken2==5.15.2.1
six==1.16.0
smmap==5.0.0
sortedcontainers==2.4.0
soupsieve==2.3.1
sqlparse==0.4.2
stack-data==0.2.0
sympy==1.10.1
tinycss2==1.1.1
tokenizers==0.11.6
toml==0.10.2
tomli==2.0.1
tqdm==4.63.1
traitlets==5.1.1
transformers==4.17.0
translate-toolkit==3.6.0
typed-ast==1.5.2
types-cryptography==3.3.18
types-enum34==1.1.8
types-ipaddress==1.0.8
types-paramiko==2.8.17
types-pycurl==7.44.7
types-python-dateutil==2.8.10
types-pytz==2021.3.7
types-pyvmomi==7.0.5
types-PyYAML==6.0.7
types-requests==2.27.15
types-urllib3==1.26.11
typing_extensions==4.1.1
tzdata==2022.1
tzlocal==4.1
urllib3==1.26.9
uvicorn==0.17.6
virtualenv==20.14.0
wcwidth==0.2.5
weasyprint==54.2
webencodings==0.5.1
websocket-client==1.3.1
whitenoise==6.0.0
xml-marshaller==1.0.2
xmlsec==1.3.12
xmltodict==0.12.0
xxhash==3.0.0
zopfli==0.2.1

16
server/src/server/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for server project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
application = get_asgi_application()

View File

@@ -8,12 +8,17 @@ import django
# calculated paths for django and the site
# used as starting points for various other paths
DJANGO_ROOT = os.path.dirname(os.path.realpath(django.__file__))
BASE_DIR = '/'.join(os.path.dirname(os.path.abspath(__file__)).split('/')[:-1]) # If used 'relpath' instead of abspath, returns path of "enterprise" instead of "openuds"
BASE_DIR = '/'.join(
os.path.dirname(os.path.abspath(__file__)).split('/')[:-1]
) # If used 'relpath' instead of abspath, returns path of "enterprise" instead of "openuds"
DEBUG = True
# USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # For testing behind a reverse proxy
SECURE_PROXY_SSL_HEADER = (
'HTTP_X_FORWARDED_PROTO',
'https',
) # For testing behind a reverse proxy
DATABASES = {
'default': {
@@ -29,12 +34,12 @@ DATABASES = {
'PASSWORD': 'PASSWOR', # Not used with sqlite3.
'HOST': 'localhost', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '3306', # Set to empty string for default. Not used with sqlite3.
# 'CONN_MAX_AGE': 600, # Enable DB Pooling, 10 minutes max connection duration
# 'CONN_MAX_AGE': 600, # Enable DB Pooling, 10 minutes max connection duration
}
}
ALLOWED_HOSTS = '*'
ALLOWED_HOSTS = ['*']
DEFAULT_AUTO_FIELD='django.db.models.AutoField'
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
@@ -54,17 +59,17 @@ LANGUAGE_CODE = 'en'
ugettext = lambda s: s
LANGUAGES = (
('es', ugettext('Spanish')),
('en', ugettext('English')),
('fr', ugettext('French')),
('de', ugettext('German')),
('pt', ugettext('Portuguese')),
('it', ugettext('Italian')),
('ar', ugettext('Arabic')),
('eu', ugettext('Basque')),
('ar', ugettext('Arabian')),
('ca', ugettext('Catalan')),
('zh-hans', ugettext('Chinese')),
('es', ugettext('Spanish')),
('en', ugettext('English')),
('fr', ugettext('French')),
('de', ugettext('German')),
('pt', ugettext('Portuguese')),
('it', ugettext('Italian')),
('ar', ugettext('Arabic')),
('eu', ugettext('Basque')),
('ar', ugettext('Arabian')),
('ca', ugettext('Catalan')),
('zh-hans', ugettext('Chinese')),
)
LANGUAGE_COOKIE_NAME = 'uds_lang'
@@ -123,15 +128,15 @@ CACHES = {
'OPTIONS': {
'MAX_ENTRIES': 5000,
'CULL_FREQUENCY': 3, # 0 = Entire cache will be erased once MAX_ENTRIES is reached, this is faster on DB. if other value, will remove 1/this number items fromm cache
}
},
},
# 'memory': {
# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
# }
# 'memory': {
# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
# }
'memory': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
}
},
}
# Related to file uploading
@@ -175,6 +180,7 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'uds.core.util.middleware.security.UDSSecurityMiddleware',
'uds.core.util.middleware.request.GlobalRequestMiddleware',
'uds.core.util.middleware.xua.XUACompatibleMiddleware',
'uds.core.util.middleware.redirect.RedirectMiddleware',
@@ -229,25 +235,16 @@ LOGGING = {
'simple': {
'format': '%(levelname)s %(asctime)s %(module)s %(funcName)s %(lineno)d %(message)s'
},
'database': {
'format': '%(levelname)s %(asctime)s Database %(message)s'
},
'auth': {
'format': '%(asctime)s %(message)s'
},
'use': {
'format': '%(asctime)s %(message)s'
},
'trace': {
'format': '%(levelname)s %(asctime)s %(message)s'
}
'database': {'format': '%(levelname)s %(asctime)s Database %(message)s'},
'auth': {'format': '%(asctime)s %(message)s'},
'use': {'format': '%(asctime)s %(message)s'},
'trace': {'format': '%(levelname)s %(asctime)s %(message)s'},
},
'handlers': {
'null': {
'level': 'DEBUG',
'class': 'logging.NullHandler',
},
'file': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
@@ -256,9 +253,8 @@ LOGGING = {
'mode': 'a',
'maxBytes': ROTATINGSIZE,
'backupCount': 3,
'encoding': 'utf-8'
'encoding': 'utf-8',
},
'database': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
@@ -267,9 +263,8 @@ LOGGING = {
'mode': 'a',
'maxBytes': ROTATINGSIZE,
'backupCount': 3,
'encoding': 'utf-8'
'encoding': 'utf-8',
},
'servicesFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
@@ -278,9 +273,8 @@ LOGGING = {
'mode': 'a',
'maxBytes': ROTATINGSIZE,
'backupCount': 3,
'encoding': 'utf-8'
'encoding': 'utf-8',
},
'workersFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
@@ -289,9 +283,8 @@ LOGGING = {
'mode': 'a',
'maxBytes': ROTATINGSIZE,
'backupCount': 3,
'encoding': 'utf-8'
'encoding': 'utf-8',
},
'authFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
@@ -300,9 +293,8 @@ LOGGING = {
'mode': 'a',
'maxBytes': ROTATINGSIZE,
'backupCount': 3,
'encoding': 'utf-8'
'encoding': 'utf-8',
},
'useFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
@@ -311,9 +303,8 @@ LOGGING = {
'mode': 'a',
'maxBytes': ROTATINGSIZE,
'backupCount': 3,
'encoding': 'utf-8'
'encoding': 'utf-8',
},
'traceFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
@@ -322,19 +313,18 @@ LOGGING = {
'mode': 'a',
'maxBytes': ROTATINGSIZE,
'backupCount': 3,
'encoding': 'utf-8'
'encoding': 'utf-8',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
'formatter': 'simple',
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'filters': ['require_debug_false']
}
'filters': ['require_debug_false'],
},
},
'loggers': {
'': {
@@ -356,12 +346,16 @@ LOGGING = {
'level': 'DEBUG',
'propagate': False,
},
# Disable fonttools (used by reports) logging (too verbose)
'fontTools': {
'handlers': ['null'],
'propagate': True,
'level': 'ERROR',
},
'uds': {
'handlers': ['file'],
'level': LOGLEVEL,
},
'uds.core.workers': {
'handlers': ['workersFile'],
'level': LOGLEVEL,
@@ -372,7 +366,6 @@ LOGGING = {
'level': LOGLEVEL,
'propagate': False,
},
'uds.services': {
'handlers': ['servicesFile'],
'level': LOGLEVEL,
@@ -395,7 +388,6 @@ LOGGING = {
'handlers': ['traceFile'],
'level': 'INFO',
'propagate': False,
}
}
},
},
}

View File

@@ -1,34 +1,16 @@
"""
WSGI config for server project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
"""
from django.core.wsgi import get_wsgi_application
import six
import os
if six.PY2:
import sys
from django.core.wsgi import get_wsgi_application
# noinspection PyCompatibility
reload(sys)
sys.setdefaultencoding('UTF-8') # @UndefinedVariable
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
application = get_wsgi_application()

View File

@@ -50,14 +50,14 @@ from .handlers import (
NotFound,
RequestError,
ResponseError,
NotSupportedError
NotSupportedError,
)
from . import processors
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core.util.request import ExtendedHttpRequest
from uds.core.util.request import ExtendedHttpRequestWithUser
logger = logging.getLogger(__name__)
@@ -70,12 +70,15 @@ class Dispatcher(View):
"""
This class is responsible of dispatching REST requests
"""
# This attribute will contain all paths--> handler relations, filled at Initialized method
services: typing.ClassVar[typing.Dict[str, typing.Any]] = {'': None} # Will include a default /rest handler, but rigth now this will be fine
services: typing.ClassVar[typing.Dict[str, typing.Any]] = {
'': None
} # Will include a default /rest handler, but rigth now this will be fine
# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements
@method_decorator(csrf_exempt)
def dispatch(self, request: 'ExtendedHttpRequest', *args, **kwargs):
def dispatch(self, request: 'ExtendedHttpRequestWithUser', *args, **kwargs):
"""
Processes the REST request and routes it wherever it needs to be routed
"""
@@ -98,7 +101,9 @@ class Dispatcher(View):
content_type = path[0].split('.')[1]
clean_path = path[0].split('.')[0]
if not clean_path: # Skip empty path elements, so /x/y == /x////y for example (due to some bugs detected on some clients)
if (
not clean_path
): # Skip empty path elements, so /x/y == /x////y for example (due to some bugs detected on some clients)
path = path[1:]
continue
@@ -115,9 +120,13 @@ class Dispatcher(View):
# Here, service points to the path
cls: typing.Optional[typing.Type[Handler]] = service['']
if cls is None:
return http.HttpResponseNotFound('Method not found', content_type="text/plain")
return http.HttpResponseNotFound(
'Method not found', content_type="text/plain"
)
processor = processors.available_processors_ext_dict.get(content_type, processors.default_processor)(request)
processor = processors.available_processors_ext_dict.get(
content_type, processors.default_processor
)(request)
# Obtain method to be invoked
http_method: str = request.method.lower() if request.method else ''
@@ -128,24 +137,40 @@ class Dispatcher(View):
handler = None
try:
handler = cls(request, full_path, http_method, processor.processParameters(), *args, **kwargs)
handler = cls(
request,
full_path,
http_method,
processor.processParameters(),
*args,
**kwargs,
)
operation: typing.Callable[[], typing.Any] = getattr(handler, http_method)
except processors.ParametersException as e:
logger.debug('Path: %s', full_path)
logger.debug('Error: %s', e)
return http.HttpResponseServerError('Invalid parameters invoking {0}: {1}'.format(full_path, e), content_type="text/plain")
return http.HttpResponseServerError(
'Invalid parameters invoking {0}: {1}'.format(full_path, e),
content_type="text/plain",
)
except AttributeError:
allowedMethods = []
for n in ['get', 'post', 'put', 'delete']:
if hasattr(handler, n):
allowedMethods.append(n)
return http.HttpResponseNotAllowed(allowedMethods, content_type="text/plain")
return http.HttpResponseNotAllowed(
allowedMethods, content_type="text/plain"
)
except AccessDenied:
return http.HttpResponseForbidden('access denied', content_type="text/plain")
return http.HttpResponseForbidden(
'access denied', content_type="text/plain"
)
except Exception:
logger.exception('error accessing attribute')
logger.debug('Getting attribute %s for %s', http_method, full_path)
return http.HttpResponseServerError('Unexcepected error', content_type="text/plain")
return http.HttpResponseServerError(
'Unexcepected error', content_type="text/plain"
)
# Invokes the handler's operation, add headers to response and returns
try:
@@ -180,12 +205,16 @@ class Dispatcher(View):
Try to register Handler subclasses that have not been inherited
"""
for cls in classes:
if not cls.__subclasses__(): # Only classes that has not been inherited will be registered as Handlers
if (
not cls.__subclasses__()
): # Only classes that has not been inherited will be registered as Handlers
if not cls.name:
name = cls.__name__.lower()
else:
name = cls.name
logger.debug('Adding handler %s for method %s in path %s', cls, name, cls.path)
logger.debug(
'Adding handler %s for method %s in path %s', cls, name, cls.path
)
service_node = Dispatcher.services # Root path
if cls.path:
for k in cls.path.split('/'):
@@ -214,7 +243,9 @@ class Dispatcher(View):
pkgpath = os.path.join(os.path.dirname(sys.modules[__name__].__file__), package)
for _, name, _ in pkgutil.iter_modules([pkgpath]):
# __import__(__name__ + '.' + package + '.' + name, globals(), locals(), [], 0)
importlib.import_module( __name__ + '.' + package + '.' + name) # import module
importlib.import_module(
__name__ + '.' + package + '.' + name
) # import module
importlib.invalidate_caches()

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -12,7 +12,7 @@
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
@@ -30,11 +30,9 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import datetime
import typing
import logging
from django.utils import timezone
from django.contrib.sessions.backends.base import SessionBase
from django.contrib.sessions.backends.db import SessionStore
@@ -46,8 +44,8 @@ from uds.core.managers import cryptoManager
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core.util.request import ExtendedHttpRequest
from uds.core.util.request import ExtendedHttpRequestWithUser
logger = logging.getLogger(__name__)
AUTH_TOKEN_HEADER = 'HTTP_X_AUTH_TOKEN'
@@ -93,31 +91,59 @@ class Handler:
"""
REST requests handler base class
"""
raw: typing.ClassVar[bool] = False # If true, Handler will return directly an HttpResponse Object
name: typing.ClassVar[typing.Optional[str]] = None # If name is not used, name will be the class name in lower case
path: typing.ClassVar[typing.Optional[str]] = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way
authenticated: typing.ClassVar[bool] = True # By default, all handlers needs authentication. Will be overwriten if needs_admin or needs_staff,
needs_admin: typing.ClassVar[bool] = False # By default, the methods will be accessible by anyone if nothing else indicated
raw: typing.ClassVar[
bool
] = False # If true, Handler will return directly an HttpResponse Object
name: typing.ClassVar[
typing.Optional[str]
] = None # If name is not used, name will be the class name in lower case
path: typing.ClassVar[
typing.Optional[str]
] = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way
authenticated: typing.ClassVar[
bool
] = True # By default, all handlers needs authentication. Will be overwriten if needs_admin or needs_staff,
needs_admin: typing.ClassVar[
bool
] = False # By default, the methods will be accessible by anyone if nothing else indicated
needs_staff: typing.ClassVar[bool] = False # By default, staff
_request: 'ExtendedHttpRequest' # It's a modified HttpRequest
_request: 'ExtendedHttpRequestWithUser' # It's a modified HttpRequest
_path: str
_operation: str
_params: typing.Any # This is a deserliazied object from request. Can be anything as 'a' or {'a': 1} or ....
_args: typing.Tuple[str, ...] # This are the "path" split by /, that is, the REST invocation arguments
_args: typing.Tuple[
str, ...
] # This are the "path" split by /, that is, the REST invocation arguments
_kwargs: typing.Dict
_headers: typing.Dict[str, str]
_session: typing.Optional[SessionStore]
_authToken: typing.Optional[str]
_user: 'User'
# method names: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'
def __init__(self, request: 'ExtendedHttpRequest', path: str, operation: str, params: typing.Any, *args: str, **kwargs):
def __init__(
self,
request: 'ExtendedHttpRequestWithUser',
path: str,
operation: str,
params: typing.Any,
*args: str,
**kwargs
):
logger.debug('Data: %s %s %s', self.__class__, self.needs_admin, self.authenticated)
if (self.needs_admin or self.needs_staff) and not self.authenticated: # If needs_admin, must also be authenticated
raise Exception('class {} is not authenticated but has needs_admin or needs_staff set!!'.format(self.__class__))
logger.debug(
'Data: %s %s %s', self.__class__, self.needs_admin, self.authenticated
)
if (
self.needs_admin or self.needs_staff
) and not self.authenticated: # If needs_admin, must also be authenticated
raise Exception(
'class {} is not authenticated but has needs_admin or needs_staff set!!'.format(
self.__class__
)
)
self._request = request
self._path = path
@@ -127,7 +153,9 @@ class Handler:
self._kwargs = kwargs
self._headers = {}
self._authToken = None
if self.authenticated: # Only retrieve auth related data on authenticated handlers
if (
self.authenticated
): # Only retrieve auth related data on authenticated handlers
try:
self._authToken = self._request.META.get(AUTH_TOKEN_HEADER, '')
self._session = SessionStore(session_key=self._authToken)
@@ -150,7 +178,6 @@ class Handler:
else:
self._user = User() # Empty user for non authenticated handlers
def headers(self) -> typing.Dict[str, str]:
"""
Returns the headers of the REST request (all)
@@ -191,16 +218,16 @@ class Handler:
@staticmethod
def storeSessionAuthdata(
session: SessionBase,
id_auth: int,
username: str,
password: str,
locale: str,
platform: str,
is_admin: bool,
staff_member: bool,
scrambler: str
):
session: SessionBase,
id_auth: int,
username: str,
password: str,
locale: str,
platform: str,
is_admin: bool,
staff_member: bool,
scrambler: str,
):
"""
Stores the authentication data inside current session
:param session: session handler (Djano user session object)
@@ -220,20 +247,20 @@ class Handler:
'locale': locale,
'platform': platform,
'is_admin': is_admin,
'staff_member': staff_member
'staff_member': staff_member,
}
def genAuthToken(
self,
id_auth: int,
username: str,
password: str,
locale: str,
platform: str,
is_admin: bool,
staf_member: bool,
scrambler: str
):
self,
id_auth: int,
username: str,
password: str,
locale: str,
platform: str,
is_admin: bool,
staf_member: bool,
scrambler: str,
):
"""
Generates the authentication token from a session, that is basically
the session key itself
@@ -244,11 +271,21 @@ class Handler:
:param staf_member: If user is considered staff member or not
"""
session = SessionStore()
Handler.storeSessionAuthdata(session, id_auth, username, password, locale, platform, is_admin, staf_member, scrambler)
Handler.storeSessionAuthdata(
session,
id_auth,
username,
password,
locale,
platform,
is_admin,
staf_member,
scrambler,
)
session.save()
self._authToken = session.session_key
self._session = session
return self._authToken
def cleanAuthToken(self) -> None:
@@ -282,13 +319,20 @@ class Handler:
self._session.accessed = True
self._session.save()
except Exception:
logger.exception('Got an exception setting session value %s to %s', key, value)
logger.exception(
'Got an exception setting session value %s to %s', key, value
)
def validSource(self) -> bool:
try:
return net.ipInNetwork(self._request.ip, GlobalConfig.ADMIN_TRUSTED_SOURCES.get(True))
return net.ipInNetwork(
self._request.ip, GlobalConfig.ADMIN_TRUSTED_SOURCES.get(True)
)
except Exception as e:
logger.warning('Error checking truted ADMIN source: "%s" does not seems to be a valid network string. Using Unrestricted access.', GlobalConfig.ADMIN_TRUSTED_SOURCES.get())
logger.warning(
'Error checking truted ADMIN source: "%s" does not seems to be a valid network string. Using Unrestricted access.',
GlobalConfig.ADMIN_TRUSTED_SOURCES.get(),
)
return True
@@ -312,8 +356,10 @@ class Handler:
authId = self.getValue('auth')
username = self.getValue('username')
# Maybe it's root user??
if (GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.getBool(True) and
username == GlobalConfig.SUPER_USER_LOGIN.get(True) and
authId == -1):
if (
GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.getBool(True)
and username == GlobalConfig.SUPER_USER_LOGIN.get(True)
and authId == -1
):
return getRootUser()
return Authenticator.objects.get(pk=authId).users.get(name=username)

View File

@@ -50,6 +50,7 @@ class Accounts(ModelHandler):
"""
Processes REST requests about accounts
"""
model = Account
detail = {'usage': AccountsUsage}
@@ -72,7 +73,7 @@ class Accounts(ModelHandler):
'tags': [tag.tag for tag in item.tags.all()],
'comments': item.comments,
'time_mark': item.time_mark,
'permission': permissions.getEffectivePermission(self._user, item)
'permission': permissions.getEffectivePermission(self._user, item),
}
def getGui(self, type_: str) -> typing.List[typing.Any]:

View File

@@ -70,7 +70,7 @@ class AccountsUsage(DetailHandler): # pylint: disable=too-many-public-methods
'running': item.user_service is not None,
'elapsed': item.elapsed,
'elapsed_timemark': item.elapsed_timemark,
'permission': perm
'permission': perm,
}
return retVal

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2020 Virtual Cable S.L.U.
# Copyright (c) 2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -64,7 +64,9 @@ class ActorTokens(ModelHandler):
def item_as_dict(self, item: ActorToken) -> typing.Dict[str, typing.Any]:
return {
'id': item.token,
'name': _('Token isued by {} from {}').format(item.username, item.hostname or item.ip),
'name': _('Token isued by {} from {}').format(
item.username, item.hostname or item.ip
),
'stamp': item.stamp,
'username': item.username,
'ip': item.ip,
@@ -73,7 +75,7 @@ class ActorTokens(ModelHandler):
'pre_command': item.pre_command,
'post_command': item.post_command,
'runonce_command': item.runonce_command,
'log_level': ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level%4]
'log_level': ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level % 4],
}
def delete(self) -> str:
@@ -83,7 +85,9 @@ class ActorTokens(ModelHandler):
if len(self._args) != 1:
raise RequestError('Delete need one and only one argument')
self.ensureAccess(self.model(), permissions.PERMISSION_ALL, root=True) # Must have write permissions to delete
self.ensureAccess(
self.model(), permissions.PERMISSION_ALL, root=True
) # Must have write permissions to delete
try:
self.model.objects.get(token=self._args[0]).delete()

View File

@@ -41,7 +41,7 @@ from uds.models import (
TicketStore,
)
#from uds.core import VERSION
# from uds.core import VERSION
from uds.core.managers import userServiceManager
from uds.core import osmanagers
from uds.core.util import log, certs
@@ -64,8 +64,10 @@ UNMANAGED = 'unmanaged' # matches the definition of UDS Actors OFC
class BlockAccess(Exception):
pass
# Helpers
# Helpers
def fixIdsList(idsList: typing.List[str]) -> typing.List[str]:
return [i.upper() for i in idsList] + [i.lower() for i in idsList]
def checkBlockedIp(ip: str) -> None:
if GlobalConfig.BLOCK_ACTOR_FAILURES.getBool() is False:
@@ -73,7 +75,11 @@ def checkBlockedIp(ip: str) -> None:
cache = Cache('actorv3')
fails = cache.get(ip) or 0
if fails > ALLOWED_FAILS:
logger.info('Access to actor from %s is blocked for %s seconds since last fail', ip, GlobalConfig.LOGIN_BLOCK.getInt())
logger.info(
'Access to actor from %s is blocked for %s seconds since last fail',
ip,
GlobalConfig.LOGIN_BLOCK.getInt(),
)
raise BlockAccess()
@@ -88,7 +94,9 @@ class ActorV3Action(Handler):
path = 'actor/v3'
@staticmethod
def actorResult(result: typing.Any = None, error: typing.Optional[str] = None) -> typing.MutableMapping[str, typing.Any]:
def actorResult(
result: typing.Any = None, error: typing.Optional[str] = None
) -> typing.MutableMapping[str, typing.Any]:
result = result or ''
res = {'result': result, 'stamp': getSqlDatetimeAsUnix()}
if error:
@@ -130,6 +138,7 @@ class Test(ActorV3Action):
"""
Tests UDS Broker actor connectivity & key
"""
name = 'test'
def post(self) -> typing.MutableMapping[str, typing.Any]:
@@ -138,7 +147,9 @@ class Test(ActorV3Action):
if self._params.get('type') == UNMANAGED:
Service.objects.get(token=self._params['token'])
else:
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
ActorToken.objects.get(
token=self._params['token']
) # Not assigned, because only needs check
except Exception:
return ActorV3Action.actorResult('invalid token')
@@ -149,6 +160,7 @@ class Register(ActorV3Action):
"""
Registers an actor
"""
authenticated = True
needs_staff = True
@@ -182,16 +194,17 @@ class Register(ActorV3Action):
runonce_command=self._params['run_once_command'],
log_level=self._params['log_level'],
token=secrets.token_urlsafe(36),
stamp=getSqlDatetime()
stamp=getSqlDatetime(),
)
return ActorV3Action.actorResult(actorToken.token)
class Initiialize(ActorV3Action):
class Initialize(ActorV3Action):
"""
Information about machine action.
Also returns the id used for the rest of the actions. (Only this one will use actor key)
"""
name = 'initialize'
def action(self) -> typing.MutableMapping[str, typing.Any]:
@@ -228,38 +241,43 @@ class Initiialize(ActorV3Action):
"""
# First, validate token...
logger.debug('Args: %s, Params: %s', self._args, self._params)
service: typing.Optional[Service] = None
try:
# First, try to locate an user service providing this token.
if self._params['type'] == UNMANAGED:
# If unmanaged, use Service locator
service: Service = Service.objects.get(token=self._params['token'])
service = Service.objects.get(token=self._params['token'])
# Locate an userService that belongs to this service and which
# Build the possible ids and make initial filter to match service
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
idsList = [x['ip'] for x in self._params['id']] + [
x['mac'] for x in self._params['id']
][:10]
dbFilter = UserService.objects.filter(deployed_service__service=service)
else:
# If not service provided token, use actor tokens
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
ActorToken.objects.get(
token=self._params['token']
) # Not assigned, because only needs check
# Build the possible ids and make initial filter to match ANY userservice with provided MAC
idsList = [i['mac'] for i in self._params['id'][:5]]
dbFilter = UserService.objects.all()
# Valid actor token, now validate access allowed. That is, look for a valid mac from the ones provided.
try:
userService: UserService = next(
iter(dbFilter.filter(
unique_id__in=idsList,
state__in=[State.USABLE, State.PREPARING]
))
# ensure idsLists has upper and lower versions for case sensitive databases
idsList = fixIdsList(idsList)
# Set full filter
dbFilter = dbFilter.filter(
unique_id__in=idsList,
state__in=[State.USABLE, State.PREPARING],
)
userService: UserService = next(iter(dbFilter))
except Exception as e:
logger.info('Unmanaged host request: %s, %s', self._params, e)
return ActorV3Action.actorResult({
'own_token': None,
'max_idle': None,
'unique_id': None,
'os': None
})
return ActorV3Action.actorResult(
{'own_token': None, 'max_idle': None, 'unique_id': None, 'os': None}
)
# Managed by UDS, get initialization data from osmanager and return it
# Set last seen actor version
@@ -269,11 +287,13 @@ class Initiialize(ActorV3Action):
if osManager:
osData = osManager.actorData(userService)
return ActorV3Action.actorResult({
'own_token': userService.uuid,
'unique_id': userService.unique_id,
'os': osData
})
return ActorV3Action.actorResult(
{
'own_token': userService.uuid,
'unique_id': userService.unique_id,
'os': osData,
}
)
except (ActorToken.DoesNotExist, Service.DoesNotExist):
raise BlockAccess()
@@ -282,6 +302,7 @@ class BaseReadyChange(ActorV3Action):
"""
Records the IP change of actor
"""
name = 'notused' # Not really important, this is not a "leaf" class and will not be directly available
def action(self) -> typing.MutableMapping[str, typing.Any]:
@@ -309,7 +330,12 @@ class BaseReadyChange(ActorV3Action):
userService.updateData(userServiceInstance)
# Store communications url also
ActorV3Action.setCommsUrl(userService, self._params['ip'], int(self._params['port']), self._params['secret'])
ActorV3Action.setCommsUrl(
userService,
self._params['ip'],
int(self._params['port']),
self._params['secret'],
)
if userService.os_state != State.USABLE:
userService.setOsState(State.USABLE)
@@ -327,13 +353,20 @@ class BaseReadyChange(ActorV3Action):
userService.setProperty('priv', privateKey)
userService.setProperty('priv_passwd', password)
return ActorV3Action.actorResult({'private_key': privateKey, 'server_certificate': cert, 'password': password})
return ActorV3Action.actorResult(
{
'private_key': privateKey,
'server_certificate': cert,
'password': password,
}
)
class IpChange(BaseReadyChange):
"""
Processses IP Change.
"""
name = 'ipchange'
@@ -341,6 +374,7 @@ class Ready(BaseReadyChange):
"""
Notifies the user service is ready
"""
name = 'ready'
def action(self) -> typing.MutableMapping[str, typing.Any]:
@@ -371,6 +405,7 @@ class Version(ActorV3Action):
Notifies the version.
Used on possible "customized" actors.
"""
name = 'version'
def action(self) -> typing.MutableMapping[str, typing.Any]:
@@ -381,16 +416,26 @@ class Version(ActorV3Action):
return ActorV3Action.actorResult()
class LoginLogout(ActorV3Action):
name = 'notused' # Not really important, this is not a "leaf" class and will not be directly available
def notifyService(self, login: bool):
def notifyService(self, isLogin: bool) -> None:
try:
# If unmanaged, use Service locator
service : 'services.Service' = Service.objects.get(token=self._params['token']).getInstance()
# Locate an userService that belongs to this service and which
service: 'services.Service' = Service.objects.get(
token=self._params['token']
).getInstance()
# We have a valid service, now we can make notifications
# Build the possible ids and make initial filter to match service
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
idsList = [x['ip'] for x in self._params['id']] + [
x['mac'] for x in self._params['id']
][:10]
# ensure idsLists has upper and lower versions for case sensitive databases
idsList = fixIdsList(idsList)
validId: typing.Optional[str] = service.getValidId(idsList)
@@ -398,19 +443,16 @@ class LoginLogout(ActorV3Action):
if not validId:
raise Exception()
# Check secret if is stored
storedInfo : typing.Optional[typing.MutableMapping[str, typing.Any]] = service.recoverIdInfo(validId)
# If no secret valid
if not storedInfo or self._params['secret'] != storedInfo['secret']:
raise Exception()
# Recover Id Info from service and validId
# idInfo = service.recoverIdInfo(validId)
# Notify Service that someone logged in/out
if login:
is_remote = self._params.get('session_type', '')[:4] in ('xrdp', 'RDP-')
if isLogin:
# Try to guess if this is a remote session
is_remote = self._params.get('session_type', '')[:3] in ('xrdp', 'RDP-')
service.processLogin(validId, remote_login=is_remote)
else:
service.processLogout(validId)
service.processLogout(validId, remote_login=is_remote)
# All right, service notified...
except Exception:
@@ -421,12 +463,29 @@ class Login(LoginLogout):
"""
Notifies user logged id
"""
name = 'login'
# payload received
# {
# 'type': actor_type or types.MANAGED,
# 'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
# 'token': token,
# 'username': username,
# 'session_type': sessionType,
# 'secret': secret or '',
# }
@staticmethod
def process_login(userService: UserService, username: str) -> typing.Optional[osmanagers.OSManager]:
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
if not userService.in_use: # If already logged in, do not add a second login (windows does this i.e.)
def process_login(
userService: UserService, username: str
) -> typing.Optional[osmanagers.OSManager]:
osManager: typing.Optional[
osmanagers.OSManager
] = userService.getOsManagerInstance()
if (
not userService.in_use
): # If already logged in, do not add a second login (windows does this i.e.)
osmanagers.OSManager.loggedIn(userService, username)
return osManager
@@ -439,7 +498,9 @@ class Login(LoginLogout):
try:
userService: UserService = self.getUserService()
osManager = Login.process_login(userService, self._params.get('username') or '')
osManager = Login.process_login(
userService, self._params.get('username') or ''
)
maxIdle = osManager.maxIdle() if osManager else None
@@ -458,30 +519,31 @@ class Login(LoginLogout):
except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
if isManaged:
raise
self.notifyService(login=True)
self.notifyService(isLogin=True)
return ActorV3Action.actorResult({
'ip': ip,
'hostname': hostname,
'dead_line': deadLine,
'max_idle': maxIdle
})
return ActorV3Action.actorResult(
{'ip': ip, 'hostname': hostname, 'dead_line': deadLine, 'max_idle': maxIdle}
)
class Logout(LoginLogout):
"""
Notifies user logged out
"""
name = 'logout'
@staticmethod
def process_logout(userService: UserService, username: str) -> None:
def process_logout(userService: UserService, username: str) -> None:
"""
This method is static so can be invoked from elsewhere
"""
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
if userService.in_use: # If already logged out, do not add a second logout (windows does this i.e.)
osManager: typing.Optional[
osmanagers.OSManager
] = userService.getOsManagerInstance()
if (
userService.in_use
): # If already logged out, do not add a second logout (windows does this i.e.)
osmanagers.OSManager.loggedOut(userService, username)
if osManager:
if osManager.isRemovableOnLogout(userService):
@@ -490,7 +552,6 @@ class Logout(LoginLogout):
else:
userService.remove()
def action(self) -> typing.MutableMapping[str, typing.Any]:
isManaged = self._params.get('type') != UNMANAGED
@@ -501,7 +562,8 @@ class Logout(LoginLogout):
except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
if isManaged:
raise
self.notifyService(login=False) # Logout notification
self.notifyService(isLogin=False) # Logout notification
return ActorV3Action.actorResult('notified') # Result is that we have not processed the logout in fact, but notified the service
return ActorV3Action.actorResult('ok')
@@ -510,13 +572,19 @@ class Log(ActorV3Action):
"""
Sends a log from the service
"""
name = 'log'
def action(self) -> typing.MutableMapping[str, typing.Any]:
logger.debug('Args: %s, Params: %s', self._args, self._params)
userService = self.getUserService()
# Adjust loglevel to own, we start on 10000 for OTHER, and received is 0 for OTHER
log.doLog(userService, int(self._params['level']) + 10000, self._params['message'], log.ACTOR)
log.doLog(
userService,
int(self._params['level']) + 10000,
self._params['message'],
log.ACTOR,
)
return ActorV3Action.actorResult('ok')
@@ -525,6 +593,7 @@ class Ticket(ActorV3Action):
"""
Gets an stored ticket
"""
name = 'ticket'
def action(self) -> typing.MutableMapping[str, typing.Any]:
@@ -532,12 +601,16 @@ class Ticket(ActorV3Action):
try:
# Simple check that token exists
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
ActorToken.objects.get(
token=self._params['token']
) # Not assigned, because only needs check
except ActorToken.DoesNotExist:
raise BlockAccess() # If too many blocks...
try:
return ActorV3Action.actorResult(TicketStore.get(self._params['ticket'], invalidate=True))
return ActorV3Action.actorResult(
TicketStore.get(self._params['ticket'], invalidate=True)
)
except TicketStore.DoesNotExist:
return ActorV3Action.actorResult(error='Invalid ticket')
@@ -550,7 +623,7 @@ class Unmanaged(ActorV3Action):
unmanaged method expect a json POST with this fields:
* id: List[dict] -> List of dictionary containing ip and mac:
* token: str -> Valid Actor "master_token" (if invalid, will return an error).
* secret: Secret for commsUrl for actor
* secret: Secret for commsUrl for actor (Cu
* port: port of the listener (normally 43910)
This method will also regenerater the public-private key pair for client, that will be needed for the new ip
@@ -570,11 +643,42 @@ class Unmanaged(ActorV3Action):
# Build the possible ids and ask service if it recognizes any of it
# If not recognized, will generate anyway the certificate, but will not be saved
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
idsList = [x['ip'] for x in self._params['id']] + [
x['mac'] for x in self._params['id']
][:10]
validId: typing.Optional[str] = service.getValidId(idsList)
# ensure idsLists has upper and lower versions for case sensitive databases
idsList = fixIdsList(idsList)
# Check if there is already an assigned user service
# To notify it logout
userService: typing.Optional[UserService]
try:
dbFilter = UserService.objects.filter(
unique_id__in=idsList,
state__in=[State.USABLE, State.PREPARING],
)
userService = next(
iter(
dbFilter.filter(
unique_id__in=idsList,
state__in=[State.USABLE, State.PREPARING],
)
)
)
except StopIteration:
userService = None
# Try to infer the ip from the valid id (that could be an IP or a MAC)
ip: str
try:
ip = next(x['ip'] for x in self._params['id'] if x['ip'] == validId or x['mac'] == validId)
ip = next(
x['ip']
for x in self._params['id']
if x['ip'] == validId or x['mac'] == validId
)
except StopIteration:
ip = self._params['id'][0]['ip'] # Get first IP if no valid ip found
@@ -583,18 +687,25 @@ class Unmanaged(ActorV3Action):
cert: typing.Dict[str, str] = {
'private_key': privateKey,
'server_certificate': certificate,
'password': password
'password': password,
}
if validId:
# Notify service of it "just start" action
service.notifyInitialization(validId)
# If id is assigned to an user service, notify "logout" to it
if userService:
Logout.process_logout(userService, 'init')
else:
# If it is not assgined to an user service, notify service
service.notifyInitialization(validId)
# Store certificate, secret & port with service if validId
service.storeIdInfo(validId, {
'cert': certificate,
'secret': self._params['secret'],
'port': int(self._params['port'])
})
service.storeIdInfo(
validId,
{
'cert': certificate,
'secret': self._params['secret'],
'port': int(self._params['port']),
},
)
return ActorV3Action.actorResult(cert)
@@ -608,7 +719,11 @@ class Notify(ActorV3Action):
def get(self) -> typing.MutableMapping[str, typing.Any]:
logger.debug('Args: %s, Params: %s', self._args, self._params)
if 'action' not in self._params or 'token' not in self._params or self._params['action'] not in ('login', 'logout'):
if (
'action' not in self._params
or 'token' not in self._params
or self._params['action'] not in ('login', 'logout')
):
# Requested login or logout
raise RequestError('Invalid parameters')

View File

@@ -32,7 +32,7 @@
"""
import logging
from django.core.cache import cache as djCache
from django.core.cache import caches
from uds.core.util.cache import Cache as uCache
from uds.REST import Handler, RequestError
@@ -57,5 +57,9 @@ class Cache(Handler):
raise RequestError('Invalid Request')
uCache.purge()
djCache.clear()
for i in ('default', 'memory'):
try:
caches[i].clear()
except Exception:
pass # Ignore non existing cache
return 'done'

View File

@@ -75,7 +75,7 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
'interval': item.interval,
'duration': item.duration,
'duration_unit': item.duration_unit,
'permission': perm
'permission': perm,
}
return retVal
@@ -98,7 +98,13 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
{'name': {'title': _('Rule name')}},
{'start': {'title': _('Starts'), 'type': 'datetime'}},
{'end': {'title': _('Ends'), 'type': 'date'}},
{'frequency': {'title': _('Repeats'), 'type': 'dict', 'dict': dict((v[0], str(v[1])) for v in freqs)}},
{
'frequency': {
'title': _('Repeats'),
'type': 'dict',
'dict': dict((v[0], str(v[1])) for v in freqs),
}
},
{'interval': {'title': _('Every'), 'type': 'callback'}},
{'duration': {'title': _('Duration'), 'type': 'callback'}},
{'comments': {'title': _('Comments')}},
@@ -108,7 +114,18 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
# Extract item db fields
# We need this fields for all
logger.debug('Saving rule %s / %s', parent, item)
fields = self.readFieldsFromParams(['name', 'comments', 'frequency', 'start', 'end', 'interval', 'duration', 'duration_unit'])
fields = self.readFieldsFromParams(
[
'name',
'comments',
'frequency',
'start',
'end',
'interval',
'duration',
'duration_unit',
]
)
if int(fields['interval']) < 1:
raise self.invalidItemException('Repeat must be greater than zero')

View File

@@ -50,6 +50,7 @@ class Calendars(ModelHandler):
"""
Processes REST requests about calendars
"""
model = Calendar
detail = {'rules': CalendarRules}
@@ -57,7 +58,14 @@ class Calendars(ModelHandler):
table_title = _('Calendars')
table_fields = [
{'name': {'title': _('Name'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-calendar text-success'}},
{
'name': {
'title': _('Name'),
'visible': True,
'type': 'icon',
'icon': 'fa fa-calendar text-success',
}
},
{'comments': {'title': _('Comments')}},
{'modified': {'title': _('Modified'), 'type': 'datetime'}},
{'tags': {'title': _('tags'), 'visible': False}},
@@ -70,7 +78,7 @@ class Calendars(ModelHandler):
'tags': [tag.tag for tag in item.tags.all()],
'comments': item.comments,
'modified': item.modified,
'permission': permissions.getEffectivePermission(self._user, item)
'permission': permissions.getEffectivePermission(self._user, item),
}
def getGui(self, type_: str) -> typing.List[typing.Any]:

View File

@@ -38,7 +38,7 @@ from django.utils.translation import ugettext as _
from django.urls import reverse
from uds.REST import Handler
from uds.REST import RequestError
from uds.models import TicketStore, user
from uds.models import TicketStore
from uds.models import User
from uds.web.util import errors
from uds.core.managers import cryptoManager, userServiceManager
@@ -46,11 +46,14 @@ from uds.core.util.config import GlobalConfig
from uds.core.services.exceptions import ServiceNotReadyError
from uds.core import VERSION as UDS_VERSION
if typing.TYPE_CHECKING:
from uds.models import UserService
logger = logging.getLogger(__name__)
CLIENT_VERSION = UDS_VERSION
REQUIRED_CLIENT_VERSION = '3.5.0'
#CLIENT_VERSION = UDS_VERSION
REQUIRED_CLIENT_VERSION = '3.6.0'
CLIENT_VERSION = REQUIRED_CLIENT_VERSION
# Enclosed methods under /client path
@@ -58,15 +61,16 @@ class Client(Handler):
"""
Processes Client requests
"""
authenticated = False # Client requests are not authenticated
@staticmethod
def result(
result: typing.Any = None,
error: typing.Optional[typing.Union[str, int]] = None,
errorCode: int = 0,
retryable: bool = False
) -> typing.Dict[str, typing.Any]:
result: typing.Any = None,
error: typing.Optional[typing.Union[str, int]] = None,
errorCode: int = 0,
retryable: bool = False,
) -> typing.Dict[str, typing.Any]:
"""
Helper method to create a "result" set for actor response
:param result: Result value to return (can be None, in which case it is converted to empty string '')
@@ -84,7 +88,9 @@ class Client(Handler):
if errorCode != 0:
# Reformat error so it is better understood by users
# error += ' (code {0:04X})'.format(errorCode)
error = _('Your service is being created. Please, wait while we complete it') + ' ({}%)'.format(int(errorCode * 25))
error = _(
'Your service is being created. Please, wait while we complete it'
) + ' ({}%)'.format(int(errorCode * 25))
res['error'] = error
res['retryable'] = '1' if retryable else '0'
@@ -106,18 +112,31 @@ class Client(Handler):
logger.debug('Client args for GET: %s', self._args)
if not self._args: # Gets version
return Client.result({
'availableVersion': CLIENT_VERSION,
'requiredVersion': REQUIRED_CLIENT_VERSION,
'downloadUrl': self._request.build_absolute_uri(reverse('page.client-download'))
})
return Client.result(
{
'availableVersion': CLIENT_VERSION,
'requiredVersion': REQUIRED_CLIENT_VERSION,
'downloadUrl': 'A new version of UDS Client is required.\nPlease, download it from Client Download section.',
}
)
if len(self._args) == 1: # Simple test
return Client.result(_('Correct'))
userService: typing.Optional['UserService'] = None
try:
ticket, scrambler = self._args # If more than 2 args, got an error. pylint: disable=unbalanced-tuple-unpacking
(
ticket,
scrambler,
) = (
self._args
) # If more than 2 args, got an error. pylint: disable=unbalanced-tuple-unpacking
hostname = self._params['hostname'] # Or if hostname is not included...
version = self._params.get('version', '0.0.0')
if version < '3.6.0':
return Client.result(error='Client version not supported.\n Please, upgrade it.')
srcIp = self._request.ip
# Ip is optional,
@@ -127,7 +146,13 @@ class Client(Handler):
except Exception:
raise RequestError('Invalid request')
logger.debug('Got Ticket: %s, scrambled: %s, Hostname: %s, Ip: %s', ticket, scrambler, hostname, srcIp)
logger.debug(
'Got Ticket: %s, scrambled: %s, Hostname: %s, Ip: %s',
ticket,
scrambler,
hostname,
srcIp,
)
try:
data = TicketStore.get(ticket)
@@ -138,33 +163,76 @@ class Client(Handler):
try:
logger.debug(data)
ip, userService, userServiceInstance, transport, transportInstance = userServiceManager().getService(
self._request.user, self._request.os, self._request.ip, data['service'], data['transport'], clientHostname=hostname
(
ip,
userService,
userServiceInstance,
transport,
transportInstance,
) = userServiceManager().getService(
self._request.user,
self._request.os,
self._request.ip,
data['service'],
data['transport'],
clientHostname=hostname,
)
logger.debug(
'Res: %s %s %s %s %s',
ip,
userService,
userServiceInstance,
transport,
transportInstance,
)
logger.debug('Res: %s %s %s %s %s', ip, userService, userServiceInstance, transport, transportInstance)
password = cryptoManager().symDecrpyt(data['password'], scrambler)
# userService.setConnectionSource(srcIp, hostname) # Store where we are accessing from so we can notify Service
if not ip:
raise ServiceNotReadyError
raise ServiceNotReadyError()
# Set "accesedByClient"
userService.setProperty('accessedByClient', '1')
# This should never happen, but it's here just in case
if not transportInstance:
raise Exception('No transport instance!!!')
transportScript, signature, params = transportInstance.getEncodedTransportScript(userService, transport, ip, self._request.os, self._request.user, password, self._request)
(
transportScript,
signature,
params,
) = transportInstance.getEncodedTransportScript(
userService,
transport,
ip,
self._request.os,
self._request.user,
password,
self._request,
)
logger.debug('Signature: %s', signature)
logger.debug('Data:#######\n%s\n###########', params)
return Client.result(result={
'script': transportScript,
'signature': signature, # It is already on base64
'params': codecs.encode(codecs.encode(json.dumps(params).encode(), 'bz2'), 'base64').decode(),
})
return Client.result(
result={
'script': transportScript,
'signature': signature, # It is already on base64
'params': codecs.encode(
codecs.encode(json.dumps(params).encode(), 'bz2'), 'base64'
).decode(),
}
)
except ServiceNotReadyError as e:
# Refresh ticket and make this retrayable
TicketStore.revalidate(ticket, 20) # Retry will be in at most 5 seconds, so 20 is fine :)
return Client.result(error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True)
TicketStore.revalidate(
ticket, 20
) # Retry will be in at most 5 seconds, so 20 is fine :)
return Client.result(
error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True
)
except Exception as e:
logger.exception("Exception")
return Client.result(error=str(e))
finally:
if userService:
userService.setProperty('accessedByClient', '1')

View File

@@ -42,9 +42,15 @@ logger = logging.getLogger(__name__)
# Pair of section/value removed from current UDS version
REMOVED = {
'UDS': (
'allowPreferencesAccess', 'customHtmlLogin', 'UDS Theme',
'UDS Theme Enhaced', 'css', 'allowPreferencesAccess',
'loginUrl', 'maxLoginTries', 'loginBlockTime'
'allowPreferencesAccess',
'customHtmlLogin',
'UDS Theme',
'UDS Theme Enhaced',
'css',
'allowPreferencesAccess',
'loginUrl',
'maxLoginTries',
'loginBlockTime',
),
'Cluster': ('Destination CPU Load', 'Migration CPU Load', 'Migration Free Memory'),
'IPAUTH': ('autoLogin',),
@@ -81,7 +87,7 @@ class Config(Handler):
'crypt': cfg.isCrypted(),
'longText': cfg.isLongText(),
'type': cfg.getType(),
'params': cfg.getParams()
'params': cfg.getParams(),
}
logger.debug('Configuration: %s', res)
return res

View File

@@ -88,7 +88,11 @@ class Connection(Handler):
# Ensure user is present on request, used by web views methods
self._request.user = self._user
return Connection.result(result=getServicesData(typing.cast(ExtendedHttpRequestWithUser, self._request)))
return Connection.result(
result=services.getServicesData(
typing.cast(ExtendedHttpRequestWithUser, self._request)
)
)
def connection(self, doNotCheck: bool = False):
idService = self._args[0]
@@ -152,7 +156,7 @@ class Connection(Handler):
self._request.ip, hostname
) # Store where we are accessing from so we can notify Service
if not ip:
if not ip or not transportInstance:
raise ServiceNotReadyError()
transportScript = transportInstance.getEncodedTransportScript(
@@ -183,7 +187,9 @@ class Connection(Handler):
self._request.user = self._user # type: ignore
self._request._cryptedpass = self._session['REST']['password'] # type: ignore
self._request._scrambler = self._request.META['HTTP_SCRAMBLER'] # type: ignore
linkInfo = services.enableService(self._request, idService=self._args[0], idTransport=self._args[1])
linkInfo = services.enableService(
self._request, idService=self._args[0], idTransport=self._args[1]
)
if linkInfo['error']:
return Connection.result(error=linkInfo['error'])
return Connection.result(result=linkInfo['url'])

View File

@@ -49,19 +49,27 @@ class Images(ModelHandler):
"""
Handles the gallery REST interface
"""
path = 'gallery'
model = Image
save_fields = ['name', 'data']
table_title = _('Image Gallery')
table_fields = [
{'thumb': {'title': _('Image'), 'visible': True, 'type': 'image', 'width': '96px'}},
{
'thumb': {
'title': _('Image'),
'visible': True,
'type': 'image',
'width': '96px',
}
},
{'name': {'title': _('Name')}},
{'size': {'title': _('Size')}},
]
def beforeSave(self, fields: typing.Dict[str, typing.Any]) -> None:
fields['data'] = Image.prepareForDb(Image.decode64(fields['data'].encode('utf8')))
fields['data'] = Image.prepareForDb(Image.decode64(fields['data']))
def afterSave(self, item: Image) -> None:
# Updates the thumbnail and re-saves it
@@ -69,17 +77,17 @@ class Images(ModelHandler):
item.updateThumbnail()
item.save()
def getGui(self, type_: str) -> typing.List[typing.Any]:
return self.addField(
self.addDefaultFields([], ['name']), {
self.addDefaultFields([], ['name']),
{
'name': 'data',
'value': '',
'label': ugettext('Image'),
'tooltip': ugettext('Image object'),
'type': gui.InputField.IMAGECHOICE_TYPE,
'order': 100, # At end
}
},
)
def item_as_dict(self, item: Image) -> typing.Dict[str, typing.Any]:
@@ -92,7 +100,9 @@ class Images(ModelHandler):
def item_as_dict_overview(self, item: Image) -> typing.Dict[str, typing.Any]:
return {
'id': item.uuid,
'size': '{}x{}, {} bytes (thumb {} bytes)'.format(item.width, item.height, len(item.data), len(item.thumb)),
'size': '{}x{}, {} bytes (thumb {} bytes)'.format(
item.width, item.height, len(item.data), len(item.thumb)
),
'name': item.name,
'thumb': item.thumb64,
}

View File

@@ -53,15 +53,22 @@ logger = logging.getLogger(__name__)
# Enclosed methods under /auth path
class Login(Handler):
"""
Responsible of user authentication
"""
path = 'auth'
authenticated = False # Public method
@staticmethod
def result(result: str = 'error', token: str = None, scrambler: str = None, error: str = None) -> typing.MutableMapping[str, typing.Any]:
def result(
result: str = 'error',
token: str = None,
scrambler: str = None,
error: str = None,
) -> typing.MutableMapping[str, typing.Any]:
res = {
'result': result,
'token': token,
@@ -109,15 +116,31 @@ class Login(Handler):
cache = Cache('RESTapi')
fails = cache.get(self._request.ip) or 0
if fails > ALLOWED_FAILS:
logger.info('Access to REST API %s is blocked for %s seconds since last fail', self._request.ip, GlobalConfig.LOGIN_BLOCK.getInt())
logger.info(
'Access to REST API %s is blocked for %s seconds since last fail',
self._request.ip,
GlobalConfig.LOGIN_BLOCK.getInt(),
)
try:
if 'auth_id' not in self._params and 'authId' not in self._params and 'authSmallName' not in self._params and 'auth' not in self._params:
if (
'auth_id' not in self._params
and 'authId' not in self._params
and 'authSmallName' not in self._params
and 'auth' not in self._params
):
raise RequestError('Invalid parameters (no auth)')
scrambler: str = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(32)) # @UndefinedVariable
authId: typing.Optional[str] = self._params.get('authId', self._params.get('auth_id', None))
authSmallName: typing.Optional[str] = self._params.get('authSmallName', None)
scrambler: str = ''.join(
random.SystemRandom().choice(string.ascii_letters + string.digits)
for _ in range(32)
) # @UndefinedVariable
authId: typing.Optional[str] = self._params.get(
'authId', self._params.get('auth_id', None)
)
authSmallName: typing.Optional[str] = self._params.get(
'authSmallName', None
)
authName: typing.Optional[str] = self._params.get('auth', None)
platform: str = self._params.get('platform', self._request.os)
@@ -126,9 +149,18 @@ class Login(Handler):
username, password = self._params['username'], self._params['password']
locale: str = self._params.get('locale', 'en')
if authName == 'admin' or authSmallName == 'admin' or authId == '00000000-0000-0000-0000-000000000000':
if GlobalConfig.SUPER_USER_LOGIN.get(True) == username and GlobalConfig.SUPER_USER_PASS.get(True) == password:
self.genAuthToken(-1, username, password, locale, platform, True, True, scrambler)
if (
authName == 'admin'
or authSmallName == 'admin'
or authId == '00000000-0000-0000-0000-000000000000'
):
if (
GlobalConfig.SUPER_USER_LOGIN.get(True) == username
and GlobalConfig.SUPER_USER_PASS.get(True) == password
):
self.genAuthToken(
-1, username, password, locale, platform, True, True, scrambler
)
return Login.result(result='ok', token=self.getAuthToken())
return Login.result(error='Invalid credentials')
@@ -149,13 +181,24 @@ class Login(Handler):
# Sleep a while here to "prottect"
time.sleep(3) # Wait 3 seconds if credentials fails for "protection"
# And store in cache for blocking for a while if fails
cache.put(self._request.ip, fails+1, GlobalConfig.LOGIN_BLOCK.getInt())
cache.put(
self._request.ip, fails + 1, GlobalConfig.LOGIN_BLOCK.getInt()
)
return Login.result(error='Invalid credentials')
return Login.result(
result='ok',
token=self.genAuthToken(auth.id, user.name, password, locale, platform, user.is_admin, user.staff_member, scrambler),
scrambler=scrambler
token=self.genAuthToken(
auth.id,
user.name,
password,
locale,
platform,
user.is_admin,
user.staff_member,
scrambler,
),
scrambler=scrambler,
)
except Exception:
@@ -169,6 +212,7 @@ class Logout(Handler):
"""
Responsible of user de-authentication
"""
path = 'auth'
authenticated = True # By default, all handlers needs authentication
@@ -190,14 +234,16 @@ class Auths(Handler):
auth: Authenticator
for auth in Authenticator.objects.all():
theType = auth.getType()
if paramAll or (theType.isCustom() is False and theType.typeType not in ('IP',)):
if paramAll or (
theType.isCustom() is False and theType.typeType not in ('IP',)
):
yield {
'authId': auth.uuid,
'authSmallName': str(auth.small_name),
'auth': auth.name,
'type': theType.typeType,
'priority': auth.priority,
'isCustom': theType.isCustom()
'isCustom': theType.isCustom(),
}
def get(self):

View File

@@ -54,6 +54,7 @@ class MetaPools(ModelHandler):
"""
Handles Services Pools REST requests
"""
model = MetaPool
detail = {
'pools': MetaServicesPool,
@@ -62,8 +63,18 @@ class MetaPools(ModelHandler):
'access': AccessCalendars,
}
save_fields = ['name', 'short_name', 'comments', 'tags',
'image_id', 'servicesPoolGroup_id', 'visible', 'policy', 'calendar_message', 'transport_grouping']
save_fields = [
'name',
'short_name',
'comments',
'tags',
'image_id',
'servicesPoolGroup_id',
'visible',
'policy',
'calendar_message',
'transport_grouping',
]
table_title = _('Meta Pools')
table_fields = [
@@ -93,8 +104,16 @@ class MetaPools(ModelHandler):
poolGroupThumb = item.servicesPoolGroup.image.thumb64
allPools = item.members.all()
userServicesCount = sum((i.pool.userServices.exclude(state__in=State.INFO_STATES).count() for i in allPools))
userServicesInPreparation = sum((i.pool.userServices.filter(state=State.PREPARING).count()) for i in allPools)
userServicesCount = sum(
(
i.pool.userServices.exclude(state__in=State.INFO_STATES).count()
for i in allPools
)
)
userServicesInPreparation = sum(
(i.pool.userServices.filter(state=State.PREPARING).count())
for i in allPools
)
val = {
'id': item.uuid,
@@ -102,7 +121,9 @@ class MetaPools(ModelHandler):
'short_name': item.short_name,
'tags': [tag.tag for tag in item.tags.all()],
'comments': item.comments,
'thumb': item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64,
'thumb': item.image.thumb64
if item.image is not None
else DEFAULT_THUMB_BASE64,
'image_id': item.image.uuid if item.image is not None else None,
'servicesPoolGroup_id': poolGroupId,
'pool_group_name': poolGroupName,
@@ -114,7 +135,7 @@ class MetaPools(ModelHandler):
'fallbackAccess': item.fallbackAccess,
'permission': permissions.getEffectivePermission(self._user, item),
'calendar_message': item.calendar_message,
'transport_grouping': item.transport_grouping
'transport_grouping': item.transport_grouping,
}
return val
@@ -123,30 +144,50 @@ class MetaPools(ModelHandler):
def getGui(self, type_: str) -> typing.List[typing.Any]:
localGUI = self.addDefaultFields([], ['name', 'short_name', 'comments', 'tags'])
for field in [{
for field in [
{
'name': 'policy',
'values': [gui.choiceItem(k, str(v)) for k, v in MetaPool.TYPES.items()],
'values': [
gui.choiceItem(k, str(v)) for k, v in MetaPool.TYPES.items()
],
'label': ugettext('Policy'),
'tooltip': ugettext('Service pool policy'),
'type': gui.InputField.CHOICE_TYPE,
'order': 100,
}, {
},
{
'name': 'image_id',
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in Image.objects.all()]),
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
for v in Image.objects.all()
]
),
'label': ugettext('Associated Image'),
'tooltip': ugettext('Image assocciated with this service'),
'type': gui.InputField.IMAGECHOICE_TYPE,
'order': 120,
'tab': gui.DISPLAY_TAB,
}, {
},
{
'name': 'servicesPoolGroup_id',
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in ServicePoolGroup.objects.all()]),
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
for v in ServicePoolGroup.objects.all()
]
),
'label': ugettext('Pool group'),
'tooltip': ugettext('Pool group for this pool (for pool classify on display)'),
'tooltip': ugettext(
'Pool group for this pool (for pool classify on display)'
),
'type': gui.InputField.IMAGECHOICE_TYPE,
'order': 121,
'tab': gui.DISPLAY_TAB,
}, {
},
{
'name': 'visible',
'value': True,
'label': ugettext('Visible'),
@@ -154,23 +195,31 @@ class MetaPools(ModelHandler):
'type': gui.InputField.CHECKBOX_TYPE,
'order': 123,
'tab': gui.DISPLAY_TAB,
}, {
},
{
'name': 'calendar_message',
'value': '',
'label': ugettext('Calendar access denied text'),
'tooltip': ugettext('Custom message to be shown to users if access is limited by calendar rules.'),
'tooltip': ugettext(
'Custom message to be shown to users if access is limited by calendar rules.'
),
'type': gui.InputField.TEXT_TYPE,
'order': 124,
'tab': gui.DISPLAY_TAB,
}, {
},
{
'name': 'transport_grouping',
'values': [gui.choiceItem(k, str(v)) for k, v in MetaPool.TRANSPORT_SELECT.items()],
'values': [
gui.choiceItem(k, str(v))
for k, v in MetaPool.TRANSPORT_SELECT.items()
],
'label': ugettext('Transport Selection'),
'tooltip': ugettext('Transport selection policy'),
'type': gui.InputField.CHOICE_TYPE,
'order': 125,
'tab': gui.DISPLAY_TAB
}]:
'tab': gui.DISPLAY_TAB,
},
]:
self.addField(localGUI, field)
return localGUI

View File

@@ -154,7 +154,7 @@ class MetaAssignedService(DetailHandler):
return UserService.objects.filter(
uuid=processUuid(userServiceId),
cache_level=0,
deployed_service__meta=metaPool,
deployed_service__in=[i.pool for i in metaPool.members.all()],
)[0]
except Exception:
pass

View File

@@ -52,12 +52,20 @@ class Networks(ModelHandler):
Processes REST requests about networks
Implements specific handling for network related requests using GUI
"""
model = Network
save_fields = ['name', 'net_string', 'tags']
table_title = _('Networks')
table_fields = [
{'name': {'title': _('Name'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-globe text-success'}},
{
'name': {
'title': _('Name'),
'visible': True,
'type': 'icon',
'icon': 'fa fa-globe text-success',
}
},
{'net_string': {'title': _('Range')}},
{'networks_count': {'title': _('Used by'), 'type': 'numeric', 'width': '8em'}},
{'tags': {'title': _('tags'), 'visible': False}},
@@ -75,14 +83,17 @@ class Networks(ModelHandler):
def getGui(self, type_: str) -> typing.List[typing.Any]:
return self.addField(
self.addDefaultFields([], ['name', 'tags']), {
self.addDefaultFields([], ['name', 'tags']),
{
'name': 'net_string',
'value': '',
'label': ugettext('Network range'),
'tooltip': ugettext('Network range. Accepts most network definitions formats (range, subnet, host, etc...'),
'tooltip': ugettext(
'Network range. Accepts most network definitions formats (range, subnet, host, etc...'
),
'type': gui.InputField.TEXT_TYPE,
'order': 100, # At end
}
},
)
def item_as_dict(self, item: Network) -> typing.Dict[str, typing.Any]:
@@ -92,5 +103,5 @@ class Networks(ModelHandler):
'tags': [tag.tag for tag in item.tags.all()],
'net_string': item.net_string,
'networks_count': item.transports.count(),
'permission': permissions.getEffectivePermission(self._user, item)
'permission': permissions.getEffectivePermission(self._user, item),
}

View File

@@ -52,6 +52,7 @@ logger = logging.getLogger(__name__)
ALLOW = 'ALLOW'
DENY = 'DENY'
class AccessCalendars(DetailHandler):
@staticmethod
def as_dict(item: 'CalendarAccess'):
@@ -67,7 +68,9 @@ class AccessCalendars(DetailHandler):
try:
if not item:
return [AccessCalendars.as_dict(i) for i in parent.calendarAccess.all()]
return AccessCalendars.as_dict(parent.calendarAccess.get(uuid=processUuid(item)))
return AccessCalendars.as_dict(
parent.calendarAccess.get(uuid=processUuid(item))
)
except Exception:
logger.exception('err: %s', item)
raise self.invalidItemException()
@@ -87,7 +90,9 @@ class AccessCalendars(DetailHandler):
uuid = processUuid(item) if item is not None else None
try:
calendar: Calendar = Calendar.objects.get(uuid=processUuid(self._params['calendarId']))
calendar: Calendar = Calendar.objects.get(
uuid=processUuid(self._params['calendarId'])
)
access: str = self._params['access'].upper()
if access not in (ALLOW, DENY):
raise Exception()
@@ -103,13 +108,24 @@ class AccessCalendars(DetailHandler):
calAccess.priority = priority
calAccess.save()
else:
parent.calendarAccess.create(calendar=calendar, access=access, priority=priority)
parent.calendarAccess.create(
calendar=calendar, access=access, priority=priority
)
log.doLog(parent, log.INFO, "Added access calendar {}/{} by {}".format(calendar.name, access, self._user.pretty_name), log.ADMIN)
log.doLog(
parent,
log.INFO,
"Added access calendar {}/{} by {}".format(
calendar.name, access, self._user.pretty_name
),
log.ADMIN,
)
def deleteItem(self, parent: 'ServicePool', item: str) -> None:
calendarAccess = parent.calendarAccess.get(uuid=processUuid(self._args[0]))
logStr = "Removed access calendar {} by {}".format(calendarAccess.calendar.name, self._user.pretty_name)
logStr = "Removed access calendar {} by {}".format(
calendarAccess.calendar.name, self._user.pretty_name
)
calendarAccess.delete()
@@ -120,7 +136,10 @@ class ActionsCalendars(DetailHandler):
"""
Processes the transports detail requests of a Service Pool
"""
custom_methods = ['execute',]
custom_methods = [
'execute',
]
@staticmethod
def as_dict(item: 'CalendarAction') -> typing.Dict[str, typing.Any]:
@@ -131,19 +150,21 @@ class ActionsCalendars(DetailHandler):
'calendarId': item.calendar.uuid,
'calendar': item.calendar.name,
'action': item.action,
'actionDescription': action.get('description'),
'actionDescription': action.get('description'),
'atStart': item.at_start,
'eventsOffset': item.events_offset,
'params': params,
'pretty_params': item.prettyParams,
'nextExecution': item.next_execution,
'lastExecution': item.last_execution
'lastExecution': item.last_execution,
}
def getItems(self, parent: 'ServicePool', item: typing.Optional[str]):
try:
if item is None:
return [ActionsCalendars.as_dict(i) for i in parent.calendaraction_set.all()]
return [
ActionsCalendars.as_dict(i) for i in parent.calendaraction_set.all()
]
i = parent.calendaraction_set.get(uuid=processUuid(item))
return ActionsCalendars.as_dict(i)
except Exception:
@@ -177,8 +198,12 @@ class ActionsCalendars(DetailHandler):
# logger.debug('Got parameters: {} {} {} {} ----> {}'.format(calendar, action, eventsOffset, atStart, params))
logStr = "Added scheduled action \"{},{},{},{},{}\" by {}".format(
calendar.name, action, eventsOffset,
atStart and 'Start' or 'End', params, self._user.pretty_name
calendar.name,
action,
eventsOffset,
atStart and 'Start' or 'End',
params,
self._user.pretty_name,
)
if uuid is not None:
@@ -191,16 +216,26 @@ class ActionsCalendars(DetailHandler):
calAction.params = params
calAction.save()
else:
CalendarAction.objects.create(calendar=calendar, service_pool=parent, action=action, at_start=atStart, events_offset=eventsOffset, params=params)
CalendarAction.objects.create(
calendar=calendar,
service_pool=parent,
action=action,
at_start=atStart,
events_offset=eventsOffset,
params=params,
)
log.doLog(parent, log.INFO, logStr, log.ADMIN)
def deleteItem(self, parent: 'ServicePool', item: str) -> None:
calendarAction = CalendarAction.objects.get(uuid=processUuid(self._args[0]))
logStr = "Removed scheduled action \"{},{},{},{},{}\" by {}".format(
calendarAction.calendar.name, calendarAction.action,
calendarAction.events_offset, calendarAction.at_start and 'Start' or 'End', calendarAction.params,
self._user.pretty_name
calendarAction.calendar.name,
calendarAction.action,
calendarAction.events_offset,
calendarAction.at_start and 'Start' or 'End',
calendarAction.params,
self._user.pretty_name,
)
calendarAction.delete()
@@ -213,11 +248,14 @@ class ActionsCalendars(DetailHandler):
calendarAction: CalendarAction = CalendarAction.objects.get(uuid=uuid)
self.ensureAccess(calendarAction, permissions.PERMISSION_MANAGEMENT)
logStr = "Launched scheduled action \"{},{},{},{},{}\" by {}".format(
calendarAction.calendar.name, calendarAction.action,
calendarAction.events_offset, calendarAction.at_start and 'Start' or 'End', calendarAction.params,
self._user.pretty_name
calendarAction.calendar.name,
calendarAction.action,
calendarAction.events_offset,
calendarAction.at_start and 'Start' or 'End',
calendarAction.params,
self._user.pretty_name,
)
calendarAction.execute()
log.doLog(parent, log.INFO, logStr, log.ADMIN)

View File

@@ -70,7 +70,7 @@ class OsManagers(ModelHandler):
'type_name': type_.name(),
'servicesTypes': type_.servicesType,
'comments': osm.comments,
'permission': permissions.getEffectivePermission(self._user, osm)
'permission': permissions.getEffectivePermission(self._user, osm),
}
def item_as_dict(self, item: OSManager) -> typing.Dict[str, typing.Any]:
@@ -79,7 +79,9 @@ class OsManagers(ModelHandler):
def checkDelete(self, item: OSManager) -> None:
# Only can delete if no ServicePools attached
if item.deployedServices.count() > 0:
raise RequestError(ugettext('Can\'t delete an OS Manager with services pools associated'))
raise RequestError(
ugettext('Can\'t delete an OS Manager with services pools associated')
)
# Types related
def enum_types(self) -> typing.Iterable[typing.Type[osmanagers.OSManager]]:
@@ -88,6 +90,9 @@ class OsManagers(ModelHandler):
# Gui related
def getGui(self, type_: str) -> typing.List[typing.Any]:
try:
return self.addDefaultFields(osmanagers.factory().lookup(type_).guiDescription(), ['name', 'comments', 'tags'])
return self.addDefaultFields(
osmanagers.factory().lookup(type_).guiDescription(), # type: ignore # may raise an exception if lookup fails
['name', 'comments', 'tags'],
)
except:
raise NotFound('type not found')

View File

@@ -50,6 +50,7 @@ class Proxies(ModelHandler):
"""
Processes REST requests about proxys
"""
model = Proxy
save_fields = ['name', 'host', 'port', 'ssl', 'check_cert', 'comments', 'tags']
@@ -74,42 +75,49 @@ class Proxies(ModelHandler):
'port': item.port,
'ssl': item.ssl,
'check_cert': item.check_cert,
'permission': permissions.getEffectivePermission(self._user, item)
'permission': permissions.getEffectivePermission(self._user, item),
}
def getGui(self, type_: str) -> typing.List[typing.Any]:
g = self.addDefaultFields([], ['name', 'comments', 'tags'])
for f in [
{
'name': 'host',
'value': '',
'label': ugettext('Host'),
'tooltip': ugettext('Server (IP or FQDN) that will serve as proxy.'),
'type': gui.InputField.TEXT_TYPE,
'order': 110,
}, {
'name': 'port',
'value': '9090',
'minValue': '0',
'label': ugettext('Port'),
'tooltip': ugettext('Port of proxy server'),
'type': gui.InputField.NUMERIC_TYPE,
'order': 111,
}, {
'name': 'ssl',
'value': True,
'label': ugettext('Use SSL'),
'tooltip': ugettext('If active, the proxied connections will be done using HTTPS'),
'type': gui.InputField.CHECKBOX_TYPE,
}, {
'name': 'check_cert',
'value': True,
'label': ugettext('Check Certificate'),
'tooltip': ugettext('If active, any SSL certificate will be checked (will not allow self signed certificates on proxy)'),
'type': gui.InputField.CHECKBOX_TYPE,
},
]:
{
'name': 'host',
'value': '',
'label': ugettext('Host'),
'tooltip': ugettext('Server (IP or FQDN) that will serve as proxy.'),
'type': gui.InputField.TEXT_TYPE,
'order': 110,
},
{
'name': 'port',
'value': '9090',
'minValue': '0',
'label': ugettext('Port'),
'tooltip': ugettext('Port of proxy server'),
'type': gui.InputField.NUMERIC_TYPE,
'order': 111,
},
{
'name': 'ssl',
'value': True,
'label': ugettext('Use SSL'),
'tooltip': ugettext(
'If active, the proxied connections will be done using HTTPS'
),
'type': gui.InputField.CHECKBOX_TYPE,
},
{
'name': 'check_cert',
'value': True,
'label': ugettext('Check Certificate'),
'tooltip': ugettext(
'If active, any SSL certificate will be checked (will not allow self signed certificates on proxy)'
),
'type': gui.InputField.CHECKBOX_TYPE,
},
]:
self.addField(g, f)
return g

View File

@@ -41,7 +41,17 @@ from uds import reports
logger = logging.getLogger(__name__)
VALID_PARAMS = ('authId', 'authSmallName', 'auth', 'username', 'realname', 'password', 'groups', 'servicePool', 'transport')
VALID_PARAMS = (
'authId',
'authSmallName',
'auth',
'username',
'realname',
'password',
'groups',
'servicePool',
'transport',
)
# Enclosed methods under /actor path
@@ -49,14 +59,21 @@ class Reports(model.BaseModelHandler):
"""
Processes actor requests
"""
needs_admin = True # By default, staff is lower level needed
table_title = _('Available reports')
table_fields = [
{'group': {'title': _('Group')}},
{'name': {'title': _('Name')}}, # Will process this field on client in fact, not sent by server
{'description': {'title': _('Description')}}, # Will process this field on client in fact, not sent by server
{'mime_type': {'title': _('Generates')}}, # Will process this field on client in fact, not sent by server
{
'name': {'title': _('Name')}
}, # Will process this field on client in fact, not sent by server
{
'description': {'title': _('Description')}
}, # Will process this field on client in fact, not sent by server
{
'mime_type': {'title': _('Generates')}
}, # Will process this field on client in fact, not sent by server
]
# Field from where to get "class" and prefix for that class, so this will generate "row-state-A, row-state-X, ....
table_row_style = {'field': 'state', 'prefix': 'row-state-'}
@@ -85,7 +102,9 @@ class Reports(model.BaseModelHandler):
if self._args[0] == model.OVERVIEW:
return list(self.getItems())
elif self._args[0] == model.TABLEINFO:
return self.processTableFields(self.table_title, self.table_fields, self.table_row_style)
return self.processTableFields(
self.table_title, self.table_fields, self.table_row_style
)
if nArgs == 2:
if self._args[0] == model.GUI:
@@ -97,7 +116,12 @@ class Reports(model.BaseModelHandler):
"""
Processes a PUT request
"""
logger.debug('method PUT for %s, %s, %s', self.__class__.__name__, self._args, self._params)
logger.debug(
'method PUT for %s, %s, %s',
self.__class__.__name__,
self._args,
self._params,
)
if len(self._args) != 1:
raise self.invalidRequestException()
@@ -112,7 +136,7 @@ class Reports(model.BaseModelHandler):
'mime_type': report.mime_type,
'encoded': report.encoded,
'filename': report.filename,
'data': result
'data': result,
}
return data
@@ -126,7 +150,9 @@ class Reports(model.BaseModelHandler):
return sorted(report.guiDescription(report), key=lambda f: f['gui']['order'])
# Returns the list of
def getItems(self, *args, **kwargs) -> typing.Generator[typing.Dict[str, typing.Any], None, None]:
def getItems(
self, *args, **kwargs
) -> typing.Generator[typing.Dict[str, typing.Any], None, None]:
for i in reports.availableReports:
yield {
'id': i.getUuid(),
@@ -134,5 +160,5 @@ class Reports(model.BaseModelHandler):
'encoded': i.encoded,
'group': i.translated_group(),
'name': i.translated_name(),
'description': i.translated_description()
'description': i.translated_description(),
}

View File

@@ -50,6 +50,7 @@ class ServicesPoolGroups(ModelHandler):
"""
Handles the gallery REST interface
"""
# needs_admin = True
path = 'gallery'
@@ -59,7 +60,14 @@ class ServicesPoolGroups(ModelHandler):
table_title = _('Services Pool Groups')
table_fields = [
{'priority': {'title': _('Priority'), 'type': 'numeric', 'width': '6em'}},
{'thumb': {'title': _('Image'), 'visible': True, 'type': 'image', 'width': '96px'}},
{
'thumb': {
'title': _('Image'),
'visible': True,
'type': 'image',
'width': '96px',
}
},
{'name': {'title': _('Name')}},
{'comments': {'title': _('Comments')}},
]
@@ -79,14 +87,22 @@ class ServicesPoolGroups(ModelHandler):
def getGui(self, type_: str) -> typing.List[typing.Any]:
localGui = self.addDefaultFields([], ['name', 'comments', 'priority'])
for field in [{
for field in [
{
'name': 'image_id',
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in Image.objects.all()]),
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
for v in Image.objects.all()
]
),
'label': ugettext('Associated Image'),
'tooltip': ugettext('Image assocciated with this service'),
'type': gui.InputField.IMAGECHOICE_TYPE,
'order': 102,
}]:
}
]:
self.addField(localGui, field)
return localGui
@@ -100,7 +116,9 @@ class ServicesPoolGroups(ModelHandler):
'image_id': item.image.uuid if item.image else None,
}
def item_as_dict_overview(self, item: ServicePoolGroup) -> typing.Dict[str, typing.Any]:
def item_as_dict_overview(
self, item: ServicePoolGroup
) -> typing.Dict[str, typing.Any]:
return {
'id': item.uuid,
'priority': item.priority,

View File

@@ -54,10 +54,13 @@ from uds.models.calendar_action import (
CALENDAR_ACTION_PUBLISH,
CALENDAR_ACTION_ADD_TRANSPORT,
CALENDAR_ACTION_DEL_TRANSPORT,
CALENDAR_ACTION_DEL_ALL_TRANSPORTS,
CALENDAR_ACTION_ADD_GROUP,
CALENDAR_ACTION_DEL_GROUP,
CALENDAR_ACTION_DEL_ALL_GROUPS,
CALENDAR_ACTION_IGNORE_UNUSED,
CALENDAR_ACTION_REMOVE_USERSERVICES,
CALENDAR_ACTION_REMOVE_STUCK_USERSERVICES,
)
from uds.core.managers import userServiceManager
@@ -466,7 +469,7 @@ class ServicesPools(ModelHandler):
{
'name': 'max_srvs',
'value': '0',
'minValue': '1',
'minValue': '0',
'label': ugettext('Maximum number of services to provide'),
'tooltip': ugettext(
'Maximum number of service (assigned and L1 cache) that can be created for this service'
@@ -536,16 +539,8 @@ class ServicesPools(ModelHandler):
for k, v in serviceType.cacheConstrains.items():
fields[k] = v
if serviceType.maxDeployed != -1:
fields['max_srvs'] = min(
(int(fields['max_srvs']), serviceType.maxDeployed)
)
fields['initial_srvs'] = min(
int(fields['initial_srvs']), serviceType.maxDeployed
)
fields['cache_l1_srvs'] = min(
int(fields['cache_l1_srvs']), serviceType.maxDeployed
)
if serviceType.usesCache_L2 is False:
fields['cache_l2_srvs'] = 0
if serviceType.usesCache is False:
for k in (
@@ -555,9 +550,22 @@ class ServicesPools(ModelHandler):
'max_srvs',
):
fields[k] = 0
else: # uses cache, adjust values
fields['max_srvs'] = int(fields['max_srvs']) or 1 # ensure max_srvs is at least 1
fields['initial_srvs'] = int(fields['initial_srvs'])
fields['cache_l1_srvs'] = int(fields['cache_l1_srvs'])
if serviceType.maxDeployed != -1:
fields['max_srvs'] = min(
(fields['max_srvs'], serviceType.maxDeployed)
)
fields['initial_srvs'] = min(
fields['initial_srvs'], serviceType.maxDeployed
)
fields['cache_l1_srvs'] = min(
fields['cache_l1_srvs'], serviceType.maxDeployed
)
if serviceType.usesCache_L2 is False:
fields['cache_l2_srvs'] = 0
except Exception:
raise RequestError(ugettext('This service requires an OS Manager'))
@@ -668,14 +676,17 @@ class ServicesPools(ModelHandler):
validActions += (
CALENDAR_ACTION_ADD_TRANSPORT,
CALENDAR_ACTION_DEL_TRANSPORT,
CALENDAR_ACTION_DEL_ALL_TRANSPORTS,
CALENDAR_ACTION_ADD_GROUP,
CALENDAR_ACTION_DEL_GROUP,
CALENDAR_ACTION_DEL_ALL_GROUPS
)
# Advanced actions
validActions += (
CALENDAR_ACTION_IGNORE_UNUSED,
CALENDAR_ACTION_REMOVE_USERSERVICES,
CALENDAR_ACTION_REMOVE_STUCK_USERSERVICES,
)
return validActions

View File

@@ -64,16 +64,10 @@ class ServicesUsage(DetailHandler):
if item.user is None:
owner = ''
owner_info = {
'auth_id': '',
'user_id': ''
}
owner_info = {'auth_id': '', 'user_id': ''}
else:
owner = item.user.pretty_name
owner_info = {
'auth_id': item.user.manager.uuid,
'user_id': item.user.uuid
}
owner_info = {'auth_id': item.user.manager.uuid, 'user_id': item.user.uuid}
return {
'id': item.uuid,
@@ -90,19 +84,30 @@ class ServicesUsage(DetailHandler):
'ip': props.get('ip', _('unknown')),
'source_host': item.src_hostname,
'source_ip': item.src_ip,
'in_use': item.in_use
'in_use': item.in_use,
}
def getItems(self, parent: 'Provider', item: typing.Optional[str]):
try:
if item is None:
userServicesQuery = UserService.objects.filter(deployed_service__service__provider=parent)
userServicesQuery = UserService.objects.filter(
deployed_service__service__provider=parent
)
else:
userServicesQuery = UserService.objects.filter(deployed_service__service_uuid=processUuid(item))
userServicesQuery = UserService.objects.filter(
deployed_service__service_uuid=processUuid(item)
)
return [ServicesUsage.itemToDict(k) for k in userServicesQuery.filter(state=State.USABLE).order_by('creation_date').
prefetch_related('deployed_service').prefetch_related('deployed_service__service').prefetch_related('properties').
prefetch_related('user').prefetch_related('user__manager')]
return [
ServicesUsage.itemToDict(k)
for k in userServicesQuery.filter(state=State.USABLE)
.order_by('creation_date')
.prefetch_related('deployed_service')
.prefetch_related('deployed_service__service')
.prefetch_related('properties')
.prefetch_related('user')
.prefetch_related('user__manager')
]
except Exception:
logger.exception('getItems')
@@ -131,7 +136,9 @@ class ServicesUsage(DetailHandler):
def deleteItem(self, parent: 'Provider', item: str) -> None:
userService: UserService
try:
userService = UserService.objects.get(uuid=processUuid(item), deployed_service__service__provider=parent)
userService = UserService.objects.get(
uuid=processUuid(item), deployed_service__service__provider=parent
)
except Exception:
raise self.invalidItemException()

View File

@@ -53,13 +53,16 @@ if typing.TYPE_CHECKING:
cache = Cache('StatsDispatcher')
# Enclosed methods under /stats path
POINTS = 300
SINCE = 30 # Days, if higer values used, ensure mysql/mariadb has a bigger sort buffer
POINTS = 150
SINCE = 7 # Days, if higer values used, ensure mysql/mariadb has a bigger sort buffer
USE_MAX = True
CACHE_TIME = SINCE * 24 * 3600 // POINTS
def getServicesPoolsCounters(
servicePool: typing.Optional[models.ServicePool], counter_type: int, since_days: int = SINCE
servicePool: typing.Optional[models.ServicePool],
counter_type: int,
since_days: int = SINCE,
) -> typing.List[typing.Mapping[str, typing.Any]]:
try:
cacheKey = (
@@ -91,7 +94,7 @@ def getServicesPoolsCounters(
val.append({'stamp': x[0], 'value': int(x[1])})
logger.debug('val: %s', val)
if len(val) >= 2:
cache.put(cacheKey, codecs.encode(pickle.dumps(val), 'zip'), 600)
cache.put(cacheKey, codecs.encode(pickle.dumps(val), 'zip'), CACHE_TIME*2)
else:
val = [{'stamp': since, 'value': 0}, {'stamp': to, 'value': 0}]
else:
@@ -142,14 +145,18 @@ class System(Handler):
pool: typing.Optional[models.ServicePool] = None
if len(self._args) == 3:
try:
pool = models.ServicePool.objects.get(uuid=processUuid(self._args[2]))
pool = models.ServicePool.objects.get(
uuid=processUuid(self._args[2])
)
except Exception:
pool = None
# If pool is None, needs admin also
if not pool and not self._user.is_admin:
raise AccessDenied()
# Check permission for pool..
if not permissions.checkPermissions(self._user, typing.cast('Model', pool), permissions.PERMISSION_READ):
if not permissions.checkPermissions(
self._user, typing.cast('Model', pool), permissions.PERMISSION_READ
):
raise AccessDenied()
if self._args[0] == 'stats':
if self._args[1] == 'assigned':
@@ -160,9 +167,15 @@ class System(Handler):
return getServicesPoolsCounters(pool, counters.CT_CACHED)
elif self._args[1] == 'complete':
return {
'assigned': getServicesPoolsCounters(pool, counters.CT_ASSIGNED, since_days=7),
'inuse': getServicesPoolsCounters(pool, counters.CT_INUSE, since_days=7),
'cached': getServicesPoolsCounters(pool, counters.CT_CACHED, since_days=7),
'assigned': getServicesPoolsCounters(
pool, counters.CT_ASSIGNED, since_days=7
),
'inuse': getServicesPoolsCounters(
pool, counters.CT_INUSE, since_days=7
),
'cached': getServicesPoolsCounters(
pool, counters.CT_CACHED, since_days=7
),
}
raise RequestError('invalid request')

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2019 Virtual Cable S.L.
# Copyright (c) 2014-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -44,10 +44,20 @@ from uds.core.util import tools
logger = logging.getLogger(__name__)
# Valid parameters accepted by ticket creation method
VALID_PARAMS = (
'authId', 'authTag', 'authSmallName', 'auth', 'username',
'realname', 'password', 'groups', 'servicePool', 'transport',
'force', 'userIp'
'authId',
'authTag',
'authSmallName',
'auth',
'username',
'realname',
'password',
'groups',
'servicePool',
'transport', # Admited to be backwards compatible, but not used. Will be removed on a future release.
'force',
'userIp',
)
@@ -67,15 +77,18 @@ class Tickets(Handler):
password:
groups:
servicePool:
transport:
transport: Ignored. Transport must be auto-detected on ticket auth
force: If "1" or "true" will ensure that:
- Groups exists on authenticator
- servicePool has these groups in it's allowed list
"""
needs_admin = True # By default, staff is lower level needed
@staticmethod
def result(result: str = '', error: typing.Optional[str] = None) -> typing.Dict[str, typing.Any]:
def result(
result: str = '', error: typing.Optional[str] = None
) -> typing.Dict[str, typing.Any]:
"""
Returns a result for a Ticket request
"""
@@ -112,7 +125,9 @@ class Tickets(Handler):
raise RequestError('Invalid parameters (no auth)')
# Must be invoked as '/rest/ticket/create, with "username", ("authId" or ("authSmallName" or "authTag"), "groups" (array) and optionally "time" (in seconds) as paramteres
def put(self): # pylint: disable=too-many-locals,too-many-branches,too-many-statements
def put(
self,
): # pylint: disable=too-many-locals,too-many-branches,too-many-statements
"""
Processes put requests, currently only under "create"
"""
@@ -124,40 +139,57 @@ class Tickets(Handler):
if 'username' not in self._params or 'groups' not in self._params:
raise RequestError('Invalid parameters')
force: bool = self._params.get('force', '0') in ('1', 'true', 'True')
force: bool = self._params.get('force', '0') in ('1', 'true', 'True', True)
userIp: typing.Optional[str] = self._params.get('userIp', None)
try:
servicePoolId = None
transportId = None
authId = self._params.get('authId', None)
authName = self._params.get('auth', None)
authTag = self._params.get('authTag', self._params.get('authSmallName', None))
authTag = self._params.get(
'authTag', self._params.get('authSmallName', None)
)
# Will raise an exception if no auth found
if authId:
auth = models.Authenticator.objects.get(uuid=processUuid(authId.lower()))
auth = models.Authenticator.objects.get(
uuid=processUuid(authId.lower())
)
elif authName:
auth = models.Authenticator.objects.get(name=authName)
else:
auth = models.Authenticator.objects.get(small_name=authTag)
username: str = self._params['username']
password: str = self._params.get('password', '') # Some machines needs password, depending on configuration
password: str = self._params.get(
'password', ''
) # Some machines needs password, depending on configuration
groupIds: typing.List[str] = []
for groupName in tools.asList(self._params['groups']):
try:
groupIds.append(auth.groups.get(name=groupName).uuid)
except Exception:
logger.info('Group %s from ticket does not exists on auth %s, forced creation: %s', groupName, auth, force)
logger.info(
'Group %s from ticket does not exists on auth %s, forced creation: %s',
groupName,
auth,
force,
)
if force: # Force creation by call
groupIds.append(auth.groups.create(name=groupName, comments='Autocreated form ticket by using force paratemeter').uuid)
groupIds.append(
auth.groups.create(
name=groupName,
comments='Autocreated form ticket by using force paratemeter',
).uuid
)
if not groupIds: # No valid group in groups names
raise RequestError('Authenticator does not contain ANY of the requested groups and force is not used')
raise RequestError(
'Authenticator does not contain ANY of the requested groups and force is not used'
)
time = int(self._params.get('time', 60))
time = 60 if time < 1 else time
@@ -166,55 +198,50 @@ class Tickets(Handler):
if 'servicePool' in self._params:
# Check if is pool or metapool
poolUuid = processUuid(self._params['servicePool'])
pool : typing.Union[models.ServicePool, models.MetaPool]
pool: typing.Union[models.ServicePool, models.MetaPool]
try:
pool = typing.cast(models.MetaPool, models.MetaPool.objects.get(uuid=poolUuid)) # If not an metapool uuid, will process it as a servicePool
pool = typing.cast(
models.MetaPool, models.MetaPool.objects.get(uuid=poolUuid)
) # If not an metapool uuid, will process it as a servicePool
if force:
# First, add groups to metapool
for addGrp in set(groupIds) - set(pool.assignedGroups.values_list('uuid', flat=True)):
for addGrp in set(groupIds) - set(
pool.assignedGroups.values_list('uuid', flat=True)
):
pool.assignedGroups.add(auth.groups.get(uuid=addGrp))
# And now, to ALL metapool members
for metaMember in pool.members.all():
# First, add groups to metapool
for addGrp in set(groupIds) - set(metaMember.pool.assignedGroups.values_list('uuid', flat=True)):
metaMember.assignedGroups.add(auth.groups.get(uuid=addGrp))
# Now add groups to pools
for addGrp in set(groupIds) - set(
metaMember.pool.assignedGroups.values_list(
'uuid', flat=True
)
):
metaMember.pool.assignedGroups.add(
auth.groups.get(uuid=addGrp)
)
# For metapool, transport is ignored..
servicePoolId = 'M' + pool.uuid
transportId = 'meta'
except models.MetaPool.DoesNotExist:
pool = typing.cast(models.ServicePool, models.ServicePool.objects.get(uuid=poolUuid))
pool = typing.cast(
models.ServicePool,
models.ServicePool.objects.get(uuid=poolUuid),
)
# If forced that servicePool must honor groups
if force:
for addGrp in set(groupIds) - set(pool.assignedGroups.values_list('uuid', flat=True)):
for addGrp in set(groupIds) - set(
pool.assignedGroups.values_list('uuid', flat=True)
):
pool.assignedGroups.add(auth.groups.get(uuid=addGrp))
if 'transport' in self._params:
transport: models.Transport = models.Transport.objects.get(uuid=processUuid(self._params['transport']))
try:
pool.validateTransport(transport)
except Exception:
logger.error('Transport %s is not valid for Service Pool %s', transport.name, pool.name)
raise Exception('Invalid transport for Service Pool')
else:
transport = models.Transport(uuid=None)
if userIp:
for v in pool.transports.order_by('priority'):
if v.validForIp(userIp):
transport = v
break
if transport.uuid is None:
logger.error('Service pool %s does not has valid transports for ip %s', pool.name, userIp)
raise Exception('Service pool does not has any valid transports for ip {}'.format(userIp))
servicePoolId = 'F' + pool.uuid
transportId = transport.uuid
except models.Authenticator.DoesNotExist:
return Tickets.result(error='Authenticator does not exists')
except models.ServicePool.DoesNotExist:
@@ -231,7 +258,6 @@ class Tickets(Handler):
'groups': groupIds,
'auth': auth.uuid,
'servicePool': servicePoolId,
'transport': transportId,
}
ticket = models.TicketStore.create(data)

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2019 Virtual Cable S.L.
# Copyright (c) 2014-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -12,7 +12,7 @@
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
@@ -50,7 +50,15 @@ logger = logging.getLogger(__name__)
class Transports(ModelHandler):
model = Transport
save_fields = ['name', 'comments', 'tags', 'priority', 'nets_positive', 'allowed_oss', 'label']
save_fields = [
'name',
'comments',
'tags',
'priority',
'nets_positive',
'allowed_oss',
'label',
]
table_title = _('Transports')
table_fields = [
@@ -58,7 +66,13 @@ class Transports(ModelHandler):
{'name': {'title': _('Name'), 'visible': True, 'type': 'iconType'}},
{'type_name': {'title': _('Type')}},
{'comments': {'title': _('Comments')}},
{'pools_count': {'title': _('Service Pools'), 'type': 'numeric', 'width': '6em'}},
{
'pools_count': {
'title': _('Service Pools'),
'type': 'numeric',
'width': '6em',
}
},
{'allowed_oss': {'title': _('Devices'), 'width': '8em'}},
{'tags': {'title': _('tags'), 'visible': False}},
]
@@ -72,52 +86,90 @@ class Transports(ModelHandler):
if not transport:
raise self.invalidItemException()
field = self.addDefaultFields(transport.guiDescription(), ['name', 'comments', 'tags', 'priority'])
field = self.addField(field, {
'name': 'nets_positive',
'value': True,
'label': ugettext('Network access'),
'tooltip': ugettext('If checked, the transport will be enabled for the selected networks. If unchecked, transport will be disabled for selected networks'),
'type': 'checkbox',
'order': 100, # At end
})
field = self.addField(field, {
'name': 'networks',
'value': [],
'values': sorted([{'id': x.uuid, 'text': x.name} for x in Network.objects.all()], key=lambda x: x['text'].lower()),
'label': ugettext('Networks'),
'tooltip': ugettext('Networks associated with this transport. If No network selected, will mean "all networks"'),
'type': 'multichoice',
'order': 101
})
field = self.addField(field, {
'name': 'allowed_oss',
'value': [],
'values': sorted([{'id': x, 'text': x.replace('CrOS', 'Chrome OS')} for x in OsDetector.knownOss], key=lambda x: x['text'].lower()),
'label': ugettext('Allowed Devices'),
'tooltip': ugettext('If empty, any kind of device compatible with this transport will be allowed. Else, only devices compatible with selected values will be allowed'),
'type': 'multichoice',
'order': 102
})
field = self.addField(field, {
'name': 'pools',
'value': [],
'values': [{'id': x.uuid, 'text': x.name} for x in ServicePool.objects.all().order_by('name') if transport.protocol in x.service.getType().allowedProtocols],
'label': ugettext('Service Pools'),
'tooltip': ugettext('Currently assigned services pools'),
'type': 'multichoice',
'order': 103
})
field = self.addField(field, {
'name': 'label',
'length': 32,
'value': '',
'label': ugettext('Label'),
'tooltip': ugettext('Metapool transport label (only used on metapool transports grouping)'),
'type': 'text',
'order': 201,
'tab': gui.ADVANCED_TAB
})
field = self.addDefaultFields(
transport.guiDescription(), ['name', 'comments', 'tags', 'priority']
)
field = self.addField(
field,
{
'name': 'nets_positive',
'value': True,
'label': ugettext('Network access'),
'tooltip': ugettext(
'If checked, the transport will be enabled for the selected networks. If unchecked, transport will be disabled for selected networks'
),
'type': 'checkbox',
'order': 100, # At end
},
)
field = self.addField(
field,
{
'name': 'networks',
'value': [],
'values': sorted(
[{'id': x.uuid, 'text': x.name} for x in Network.objects.all()],
key=lambda x: x['text'].lower(),
),
'label': ugettext('Networks'),
'tooltip': ugettext(
'Networks associated with this transport. If No network selected, will mean "all networks"'
),
'type': 'multichoice',
'order': 101,
},
)
field = self.addField(
field,
{
'name': 'allowed_oss',
'value': [],
'values': sorted(
[
{'id': x.name, 'text': x.name}
for x in OsDetector.knownOss
],
key=lambda x: x['text'].lower(),
),
'label': ugettext('Allowed Devices'),
'tooltip': ugettext(
'If empty, any kind of device compatible with this transport will be allowed. Else, only devices compatible with selected values will be allowed'
),
'type': 'multichoice',
'order': 102,
},
)
field = self.addField(
field,
{
'name': 'pools',
'value': [],
'values': [
{'id': x.uuid, 'text': x.name}
for x in ServicePool.objects.all().order_by('name')
if transport.protocol in x.service.getType().allowedProtocols
],
'label': ugettext('Service Pools'),
'tooltip': ugettext('Currently assigned services pools'),
'type': 'multichoice',
'order': 103,
},
)
field = self.addField(
field,
{
'name': 'label',
'length': 32,
'value': '',
'label': ugettext('Label'),
'tooltip': ugettext(
'Metapool transport label (only used on metapool transports grouping)'
),
'type': 'text',
'order': 201,
'tab': ugettext(gui.ADVANCED_TAB),
},
)
return field
@@ -133,14 +185,16 @@ class Transports(ModelHandler):
'nets_positive': item.nets_positive,
'label': item.label,
'networks': [{'id': n.uuid} for n in item.networks.all()],
'allowed_oss': [{'id': x} for x in item.allowed_oss.split(',')] if item.allowed_oss != '' else [],
'allowed_oss': [{'id': x} for x in item.allowed_oss.split(',')]
if item.allowed_oss != ''
else [],
'pools': pools,
'pools_count': len(pools),
'deployed_count': item.deployedServices.count(),
'type': type_.type(),
'type_name': type_.name(),
'protocol': type_.protocol,
'permission': permissions.getEffectivePermission(self._user, item)
'permission': permissions.getEffectivePermission(self._user, item),
}
def beforeSave(self, fields: typing.Dict[str, typing.Any]) -> None:

View File

@@ -38,18 +38,21 @@ from uds.core import managers
from uds.REST import Handler
from uds.REST import AccessDenied
from uds.core.auths.auth import isTrustedSource
from uds.core.util import log, net, request
from uds.core.util import log, net
from uds.core.util.stats import events
logger = logging.getLogger(__name__)
MAX_SESSION_LENGTH = 60*60*24*7
MAX_SESSION_LENGTH = (
60 * 60 * 24 * 7 * 2
) # Two weeks is max session length for a tunneled connection
# Enclosed methods under /tunnel path
class TunnelTicket(Handler):
"""
Processes tunnel requests
"""
authenticated = False # Client requests are not authenticated
path = 'tunnel'
name = 'ticket'
@@ -59,7 +62,10 @@ class TunnelTicket(Handler):
Processes get requests, currently none
"""
logger.debug(
'Tunnel parameters for GET: %s (%s) from %s', self._args, self._params, self._request.ip
'Tunnel parameters for GET: %s (%s) from %s',
self._args,
self._params,
self._request.ip,
)
if (
@@ -73,52 +79,69 @@ class TunnelTicket(Handler):
# Take token from url
token = self._args[2][:48]
if not models.TunnelToken.validateToken(token):
if self._args[1][:4] == 'stop':
# "Eat" invalid stop requests, because Applications does not like them
return {}
logger.error('Invalid token %s from %s', token, self._request.ip)
raise AccessDenied()
# Try to get ticket from DB
try:
user, userService, host, port, extra = models.TicketStore.get_for_tunnel(
self._args[0]
)
host = host or ''
data = {}
if self._args[1][:4] == 'stop':
sent, recv = self._params['sent'], self._params['recv']
# Ensures extra exists...
extra = extra or {}
now = models.getSqlDatetimeAsUnix()
totalTime = now - extra.get('b', now-1)
totalTime = now - extra.get('b', now - 1)
msg = f'User {user.name} stopped tunnel {extra.get("t", "")[:8]}... to {host}:{port}: u:{sent}/d:{recv}/t:{totalTime}.'
log.doLog(user.manager, log.INFO, msg)
log.doLog(userService, log.INFO, msg)
# Try to log Close event
try:
# If pool does not exists, do not log anything
events.addEvent(
userService.deployed_service,
events.ET_TUNNEL_CLOSE,
duration=totalTime,
sent=sent,
received=recv,
tunnel=extra.get('t', 'unknown'),
)
except Exception:
pass
else:
if net.ipToLong(self._args[1][:32]) == 0:
raise Exception('Invalid from IP')
events.addEvent(
userService.deployed_service,
events.ET_TUNNEL_ACCESS,
events.ET_TUNNEL_OPEN,
username=user.pretty_name,
srcip=self._args[1],
dstip=host,
uniqueid=userService.unique_id,
tunnel=self._args[0],
)
msg = f'User {user.name} started tunnel {self._args[0][:8]}... to {host}:{port} from {self._args[1]}.'
log.doLog(user.manager, log.INFO, msg)
log.doLog(userService, log.INFO, msg)
# Generate new, notify only, ticket
rstr = managers.cryptoManager().randomString(length=8)
notifyTicket = models.TicketStore.create_for_tunnel(
userService=userService,
port=port,
host=host,
extra={'t': self._args[0], 'b': models.getSqlDatetimeAsUnix()},
validity=MAX_SESSION_LENGTH)
data = {
'host': host,
'port': port,
'notify': notifyTicket
}
extra={
't': self._args[0], # ticket
'b': models.getSqlDatetimeAsUnix(), # Begin time stamp
},
validity=MAX_SESSION_LENGTH,
)
data = {'host': host, 'port': port, 'notify': notifyTicket}
return data
except Exception as e:
@@ -136,7 +159,9 @@ class TunnelRegister(Handler):
now = models.getSqlDatetimeAsUnix()
try:
# If already exists a token for this MAC, return it instead of creating a new one, and update the information...
tunnelToken = models.TunnelToken.objects.get(ip=self._params['ip'], hostname= self._params['hostname'])
tunnelToken = models.TunnelToken.objects.get(
ip=self._params['ip'], hostname=self._params['hostname']
)
# Update parameters
tunnelToken.username = self._user.pretty_name
tunnelToken.ip_from = self._request.ip
@@ -150,15 +175,8 @@ class TunnelRegister(Handler):
ip=self._params['ip'],
hostname=self._params['hostname'],
token=secrets.token_urlsafe(36),
stamp=models.getSqlDatetime()
stamp=models.getSqlDatetime(),
)
except Exception as e:
return {
'result': '',
'stamp': now,
'error': str(e)
}
return {
'result': tunnelToken.token,
'stamp': now
}
return {'result': '', 'stamp': now, 'error': str(e)}
return {'result': tunnelToken.token, 'stamp': now}

View File

@@ -42,13 +42,11 @@ from uds.core.util import permissions
logger = logging.getLogger(__name__)
# Enclosed methods under /osm path
class TunnelTokens(ModelHandler):
model = TunnelToken
table_title = _('Actor tokens')
table_title = _('Tunnel tokens')
table_fields = [
{'token': {'title': _('Token')}},
{'stamp': {'title': _('Date'), 'type': 'datetime'}},
@@ -65,7 +63,7 @@ class TunnelTokens(ModelHandler):
'username': item.username,
'ip': item.ip,
'hostname': item.hostname,
'token': item.token
'token': item.token,
}
def delete(self) -> str:
@@ -75,7 +73,9 @@ class TunnelTokens(ModelHandler):
if len(self._args) != 1:
raise RequestError('Delete need one and only one argument')
self.ensureAccess(self.model(), permissions.PERMISSION_ALL, root=True) # Must have write permissions to delete
self.ensureAccess(
self.model(), permissions.PERMISSION_ALL, root=True
) # Must have write permissions to delete
try:
self.model.objects.get(token=self._args[0]).delete()

View File

@@ -51,6 +51,7 @@ class AssignedService(DetailHandler):
"""
Rest handler for Assigned Services, wich parent is Service
"""
custom_methods = [
'reset',
]
@@ -239,7 +240,10 @@ class CachedService(AssignedService):
"""
Rest handler for Cached Services, wich parent is Service
"""
custom_methods: typing.ClassVar[typing.List[str]] = [] # Remove custom methods from assigned services
custom_methods: typing.ClassVar[
typing.List[str]
] = [] # Remove custom methods from assigned services
def getItems(self, parent: models.ServicePool, item: typing.Optional[str]):
# Extract provider

View File

@@ -87,20 +87,54 @@ class Users(DetailHandler):
del v['uuid']
yield v
def getItems(self, parent, item):
def getItems(self, parent: Authenticator, item: typing.Optional[str]):
logger.debug(item)
# Extract authenticator
try:
if item is None:
values = list(Users.uuid_to_id(parent.users.all().values('uuid', 'name', 'real_name', 'comments', 'state', 'staff_member', 'is_admin', 'last_access', 'parent')))
values = list(
Users.uuid_to_id(
parent.users.all().values(
'uuid',
'name',
'real_name',
'comments',
'state',
'staff_member',
'is_admin',
'last_access',
'parent',
)
)
)
for res in values:
res['role'] = res['staff_member'] and (res['is_admin'] and _('Admin') or _('Staff member')) or _('User')
res['role'] = (
res['staff_member']
and (res['is_admin'] and _('Admin') or _('Staff member'))
or _('User')
)
return values
else:
u = parent.users.get(uuid=processUuid(item))
res = model_to_dict(u, fields=('name', 'real_name', 'comments', 'state', 'staff_member', 'is_admin', 'last_access', 'parent'))
res = model_to_dict(
u,
fields=(
'name',
'real_name',
'comments',
'state',
'staff_member',
'is_admin',
'last_access',
'parent',
),
)
res['id'] = u.uuid
res['role'] = res['staff_member'] and (res['is_admin'] and _('Admin') or _('Staff member')) or _('User')
res['role'] = (
res['staff_member']
and (res['is_admin'] and _('Admin') or _('Staff member'))
or _('User')
)
usr = aUser(u)
res['groups'] = [g.dbGroup().uuid for g in usr.groups()]
logger.debug('Item: %s', res)
@@ -111,24 +145,41 @@ class Users(DetailHandler):
def getTitle(self, parent):
try:
return _('Users of {0}').format(Authenticator.objects.get(uuid=processUuid(self._kwargs['parent_id'])).name)
return _('Users of {0}').format(
Authenticator.objects.get(
uuid=processUuid(self._kwargs['parent_id'])
).name
)
except Exception:
return _('Current users')
def getFields(self, parent):
return [
{'name': {'title': _('Username'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-user text-success'}},
{
'name': {
'title': _('Username'),
'visible': True,
'type': 'icon',
'icon': 'fa fa-user text-success',
}
},
{'role': {'title': _('Role')}},
{'real_name': {'title': _('Name')}},
{'comments': {'title': _('Comments')}},
{'state': {'title': _('state'), 'type': 'dict', 'dict': State.dictionary()}},
{
'state': {
'title': _('state'),
'type': 'dict',
'dict': State.dictionary(),
}
},
{'last_access': {'title': _('Last access'), 'type': 'datetime'}},
]
def getRowStyle(self, parent):
return {'field': 'state', 'prefix': 'row-state-'}
def getLogs(self, parent, item):
def getLogs(self, parent: Authenticator, item):
user = None
try:
user = parent.users.get(uuid=processUuid(item))
@@ -137,9 +188,19 @@ class Users(DetailHandler):
return log.getLogs(user)
def saveItem(self, parent, item):
def saveItem(self, parent: Authenticator, item: typing.Optional[str]) -> None:
logger.debug('Saving user %s / %s', parent, item)
valid_fields = ['name', 'real_name', 'comments', 'state', 'staff_member', 'is_admin']
valid_fields = [
'name',
'real_name',
'comments',
'state',
'staff_member',
'is_admin',
]
if self._params.get('name', '') == '':
raise RequestError(_('Username cannot be empty'))
if 'password' in self._params:
valid_fields.append('password')
self._params['password'] = cryptoManager().hash(self._params['password'])
@@ -153,7 +214,9 @@ class Users(DetailHandler):
try:
auth = parent.getInstance()
if item is None: # Create new
auth.createUser(fields) # this throws an exception if there is an error (for example, this auth can't create users)
auth.createUser(
fields
) # this throws an exception if there is an error (for example, this auth can't create users)
user = parent.users.create(**fields)
else:
auth.modifyUser(fields) # Notifies authenticator
@@ -161,7 +224,9 @@ class Users(DetailHandler):
user.__dict__.update(fields)
logger.debug('User parent: %s', user.parent)
if auth.isExternalSource is False and (user.parent is None or user.parent == ''):
if auth.isExternalSource is False and (
user.parent is None or user.parent == ''
):
groups = self.readFieldsFromParams(['groups'])['groups']
logger.debug('Groups: %s', groups)
logger.debug('Got Groups %s', parent.groups.filter(uuid__in=groups))
@@ -177,18 +242,20 @@ class Users(DetailHandler):
raise RequestError(str(e))
except ValidationError as e:
raise RequestError(str(e.message))
except RequestError:
raise
except Exception:
logger.exception('Saving user')
raise self.invalidRequestException()
return self.getItems(parent, user.uuid)
def deleteItem(self, parent, item):
def deleteItem(self, parent: Authenticator, item):
try:
user = parent.users.get(uuid=processUuid(item))
if not self._user.is_admin and (user.is_admin or user.staff_member):
logger.warn('Removal of user {} denied due to insufficients rights')
raise self.invalidItemException('Removal of user {} denied due to insufficients rights')
raise self.invalidItemException(
'Removal of user {} denied due to insufficients rights'
)
assignedUserService: 'UserService'
for assignedUserService in user.userServices.all():
@@ -210,23 +277,29 @@ class Users(DetailHandler):
return 'deleted'
def servicesPools(self, parent, item):
def servicesPools(self, parent: Authenticator, item):
uuid = processUuid(item)
user = parent.users.get(uuid=processUuid(uuid))
res = []
groups = list(user.getGroups())
for i in getPoolsForGroups(groups):
res.append({
'id': i.uuid,
'name': i.name,
'thumb': i.image.thumb64 if i.image is not None else DEFAULT_THUMB_BASE64,
'user_services_count': i.userServices.exclude(state__in=(State.REMOVED, State.ERROR)).count(),
'state': _('With errors') if i.isRestrained() else _('Ok'),
})
res.append(
{
'id': i.uuid,
'name': i.name,
'thumb': i.image.thumb64
if i.image is not None
else DEFAULT_THUMB_BASE64,
'user_services_count': i.userServices.exclude(
state__in=(State.REMOVED, State.ERROR)
).count(),
'state': _('With errors') if i.isRestrained() else _('Ok'),
}
)
return res
def userServices(self, parent, item):
def userServices(self, parent: Authenticator, item):
uuid = processUuid(item)
user = parent.users.get(uuid=processUuid(uuid))
res = []
@@ -244,7 +317,7 @@ class Groups(DetailHandler):
custom_methods = ['servicesPools', 'users']
def getItems(self, parent, item):
def getItems(self, parent: Authenticator, item):
try:
multi = False
if item is None:
@@ -261,58 +334,87 @@ class Groups(DetailHandler):
'comments': i.comments,
'state': i.state,
'type': i.is_meta and 'meta' or 'group',
'meta_if_any': i.meta_if_any
'meta_if_any': i.meta_if_any,
}
if i.is_meta:
val['groups'] = list(x.uuid for x in i.groups.all().order_by('name'))
val['groups'] = list(
x.uuid for x in i.groups.all().order_by('name')
)
res.append(val)
if multi or not i:
return res
# Add pools field if 1 item only
res = res[0]
result = res[0]
if i.is_meta:
res['pools'] = [] # Meta groups do not have "assigned "pools, they get it from groups interaction
result[
'pools'
] = (
[]
) # Meta groups do not have "assigned "pools, they get it from groups interaction
else:
res['pools'] = [v.uuid for v in i.deployedServices.all()]
return res
result['pools'] = [v.uuid for v in i.deployedServices.all()]
return result
except Exception:
logger.exception('REST groups')
raise self.invalidItemException()
def getTitle(self, parent):
try:
return _('Groups of {0}').format(Authenticator.objects.get(uuid=processUuid(self._kwargs['parent_id'])).name)
return _('Groups of {0}').format(
Authenticator.objects.get(
uuid=processUuid(self._kwargs['parent_id'])
).name
)
except Exception:
return _('Current groups')
def getFields(self, parent):
return [
{'name': {'title': _('Group'), 'visible': True, 'type': 'icon_dict', 'icon_dict': {'group': 'fa fa-group text-success', 'meta': 'fa fa-gears text-info'}}},
{
'name': {
'title': _('Group'),
'visible': True,
'type': 'icon_dict',
'icon_dict': {
'group': 'fa fa-group text-success',
'meta': 'fa fa-gears text-info',
},
}
},
{'comments': {'title': _('Comments')}},
{'state': {'title': _('state'), 'type': 'dict', 'dict': State.dictionary()}},
{
'state': {
'title': _('state'),
'type': 'dict',
'dict': State.dictionary(),
}
},
]
def getTypes(self, parent, forType):
def getTypes(self, parent: Authenticator, forType):
tDct = {
'group': {'name': _('Group'), 'description': _('UDS Group')},
'meta': {'name': _('Meta group'), 'description': _('UDS Meta Group')},
}
types = [{
'name': tDct[t]['name'],
'type': t,
'description': tDct[t]['description'],
'icon': ''
} for t in tDct]
types = [
{
'name': tDct[t]['name'],
'type': t,
'description': tDct[t]['description'],
'icon': '',
}
for t in tDct
]
if forType is None:
return types
try:
return types[forType]
return next(filter(lambda x: x['type'] == forType, types))
except Exception:
raise self.invalidRequestException()
def saveItem(self, parent, item):
def saveItem(self, parent: Authenticator, item) -> None:
group = None # Avoid warning on reference before assignment
try:
is_meta = self._params['type'] == 'meta'
@@ -322,12 +424,16 @@ class Groups(DetailHandler):
logger.debug('Meta any %s', meta_if_any)
logger.debug('Pools: %s', pools)
valid_fields = ['name', 'comments', 'state']
if self._params.get('name', '') == '':
raise RequestError(_('Group name is required'))
fields = self.readFieldsFromParams(valid_fields)
is_pattern = fields.get('name', '').find('pat:') == 0
auth = parent.getInstance()
if item is None: # Create new
if not is_meta and not is_pattern:
auth.createGroup(fields) # this throws an exception if there is an error (for example, this auth can't create groups)
auth.createGroup(
fields
) # this throws an exception if there is an error (for example, this auth can't create groups)
toSave = {}
for k in valid_fields:
toSave[k] = fields[k]
@@ -362,13 +468,13 @@ class Groups(DetailHandler):
raise RequestError(_('User already exists (duplicate key error)'))
except AuthenticatorException as e:
raise RequestError(str(e))
except RequestError:
raise
except Exception:
logger.exception('Saving group')
raise self.invalidRequestException()
return self.getItems(parent, group.uuid)
def deleteItem(self, parent, item):
def deleteItem(self, parent: Authenticator, item: str) -> None:
try:
group = parent.groups.get(uuid=item)
@@ -376,24 +482,28 @@ class Groups(DetailHandler):
except Exception:
raise self.invalidItemException()
return 'deleted'
def servicesPools(self, parent, item):
def servicesPools(self, parent: Authenticator, item: str) -> typing.List[typing.Mapping[str, typing.Any]]:
uuid = processUuid(item)
group = parent.groups.get(uuid=processUuid(uuid))
res = []
res: typing.List[typing.Mapping[str, typing.Any]] = []
for i in getPoolsForGroups((group,)):
res.append({
'id': i.uuid,
'name': i.name,
'thumb': i.image.thumb64 if i.image is not None else DEFAULT_THUMB_BASE64,
'user_services_count': i.userServices.exclude(state__in=(State.REMOVED, State.ERROR)).count(),
'state': _('With errors') if i.isRestrained() else _('Ok'),
})
res.append(
{
'id': i.uuid,
'name': i.name,
'thumb': i.image.thumb64
if i.image is not None
else DEFAULT_THUMB_BASE64,
'user_services_count': i.userServices.exclude(
state__in=(State.REMOVED, State.ERROR)
).count(),
'state': _('With errors') if i.isRestrained() else _('Ok'),
}
)
return res
def users(self, parent, item):
def users(self, parent: Authenticator, item: str) -> typing.List[typing.Mapping[str, typing.Any]]:
uuid = processUuid(item)
group = parent.groups.get(uuid=processUuid(uuid))
@@ -403,10 +513,10 @@ class Groups(DetailHandler):
'name': user.name,
'real_name': user.real_name,
'state': user.state,
'last_access': user.last_access
'last_access': user.last_access,
}
res = []
res: typing.List[typing.Mapping[str, typing.Any]] = []
if group.is_meta:
# Get all users for everygroup and
groups = getGroupsFromMeta((group,))

View File

@@ -37,12 +37,10 @@ from ..handlers import Handler
logger = logging.getLogger(__name__)
class UDSVersion(Handler):
authenticated = False # Version requests are public
name = 'version'
def get(self) -> typing.MutableMapping[str, typing.Any]:
return {
'version': VERSION,
'build': VERSION_STAMP
}
return {'version': VERSION, 'build': VERSION_STAMP}

View File

@@ -1066,7 +1066,11 @@ class ModelHandler(BaseModelHandler):
if tags:
logger.debug('Updating tags: %s', tags)
item.tags.set(
[Tag.objects.get_or_create(tag=val)[0] for val in tags if val != '']
[
Tag.objects.get_or_create(tag=val)[0]
for val in tags
if val != ''
]
)
elif isinstance(
tags, list

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -12,7 +12,7 @@
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
@@ -52,6 +52,7 @@ class ContentProcessor:
"""
Process contents (request/response) so Handlers can manage them
"""
mime_type: typing.ClassVar[str] = ''
extensions: typing.ClassVar[typing.Iterable[str]] = []
@@ -81,7 +82,9 @@ class ContentProcessor:
Converts an obj to a response of specific type (json, XML, ...)
This is done using "render" method of specific type
"""
return http.HttpResponse(content=self.render(obj), content_type=self.mime_type + "; charset=utf-8")
return http.HttpResponse(
content=self.render(obj), content_type=self.mime_type + "; charset=utf-8"
)
def render(self, obj: typing.Any):
"""
@@ -98,7 +101,7 @@ class ContentProcessor:
return obj
if isinstance(obj, dict):
return {k:ContentProcessor.procesForRender(v) for k, v in obj.items()}
return {k: ContentProcessor.procesForRender(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple, types.GeneratorType)):
return [ContentProcessor.procesForRender(v) for v in obj]
@@ -117,11 +120,15 @@ class MarshallerProcessor(ContentProcessor):
If we have a simple marshaller for processing contents
this class will allow us to set up a new one simply setting "marshaller"
"""
marshaller: typing.ClassVar[typing.Any] = None
def processParameters(self) -> typing.MutableMapping[str, typing.Any]:
try:
if self._request.META.get('CONTENT_LENGTH', '0') == '0' or not self._request.body:
if (
self._request.META.get('CONTENT_LENGTH', '0') == '0'
or not self._request.body
):
return self.processGetParameters()
# logger.debug('Body: >>{}<< {}'.format(self._request.body, len(self._request.body)))
res = self.marshaller.loads(self._request.body.decode('utf8'))
@@ -143,14 +150,16 @@ class JsonProcessor(MarshallerProcessor):
"""
Provides JSON content processor
"""
mime_type = 'application/json'
extensions = ['json']
marshaller = json # type: ignore
# ---------------
# XML Processor
# ---------------
#===============================================================================
# ===============================================================================
# class XMLProcessor(MarshallerProcessor):
# """
# Provides XML content processor
@@ -158,12 +167,14 @@ class JsonProcessor(MarshallerProcessor):
# mime_type = 'application/xml'
# extensions = ['xml']
# marshaller = xml_marshaller
#===============================================================================
# ===============================================================================
processors_list = (JsonProcessor,)
default_processor: typing.Type[ContentProcessor] = JsonProcessor
available_processors_mime_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {cls.mime_type: cls for cls in processors_list}
available_processors_mime_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {
cls.mime_type: cls for cls in processors_list
}
available_processors_ext_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {}
for cls in processors_list:
for ext in cls.extensions:

View File

@@ -85,7 +85,7 @@ default_app_config = 'uds.UDSAppConfig'
@receiver(connection_created)
def extend_sqlite(connection=None, **kwargs):
if connection.vendor == "sqlite":
if connection and connection.vendor == "sqlite":
logger.debug('Connection vendor is sqlite, extending methods')
cursor = connection.cursor()
cursor.execute('PRAGMA synchronous=OFF')
@@ -95,3 +95,4 @@ def extend_sqlite(connection=None, **kwargs):
connection.connection.create_function("MIN", 2, min)
connection.connection.create_function("MAX", 2, max)
connection.connection.create_function("CEIL", 1, math.ceil)

View File

@@ -31,6 +31,7 @@
import logging
from django.http import HttpResponse
from django.middleware import csrf
from django.shortcuts import render
from django.template import RequestContext, loader
from django.utils.translation import ugettext as _
@@ -41,10 +42,22 @@ from uds.core.util.decorators import denyBrowsers
logger = logging.getLogger(__name__)
CSRF_FIELD = 'csrfmiddlewaretoken'
@denyBrowsers(browsers=['ie<10'])
@webLoginRequired(admin=True)
def index(request):
return render(request, 'uds/admin/index.html')
# Gets csrf token
csrf_token = csrf.get_token(request)
if csrf_token is not None:
csrf_token = str(csrf_token)
return render(
request,
'uds/admin/index.html',
{'csrf_field': CSRF_FIELD, 'csrf_token': csrf_token},
)
@denyBrowsers(browsers=['ie<10'])

View File

@@ -38,11 +38,7 @@ from django.utils.translation import ugettext_noop as _
from uds.core import auths
from uds.core.util import net
from uds.core.ui import gui
from uds.core.util.request import getRequest
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from django.http import HttpRequest # pylint: disable=ungrouped-imports
from uds.core.util.request import getRequest, ExtendedHttpRequest
logger = logging.getLogger(__name__)
@@ -51,7 +47,7 @@ class IPAuth(auths.Authenticator):
acceptProxy = gui.CheckBoxField(
label=_('Accept proxy'),
defvalue=gui.FALSE,
order=3,
order=50,
tooltip=_(
'If checked, requests via proxy will get FORWARDED ip address'
' (take care with this bein checked, can take internal IP addresses from internet)'
@@ -59,6 +55,14 @@ class IPAuth(auths.Authenticator):
tab=gui.ADVANCED_TAB
)
visibleFromNets = gui.TextField(
order=50,
label=_('Visible only from this networks'),
defvalue='',
tooltip=_('This authenticator will be visible only from these networks. Leave empty to allow all networks'),
tab=gui.ADVANCED_TAB
)
typeName = _('IP Authenticator')
typeType = 'IPAuth'
typeDescription = _('IP Authenticator')
@@ -93,6 +97,18 @@ class IPAuth(auths.Authenticator):
return True
return False
def isVisibleFrom(self, request: 'ExtendedHttpRequest'):
"""
Used by the login interface to determine if the authenticator is visible on the login page.
"""
validNets = self.visibleFromNets.value.strip()
try:
if not validNets or net.ipInNetwork(request.ip, validNets):
return True
except Exception as e:
logger.error('Invalid network for IP auth: %s', e)
return False
def internalAuthenticate(self, username: str, credentials: str, groupsManager: 'auths.GroupsManager') -> bool:
# In fact, username does not matter, will get IP from request
username = self.getIp() # Override provided username and use source IP
@@ -108,7 +124,7 @@ class IPAuth(auths.Authenticator):
def check(self):
return _("All seems to be fine.")
def getJavascript(self, request: 'HttpRequest') -> typing.Optional[str]:
def getJavascript(self, request: 'ExtendedHttpRequest') -> typing.Optional[str]:
# We will authenticate ip here, from request.ip
# If valid, it will simply submit form with ip submited and a cached generated random password
ip = self.getIp()

View File

@@ -158,6 +158,14 @@ class InternalDBAuth(auths.Authenticator):
groupsManager.validate([g.name for g in user.groups.all()])
def getRealName(self, username: str) -> str:
# Return the real name of the user, if it is set
try:
user = self.dbAuthenticator().users.get(name=username, state=State.ACTIVE)
return user.real_name or username
except Exception:
return super().getRealName(username)
def createUser(self, usrData):
pass

View File

@@ -123,6 +123,7 @@ class RadiusAuth(auths.Authenticator):
self.secret.value.encode(),
authPort=self.port.num(),
nasIdentifier=self.nasIdentifier.value,
appClassPrefix=self.appClassPrefix.value,
)
def authenticate(

View File

@@ -501,7 +501,9 @@ class RegexLdap(auths.Authenticator):
usr = self.__getUser(username)
if usr is None:
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Invalid user')
authLogLogin(
getRequest(), self.dbAuthenticator(), username, 'Invalid user'
)
return False
try:
@@ -510,7 +512,9 @@ class RegexLdap(auths.Authenticator):
usr['dn'], credentials
) # Will raise an exception if it can't connect
except:
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Invalid password')
authLogLogin(
getRequest(), self.dbAuthenticator(), username, 'Invalid password'
)
return False
groupsManager.validate(self.__getGroups(usr))

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -12,7 +12,7 @@
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
@@ -38,7 +38,10 @@ from uds.core.ui import gui
from uds.core import auths
if typing.TYPE_CHECKING:
from django.http import HttpRequest, HttpResponse # pylint: disable=ungrouped-imports
from django.http import (
HttpRequest,
HttpResponse,
) # pylint: disable=ungrouped-imports
logger = logging.getLogger(__name__)
@@ -125,7 +128,9 @@ class SampleAuth(auths.Authenticator):
# unserialization, and at this point all will be default values
# so self.groups.value will be []
if values and len(self.groups.value) < 2:
raise auths.Authenticator.ValidationException(_('We need more than two groups!'))
raise auths.Authenticator.ValidationException(
_('We need more than two groups!')
)
def searchUsers(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]:
"""
@@ -137,7 +142,13 @@ class SampleAuth(auths.Authenticator):
facility for users. In our case, we will simply return a list of users
(array of dictionaries with ids and names) with the pattern plus 1..10
"""
return [{'id': '{0}-{1}'.format(pattern, a), 'name': '{0} number {1}'.format(pattern, a)} for a in range(1, 10)]
return [
{
'id': '{0}-{1}'.format(pattern, a),
'name': '{0} number {1}'.format(pattern, a),
}
for a in range(1, 10)
]
def searchGroups(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]:
"""
@@ -154,7 +165,9 @@ class SampleAuth(auths.Authenticator):
res.append({'id': g, 'name': ''})
return res
def authenticate(self, username: str, credentials: str, groupsManager: 'auths.GroupsManager') -> bool:
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
) -> bool:
"""
This method is invoked by UDS whenever it needs an user to be authenticated.
It is used from web interface, but also from administration interface to
@@ -196,7 +209,9 @@ class SampleAuth(auths.Authenticator):
:note: groupsManager is an in/out parameter
"""
if username != credentials: # All users with same username and password are allowed
if (
username != credentials
): # All users with same username and password are allowed
return False
# Now the tricky part. We will make this user belong to groups that contains at leat
@@ -247,11 +262,17 @@ class SampleAuth(auths.Authenticator):
# I know, this is a bit ugly, but this is just a sample :-)
res = '<p>Login name: <input id="logname" type="text"/></p>'
res += '<p><a href="" onclick="window.location.replace(\'' + self.callbackUrl() + '?user='
res += (
'<p><a href="" onclick="window.location.replace(\''
+ self.callbackUrl()
+ '?user='
)
res += '\' + $(\'#logname\').val()); return false;">Login</a></p>'
return res
def authCallback(self, parameters: typing.Dict[str, typing.Any], gm: 'auths.GroupsManager') -> typing.Optional[str]:
def authCallback(
self, parameters: typing.Dict[str, typing.Any], gm: 'auths.GroupsManager'
) -> typing.Optional[str]:
"""
We provide this as a sample of callback for an user.
We will accept all petitions that has "user" parameter
@@ -286,6 +307,7 @@ class SampleAuth(auths.Authenticator):
Here, we will set the state to "Inactive" and realName to the same as username, but twice :-)
"""
from uds.core.util.state import State
usrData['real_name'] = usrData['name'] + ' ' + usrData['name']
usrData['state'] = State.INACTIVE

Some files were not shown because too many files have changed in this diff Show More