Compare commits

...

121 Commits

Author SHA1 Message Date
Josiah Baldwin
cac746906f Bump version 2026-02-18 15:31:36 -08:00
Josiah Baldwin
6290bc7298 Fixed ignore_output for run_command 2026-02-18 15:28:50 -08:00
Josiah Baldwin
5975e145a7 Bumped some lib versions 2026-02-18 15:27:56 -08:00
Josiah Baldwin
cd6707a279 Added tests to test ignore output on run_command variants 2026-02-18 15:25:42 -08:00
Josiah Baldwin
7b9d82b8e6 Various changes to get the test environment working with latest versions of tools used 2026-02-18 15:24:56 -08:00
Josiah Baldwin
7cefd24a9d Added release description for 1.3.2 2025-10-22 20:10:38 -07:00
Josiah Baldwin
cbc1f9223f Merge pull request #62 from HuFlungDu/fix/runcommands-race-condition
fix #46
2025-10-22 20:05:56 -07:00
Josiah Baldwin
3fa1ca2e32 Added handling for weird response from meshcentral for run_command and run_console_command 2025-10-22 19:59:17 -07:00
Josiah Baldwin
ee812220fb Attempt to fix race condition in run_commands 2025-10-15 12:17:48 -07:00
Josiah Baldwin
002f652c8c Bump version 2025-09-27 18:13:48 -07:00
Josiah Baldwin
0b09f64821 Fix race condition for run_commands
Fix run_console_command being oble to pick up run_command outputs
2025-09-27 18:12:21 -07:00
Josiah Baldwin
12a3040f89 Reapply "Feat/run console commands"
This reverts commit 4cda54ab60.
2025-09-27 14:48:55 -07:00
Josiah Baldwin
e0694f980c Merge branch 'release/1.3.0' 2025-09-26 15:58:06 -07:00
Josiah Baldwin
61053549f2 Fixed test for remove device 2025-09-26 15:54:25 -07:00
Josiah Baldwin
fb3d043431 Added Daan to contributors 2025-09-26 15:39:57 -07:00
Josiah Baldwin
c13985739b Added release notes for 1.3.0 2025-09-26 15:37:55 -07:00
Josiah Baldwin
db1914c87b Merge pull request #54 from DaanSelen/feat-remove-dev
feat: remove devices function

resolves #52
2025-09-26 15:32:09 -07:00
Daan Selen
b0d071d87f feat: add remove_device function 2025-09-26 15:29:52 -07:00
Josiah Baldwin
3bcedf5610 Kinda added a test for remove device 2025-09-26 15:20:25 -07:00
Josiah Baldwin
9c7a8c39b0 Modified some implementation details 2025-09-26 15:19:57 -07:00
Daan Selen
7ba6989325 refac: I lied, this is the last... 2025-09-26 14:50:25 -07:00
Daan Selen
748e39d5b4 refac: remove nodeid parameter 2025-09-26 14:50:25 -07:00
Daan Selen
6dae40eb40 refac: copy other style 2025-09-26 14:50:25 -07:00
Daan Selen
c7d628716e refac: renamed and added device class impl 2025-09-26 14:50:25 -07:00
Daan Selen
1f9979ddd1 feat: add remove_device function 2025-09-26 14:50:25 -07:00
d4b9524814 feat(lib): draft function for remove_device 2025-09-26 14:50:25 -07:00
Josiah Baldwin
bc1db8f2b3 Update documentation for files.rm
Resolves #53
2025-09-26 14:40:53 -07:00
Josiah Baldwin
403c0cd0ec Merge branch 'development' of github.com:HuFlungDu/pylibmeshctrl into development 2025-09-26 14:38:34 -07:00
Josiah Baldwin
0b0029563a Maybe fix race condition when using multiple nodes in run_command 2025-09-26 14:38:10 -07:00
Josiah Baldwin
0b32896c88 Merge pull request #60 from HuFlungDu/feat/run_console_commands
Add run_console_command function

resolve #55
2025-09-26 14:17:44 -07:00
Josiah Baldwin
2304810ee6 Merge pull request #59 from HuFlungDu/revert-58-feat/run_console_commands
Revert "Feat/run console commands"

Seriously, I don't want to default merge to main, github.
2025-09-26 14:16:47 -07:00
Josiah Baldwin
4cda54ab60 Revert "Feat/run console commands" 2025-09-26 14:16:18 -07:00
Josiah Baldwin
87fad5aa13 Merge pull request #58 from HuFlungDu/feat/run_console_commands
Add run_console_command function

resolve #55
2025-09-26 14:16:12 -07:00
Josiah Baldwin
6daaa91758 Added test for run_console_command 2025-09-26 14:12:56 -07:00
Josiah Baldwin
078e07cb4f Added mesh agent hex ID to agent server return value 2025-09-26 14:12:22 -07:00
Josiah Baldwin
0e569ae0cb Added support for run_console_commands 2025-09-26 14:11:39 -07:00
Josiah Baldwin
62fdc79aeb Merge pull request #57 from HuFlungDu/fix/run_commands_response
Add handling for runcommands `reply: true` option and make it the default.

Resolves #51
2025-09-26 13:08:31 -07:00
Josiah Baldwin
c450ad7a96 Added test for missing device 2025-09-26 12:57:56 -07:00
Josiah Baldwin
891f7bfc12 Fixed old style run_command failing 2025-09-26 12:57:30 -07:00
Josiah Baldwin
4953d85cdc added reply to run commands 2025-09-24 10:36:42 -07:00
Josiah Baldwin
f5c6e96597 Bumped version 2025-06-25 10:51:18 -07:00
Josiah Baldwin
428a1b31c7 Merge pull request #49 from DaanSelen/user_agent
Added user agent to ws connection.
2025-06-24 14:44:06 -07:00
DaanSelen
16f3f99427 Merge branch 'development' into user_agent 2025-06-24 22:51:10 +02:00
Josiah Baldwin
d21450e463 Merge pull request #48 from DaanSelen/increase_limit
Increase limit
2025-06-24 13:48:43 -07:00
Daan Selen
9e08a1af49 Minor corrections 2025-06-19 22:20:30 +02:00
Daan Selen
e9de43420e draft 2025-06-19 22:13:35 +02:00
Daan Selen
fcdf8add53 Just max_size 2025-06-19 21:59:54 +02:00
Josiah Baldwin
163b776dfc Fixed library __version__ var 2025-06-19 12:38:00 -07:00
Josiah Baldwin
04c8f622de Bumped version 2025-06-14 12:53:26 -07:00
Josiah Baldwin
ccb5f1eb40 Removed catch with print statement 2025-06-14 12:50:22 -07:00
Josiah Baldwin
ce2cf2bfe1 Merge branch 'fix/device-details' into development 2025-06-14 12:47:45 -07:00
Josiah Baldwin
a3b4962e7f Update timeout for WS download, becaule it takes a little longer than http 2025-06-14 12:45:36 -07:00
Josiah Baldwin
5947e48c5b modified node parsing to accept ony number of nested strings 2025-06-14 12:42:59 -07:00
Daan Selen
31a8f00cd0 syntax fix 2025-06-12 16:58:19 +02:00
Daan Selen
871d36b334 Added support for new MeshCentral response type.
2b4ab2b122
2025-06-12 16:35:08 +02:00
Josiah Baldwin
59fb1f104e Bumped version 2025-04-01 12:17:38 -07:00
Josiah Baldwin
9bd3e10ed7 Merge pull request #45 from HuFlungDu/fix/device-open-url
Fixed weird issues in device_open_url
Fix #40
2025-04-01 12:08:43 -07:00
Josiah Baldwin
28e1d94ab9 Fixed weird issues in device_open_url 2025-04-01 12:06:41 -07:00
Josiah Baldwin
51325a89d3 Merge pull request #44 from DaanSelen/simonfix2
Fix #41
2025-04-01 11:53:18 -07:00
Josiah Baldwin
97dff80222 Merge pull request #42 from DaanSelen/lastconlastaddr
Display lastaddr and lastconnected in device object.
2025-04-01 11:51:43 -07:00
Daan
8da445348b Fix a bug discovered by @si458 and also suggested to be fixed 2025-03-21 00:06:24 +01:00
Daan
ab1fba5cc1 Display lastaddr and lastconnected in device object. 2025-03-20 23:55:13 +01:00
Josiah Baldwin
34a80cdda7 Merge pull request #39 from HuFlungDu/feat/websockets15
Feat/websockets15

Implement #38
2025-02-17 12:56:29 -08:00
Josiah Baldwin
fcf523dd62 Updated to work with websockets 15 proxy handling 2025-02-17 12:54:50 -08:00
Josiah Baldwin
9a1311167d Added traceback printing for autoreconnect test in case the error type changes 2025-02-17 12:54:01 -08:00
Josiah Baldwin
c2319fcf29 Updated requirements to websockets 15. This breaks things. 2025-02-17 12:22:31 -08:00
Josiah Baldwin
4d1c25a35c Merge pull request #37 from HuFlungDu/hotfix/1.1.2
Hotfix/1.1.2

Fixes #35 

Update Cryptography version to fix SSL vulnerability.
2025-02-17 12:12:13 -08:00
Josiah Baldwin
e226fff8dd Merge pull request #36 from HuFlungDu/hotfix/1.1.2
Hotfix/1.1.2

Fixes #35 

Update Cryptography version to fix SSL vulnerability.
2025-02-17 12:11:08 -08:00
Josiah Baldwin
a07b0f129a Updated changelog 2025-02-17 12:07:19 -08:00
Josiah Baldwin
64dc5eccdf Updated cryptography requirement 2025-02-17 12:06:10 -08:00
Josiah Baldwin
1a7714663a Updated changelog 2025-02-17 11:51:13 -08:00
Josiah Baldwin
0a59edd19a Fixed semvar for requirements 2025-02-17 11:50:50 -08:00
Josiah Baldwin
f8600b09fe Merge pull request #33 from HuFlungDu/hotfix/1.1.1
Hotfix/1.1.1

Fix #29
2025-02-05 12:37:35 -08:00
Josiah Baldwin
351f425ce5 Merge pull request #32 from HuFlungDu/hotfix/1.1.1
Hotfix/1.1.1

Fix #29
2025-02-05 12:37:00 -08:00
Josiah Baldwin
77e76aeb7c Updated changelog 2025-02-05 12:35:23 -08:00
Josiah Baldwin
5393321f7b Fixed issue with getting device_info when multiple meshes are available 2025-02-05 12:30:13 -08:00
Josiah Baldwin
79554ebad6 Added differintiation between admin device info and unprivileged device info for tests, as unprivileged doesn't have any mesh info at the time of testing 2025-02-05 12:28:06 -08:00
Josiah Baldwin
1dbcd012ec Added test to check for issue with mulitple meshes 2025-02-05 12:16:01 -08:00
Josiah Baldwin
ace6884991 Merge pull request #27 from HuFlungDu/release/1.1.0
Release/1.1.0 > main
2025-01-08 14:26:38 -08:00
Josiah Baldwin
61eebf1532 Merge pull request #26 from HuFlungDu/release/1.1.0
1.1.0 Release > dev
2025-01-08 14:25:18 -08:00
Josiah Baldwin
fcfeac21a8 Updated changelog 2025-01-08 14:24:19 -08:00
Josiah Baldwin
19d10ee050 Merge pull request #25 from HuFlungDu/fix/multiple-run-commands
fix/multiple-run-commands
2025-01-08 14:10:21 -08:00
Josiah Baldwin
0c9ebf0ff2 Merge pull request #23 from HuFlungDu/feat/test-meshcetral-overrides
Feat/test-meshcentral-overrides
2025-01-08 14:10:13 -08:00
Josiah Baldwin
2556e72a73 Merge pull request #22 from HuFlungDu/fix/bad-auth
Fix/bad-auth
2025-01-08 14:10:03 -08:00
Josiah Baldwin
cda5f610a1 Merge pull request #21 from HuFlungDu/fix/raw-event-off
Fixed listening to raw not removing its listener correctly
2025-01-08 14:09:48 -08:00
Josiah Baldwin
564d466ff9 Fixed listening to raw not removing its listener correctly 2025-01-08 13:57:28 -08:00
Josiah Baldwin
125e6ac6ac Added override directory which will be copied over meshcentral code for testing purposes 2025-01-08 13:54:21 -08:00
Josiah Baldwin
1b849473bb Removed silencing of docker process. Useful for debugging server side things with overrides. 2025-01-08 13:48:41 -08:00
Josiah Baldwin
df25652ba6 Fixed run_commands parsing return from multiple devices incorrectly 2025-01-08 13:42:39 -08:00
Josiah Baldwin
9668e4d507 Added test for using run_command on multiple nodes 2025-01-08 13:38:49 -08:00
Josiah Baldwin
fe4c2fe874 Fixed connection errors not raising immediately 2025-01-08 13:26:04 -08:00
Josiah Baldwin
bb7cf17cd3 Added test for invalid auth 2025-01-08 13:23:21 -08:00
Josiah Baldwin
6919da4a42 Merge pull request #19 from DaanSelen/add-users
Fix #15
2025-01-08 11:59:04 -08:00
Josiah Baldwin
ff120490fa Merge branch 'main' into add-users 2025-01-08 11:56:47 -08:00
Josiah Baldwin
d9991156f6 Merge pull request #18 from DaanSelen/conversion
Fix #9
2025-01-08 11:46:15 -08:00
Daan
4fea858fbc Fix: https://github.com/HuFlungDu/pylibmeshctrl/issues/15 2025-01-04 17:16:30 +01:00
Daan
3b4a18b379 Also added the ValueError raise condition for lastconnect datetime. 2025-01-04 17:09:33 +01:00
Daan
c072d6012a fix https://github.com/HuFlungDu/pylibmeshctrl/issues/9 2025-01-04 17:05:57 +01:00
Josiah Baldwin
0ee2e2dc94 Merge pull request #12 from DaanSelen/main
Backport for python 3.11 and alike
2024-12-19 12:49:12 -08:00
dselen
f2d9fcd295 Update tunnel.py
Fix syntaxerror
2024-12-19 10:14:09 +01:00
Josiah Baldwin
7456743709 Updated installing docs 2024-12-13 17:10:34 -08:00
Josiah Baldwin
07b828a150 Fixed some docs 2024-12-13 17:04:11 -08:00
Josiah Baldwin
cd7a356eb5 Merge pull request #7 from HuFlungDu/release/1.0.0
1.0.0 release
2024-12-13 16:43:12 -08:00
Josiah Baldwin
5ee2c8edf3 1.0.0 release 2024-12-13 16:39:59 -08:00
Josiah Baldwin
d3d5b87287 Fixed various issues with (down/up)load file functions not passing through arguments 2024-12-13 08:51:49 -08:00
Josiah Baldwin
18eb2de5b6 Added no_proxy os variable bypass so urllib.requests acts as expected 2024-12-13 08:28:54 -08:00
Josiah Baldwin
ec23ba458d Changed how tunnuls handle their ssl contexts; fixed long standing file tunnels not being cleaned up on session close; changed file sizes in tests 2024-12-12 18:06:50 -08:00
Josiah Baldwin
a3c721318d Added ability to download files over http(s)
Also fixed some tests and a couple other bugs
2024-12-12 16:06:18 -08:00
Josiah Baldwin
4eda4e6c08 increased the odds of getting useful node node information from device_info 2024-12-10 18:17:25 -08:00
Josiah Baldwin
ab2a4c40bc Fixed auto-reconnect for proxy and created tests for auto-reconnect 2024-12-10 13:05:22 -08:00
Josiah Baldwin
0a657cee48 Added default port numbers to URL. This fixes an issue with proxy handling mhen you don't pass a port in the url. 2024-12-10 10:33:05 -08:00
Josiah Baldwin
03441161b2 Added timeout to check_socket decorator in case a connection fails to be made 2024-12-10 10:31:52 -08:00
Josiah Baldwin
24adf3baa5 Updated docs for proxy 2024-12-09 16:45:12 -08:00
Josiah Baldwin
1adaccabc0 Added proxy and tests for proxy 2024-12-09 16:42:32 -08:00
Josiah Baldwin
20843dbea7 Changed download file APIs so the stream returns at the position where it was passed in 2024-12-04 13:40:49 -08:00
Josiah Baldwin
af6c020506 Kinda fixed auto reconnect 2024-12-04 13:29:17 -08:00
Josiah Baldwin
b870aa25bd Added ping and raw raw_events; Removed some debug stuff 2024-12-03 17:46:57 -08:00
Josiah Baldwin
c63604f624 Removed test users file 2024-12-02 18:22:04 -08:00
Josiah Baldwin
f0e09c0082 Doc fix 2024-12-02 13:41:16 -08:00
Josiah Baldwin
184ce3ef3e Added Session to the __init__ file, and changed docs and test accordingly 2024-12-02 13:39:40 -08:00
Josiah Baldwin
33680dab5d Updated __init__ imports 2024-12-02 13:02:15 -08:00
33 changed files with 10369 additions and 323 deletions

View File

@@ -19,7 +19,7 @@ formats:
build: build:
os: ubuntu-22.04 os: ubuntu-22.04
tools: tools:
python: "3.11" python: "3.13"
python: python:
install: install:

View File

@@ -3,3 +3,4 @@ Contributors
============ ============
* Josiah Baldwin <jbaldwin8889@gmail.com> * Josiah Baldwin <jbaldwin8889@gmail.com>
* Daan Selen <https://github.com/DaanSelen>

View File

@@ -2,7 +2,93 @@
Changelog Changelog
========= =========
Version 0.1 version 1.3.3
=========== =============
Create Improvements:
* Dependency bumps
Bugs:
* Fix run_commands having an issue with ignore_output
version 1.3.2
=============
Improvements:
* Fix race condition that could occur when running `run_command` or `run_console_command`
version 1.3.1
=============
Improvments:
* Basically just everything in 1.3.0, this is a release fix
version 1.3.0
=============
Improvements:
* Improved how run_commands was handled (#51)
* Added remove device functionality (#52)
* Added run_console_commands functionality (#55)
Bugs:
* Silly documentation being wrong (#53)
version 1.2.2
=============
Improvements:
* Added user agent to websocket headers
Bugs:
* Fixed library's __version__ implementation
* Fixed data from certain devices not showing up due to overloading websocket packet sizes
version 1.2.1
=============
Bugs:
* Fixed handling of meshcentral's list_devices return with details=True
version 1.2.0
=============
Bugs:
* Fixed agent sometimes being None causing an oxception
* Fixed bad code in device_open_url
Features:
* Changed websockets version to 15. This now uses the proxy implemention from that library, instead of the previous hack.
* Added lastaddr and lastconnect to list_devices API
version 1.1.2
=============
Bugs:
* Fixed semver for requirements. New version of websockets broke this library.
Security:
* Updated cryptogaphy to ~44.0.1 to fix ssl vulnerability.
Version 1.1.1
=============
Bugs:
* Fixed bug when running device_info when user has access to multiple meshes
Version 1.1.0
=============
Features:
* Added overrides for meshcentral files for testing purposes
* Added `users` field to `device` object
Bugs:
* Fixed connection errors not raising immediately
* Fixed run_commands parsing return from multiple devices incorrectly
* Fixed listening to raw not removing its listener correctly
* Fixed javascript timecodes not being handled in gnu environments
* Changed some fstring formatting that locked the library into python >3.13
Version 1.0.0
=============
First release

View File

@@ -38,14 +38,13 @@ Library for remotely interacting with a
Installation Installation
------------ ------------
pip install meshctrl pip install libmeshctrl
Usage Usage
----- -----
This module is implemented as a primarily asynchronous library This module is implemented as a primarily asynchronous library
(asyncio), mostly through the ``Session`` class, which is exported as (asyncio), mostly through the `Session <https://pylibmeshctrl.readthedocs.io/en/latest/api/meshctrl.html#meshctrl.session.Session>`__ class. Because the library is asynchronous, you must wait for it to be
default. Because the library is asynchronous, you must wait for it to be
initialized before interacting with the server. The preferred way to do initialized before interacting with the server. The preferred way to do
this is to use the async context manager pattern: this is to use the async context manager pattern:
@@ -53,20 +52,20 @@ this is to use the async context manager pattern:
import meshctrl import meshctrl
async with meshctrl.session.Session(url, **options): async with meshctrl.Session(url, **options):
print(await session.list_users()) print(await session.list_users())
... ...
However, if you prefer to instantiate the object yourself, you can However, if you prefer to instantiate the object yourself, you can
simply use the ``initialized`` property: simply use the `initialized <https://pylibmeshctrl.readthedocs.io/en/latest/api/meshctrl.html#meshctrl.session.Session.initialized>`__ property:
.. code:: python .. code:: python
session = meshctrl.session.Session(url, **options) session = meshctrl.Session(url, **options)
await session.initialized.wait() await session.initialized.wait()
Note that, in this case, you will be rquired to clean up tho session Note that, in this case, you will be rquired to clean up tho session
using its ``close`` method. using its `close <https://pylibmeshctrl.readthedocs.io/en/latest/api/meshctrl.html#meshctrl.session.Session.close>`__ method.
Session Parameters Session Parameters
------------------ ------------------

View File

@@ -5,8 +5,8 @@ sphinx>=3.2.1
sphinx-jinja2-compat>=0.1.1 sphinx-jinja2-compat>=0.1.1
sphinx-toolbox>=2.16.0 sphinx-toolbox>=2.16.0
# sphinx_rtd_theme # sphinx_rtd_theme
cffi==1.17.1 cffi~=1.17.1
cryptography==43.0.3 pycparser~=2.22
pycparser==2.22 enum_tools
websockets==13.1 cryptography~=46.0.5
enum_tools websockets~=16.0.0

Binary file not shown.

View File

@@ -24,7 +24,7 @@ platforms = any
# Add here all kinds of additional classifiers as defined under # Add here all kinds of additional classifiers as defined under
# https://pypi.org/classifiers/ # https://pypi.org/classifiers/
classifiers = classifiers =
Development Status :: 4 - Beta Development Status :: 5 - Production/Stable
Programming Language :: Python Programming Language :: Python
@@ -44,8 +44,9 @@ python_requires = >=3.8
# For more information, check out https://semver.org/. # For more information, check out https://semver.org/.
install_requires = install_requires =
importlib-metadata importlib-metadata
cryptography>=43.0.3 cryptography~=46.0.5
websockets>=13.1 websockets~=16.0.0
python-socks[asyncio]~=2.8.1
[options.packages.find] [options.packages.find]

View File

@@ -8,17 +8,20 @@ else:
try: try:
# Change here if project is renamed and does not equal the package name # Change here if project is renamed and does not equal the package name
dist_name = "meshctrl" dist_name = "libmeshctrl"
__version__ = version(dist_name) __version__ = version(dist_name)
except PackageNotFoundError: # pragma: no cover except PackageNotFoundError: # pragma: no cover
__version__ = "unknown" __version__ = "unknown"
finally: finally:
del version, PackageNotFoundError del version, PackageNotFoundError
from . import session from .session import Session
from . import constants from . import constants
from . import shell from . import shell
from . import tunnel from . import tunnel
from . import util from . import util
from . import files from . import files
from . import exceptions from . import exceptions
from . import device
from . import mesh
from . import user_group

View File

@@ -12,6 +12,7 @@ class Device(object):
name (str|None): Device name as it is shown on the meshcentral server name (str|None): Device name as it is shown on the meshcentral server
description (str|None): Device description as it is shown on the meshcentral server. Also accepted as desc. description (str|None): Device description as it is shown on the meshcentral server. Also accepted as desc.
tags (list[str]|None): tags associated with device. tags (list[str]|None): tags associated with device.
users (list[str]|None): latest known usernames which have logged in.
created_at (datetime.Datetime|int|None): Time at which device mas created. Also accepted as agct. created_at (datetime.Datetime|int|None): Time at which device mas created. Also accepted as agct.
computer_name (str|None): Device name as reported from the agent. This may be different from name. Also accepted as rname. computer_name (str|None): Device name as reported from the agent. This may be different from name. Also accepted as rname.
icon (~meshctrl.constants.Icon): Icon displayed on the website icon (~meshctrl.constants.Icon): Icon displayed on the website
@@ -38,6 +39,7 @@ class Device(object):
name (str|None): Device name as it is shown on the meshcentral server name (str|None): Device name as it is shown on the meshcentral server
description (str|None): Device description as it is shown on the meshcentral server. description (str|None): Device description as it is shown on the meshcentral server.
tags (list[str]): tags associated with device. tags (list[str]): tags associated with device.
users (list[str]): latest known usernames which have logged in.
computer_name (str|None): Device name as reported from the agent. This may be different from name. Also accepted as rname. computer_name (str|None): Device name as reported from the agent. This may be different from name. Also accepted as rname.
icon (~meshctrl.constants.Icon): Icon displayed on the website icon (~meshctrl.constants.Icon): Icon displayed on the website
mesh (~meshctrl.mesh.Mesh|None): Mesh object under which this device exists. Is None for individual device access. mesh (~meshctrl.mesh.Mesh|None): Mesh object under which this device exists. Is None for individual device access.
@@ -54,11 +56,11 @@ class Device(object):
links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device
details (dict[str, dict]): Extra details about the device. These are not well defined, but are filled by calling :py:meth:`~meshctrl.session.Session.list_devices` with `details=True`. details (dict[str, dict]): Extra details about the device. These are not well defined, but are filled by calling :py:meth:`~meshctrl.session.Session.list_devices` with `details=True`.
''' '''
def __init__(self, nodeid, session, agent=None, def __init__(self, nodeid, session, agent=None,
name=None, desc=None, description=None, name=None, desc=None, description=None,
tags=None, tags=None, users=None,
agct=None, created_at=None, agct=None, created_at=None,
rname=None, computer_name=None, icon=constants.Icon.desktop, rname=None, computer_name=None, icon=constants.Icon.desktop,
mesh=None, mtype=None, meshtype=None, groupname=None, meshname=None, mesh=None, mtype=None, meshtype=None, groupname=None, meshname=None,
domain=None, host=None, ip=None, conn=None, connected=None, domain=None, host=None, ip=None, conn=None, connected=None,
pwr=None, powered_on=None, pwr=None, powered_on=None,
@@ -69,7 +71,7 @@ class Device(object):
if links is None: if links is None:
links = {} links = {}
self.links = links self.links = links
if ("ver" in agent): if agent and "ver" in agent:
agent = { agent = {
"version": agent["ver"], "version": agent["ver"],
"id": agent["id"], "id": agent["id"],
@@ -90,13 +92,14 @@ class Device(object):
self.description = description if description is not None else desc self.description = description if description is not None else desc
self.os_description = os_description if os_description is not None else osdesc self.os_description = os_description if os_description is not None else osdesc
self.tags = tags if tags is not None else [] self.tags = tags if tags is not None else []
self.users = users if users is not None else []
self.details = details if details is not None else {} self.details = details if details is not None else {}
created_at = created_at if created_at is not None else agct created_at = created_at if created_at is not None else agct
if not isinstance(created_at, datetime.datetime) and created_at is not None: if not isinstance(created_at, datetime.datetime) and created_at is not None:
try: try:
created_at = datetime.datetime.fromtimestamp(created_at) created_at = datetime.datetime.fromtimestamp(created_at)
except OSError: except (OSError, ValueError):
# Meshcentral returns in miliseconds, while fromtimestamp, and most of python, expects the argument in seconds. Try seconds frist, then translate from ms if it fails. # Meshcentral returns in miliseconds, while fromtimestamp, and most of python, expects the argument in seconds. Try seconds frist, then translate from ms if it fails.
# This doesn't work for really early timestamps, but I don't expect that to be a problem here. # This doesn't work for really early timestamps, but I don't expect that to be a problem here.
created_at = datetime.datetime.fromtimestamp(created_at/1000.0) created_at = datetime.datetime.fromtimestamp(created_at/1000.0)
@@ -106,7 +109,7 @@ class Device(object):
if not isinstance(lastconnect, datetime.datetime) and lastconnect is not None: if not isinstance(lastconnect, datetime.datetime) and lastconnect is not None:
try: try:
lastconnect = datetime.datetime.fromtimestamp(lastconnect) lastconnect = datetime.datetime.fromtimestamp(lastconnect)
except OSError: except (OSError, ValueError):
# Meshcentral returns in miliseconds, while fromtimestamp, and most of python, expects the argument in seconds. Try seconds frist, then translate from ms if it fails. # Meshcentral returns in miliseconds, while fromtimestamp, and most of python, expects the argument in seconds. Try seconds frist, then translate from ms if it fails.
# This doesn't work for really early timestamps, but I don't expect that to be a problem here. # This doesn't work for really early timestamps, but I don't expect that to be a problem here.
lastconnect = datetime.datetime.fromtimestamp(lastconnect/1000.0) lastconnect = datetime.datetime.fromtimestamp(lastconnect/1000.0)
@@ -129,7 +132,7 @@ class Device(object):
Returns: Returns:
bool: True on success, raise otherwise bool: True on success, raise otherwise
Raises: Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
@@ -147,7 +150,7 @@ class Device(object):
Returns: Returns:
bool: True on success, raise otherwise bool: True on success, raise otherwise
Raises: Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
@@ -166,7 +169,7 @@ class Device(object):
Returns: Returns:
bool: True on success, raise otherwise bool: True on success, raise otherwise
Raises: Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
@@ -183,7 +186,7 @@ class Device(object):
Returns: Returns:
~meshctrl.device.Device: Object representing the state of the device. This will be a new device, it will not update this device. ~meshctrl.device.Device: Object representing the state of the device. This will be a new device, it will not update this device.
Raises: Raises:
ValueError: `Invalid device id` if device is not found ValueError: `Invalid device id` if device is not found
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
@@ -205,7 +208,7 @@ class Device(object):
Returns: Returns:
bool: True if successful, raise otherwise bool: True if successful, raise otherwise
Raises: Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
@@ -227,7 +230,7 @@ class Device(object):
Returns: Returns:
~meshctrl.types.RunCommandResponse: Output of command ~meshctrl.types.RunCommandResponse: Output of command
Raises: Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
ValueError: `Invalid device id` if device is not found ValueError: `Invalid device id` if device is not found
@@ -268,7 +271,7 @@ class Device(object):
Returns: Returns:
bool: True if successful bool: True if successful
Raises: Raises:
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
''' '''
@@ -286,12 +289,29 @@ class Device(object):
Returns: Returns:
bool: True if successful bool: True if successful
Raises: Raises:
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
''' '''
return await self._session.reset_devices(self.nodeid, timeout=timeout) return await self._session.reset_devices(self.nodeid, timeout=timeout)
async def remove(self, timeout=None):
'''
Remove device from MeshCentral
Args:
nodeids (str|list[str]): nodeid(s) of the device(s) that have to be removed
timeout (int): duration in seconds to wait for a response before throwing an error
Returns:
bool: True on success, raise otherwise
Raises:
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out
'''
return self._session.remove_devices(self.nodeid, timeout)
async def sleep(self, timeout=None): async def sleep(self, timeout=None):
''' '''
Sleep device Sleep device
@@ -302,7 +322,7 @@ class Device(object):
Returns: Returns:
bool: True if successful bool: True if successful
Raises: Raises:
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
''' '''
@@ -317,7 +337,7 @@ class Device(object):
Returns: Returns:
bool: True if successful bool: True if successful
Raises: Raises:
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
''' '''
@@ -340,10 +360,10 @@ class Device(object):
def __str__(self): def __str__(self):
return f"<Device: nodeid={self.nodeid} name={self.name} description={self.description} computer_name={self.computer_name} icon={self.icon} "\ return f"<Device: nodeid={self.nodeid} name={self.name} description={self.description} computer_name={self.computer_name} icon={self.icon} "\
f"mesh={self.mesh} meshtype={self.meshtype} meshname={self.meshname} domain={self.domain} host={self.host} ip={self.ip} "\ f"mesh={self.mesh} meshtype={self.meshtype} meshname={self.meshname} domain={self.domain} host={self.host} ip={self.ip} "\
f"tags={self.tags} details={self.details} created_at={self.created_at} lastaddr={self.lastaddr} lastconnect={self.lastconnect} "\ f"tags={self.tags} users={self.users} details={self.details} created_at={self.created_at} lastaddr={self.lastaddr} lastconnect={self.lastconnect} "\
f"connected={self.connected} powered_on={self.powered_on} os_description={self.os_description} links={self.links} _extra_props={self._extra_props}>" f"connected={self.connected} powered_on={self.powered_on} os_description={self.os_description} links={self.links} _extra_props={self._extra_props}>"
def __repr__(self): def __repr__(self):
return f"Device(nodeid={repr(self.nodeid)}, session={repr(self._session)}, name={repr(self.name)}, description={repr(self.description)}, computer_name={repr(self.computer_name)}, icon={repr(self.icon)}, "\ return f"Device(nodeid={repr(self.nodeid)}, session={repr(self._session)}, name={repr(self.name)}, description={repr(self.description)}, computer_name={repr(self.computer_name)}, icon={repr(self.icon)}, "\
f"mesh={repr(self.mesh)}, meshtype={repr(self.meshtype)}, meshname={repr(self.meshname)}, domain={repr(self.domain)}, host={repr(self.host)}, ip={repr(self.ip)}, "\ f"mesh={repr(self.mesh)}, meshtype={repr(self.meshtype)}, meshname={repr(self.meshname)}, domain={repr(self.domain)}, host={repr(self.host)}, ip={repr(self.ip)}, "\
f"tags={repr(self.tags)}, details={repr(self.details)} created_at={repr(self.created_at)} lastaddr={repr(self.lastaddr)} lastconnect={repr(self.lastconnect)} "\ f"tags={repr(self.tags)}, users={repr(self.users)}, details={repr(self.details)} created_at={repr(self.created_at)} lastaddr={repr(self.lastaddr)} lastconnect={repr(self.lastconnect)} "\
f"connected={repr(self.connected)}, powered_on={repr(self.powered_on)}, os_description={repr(self.os_description)}, links={repr(self.links)}, **{repr(self._extra_props)})" f"connected={repr(self.connected)}, powered_on={repr(self.powered_on)}, os_description={repr(self.os_description)}, links={repr(self.links)}, **{repr(self._extra_props)})"

View File

@@ -2,7 +2,9 @@ class MeshCtrlError(Exception):
""" """
Base class for Meshctrl errors Base class for Meshctrl errors
""" """
pass def __init__(self, message, *args, **kwargs):
self.message = message
super().__init__(message, *args, **kwargs)
class ServerError(MeshCtrlError): class ServerError(MeshCtrlError):
""" """
@@ -22,12 +24,10 @@ class FileTransferError(MeshCtrlError):
Attributes: Attributes:
stats (dict): {"result" (str): Human readable result, "size" (int): number of bytes successfully transferred} stats (dict): {"result" (str): Human readable result, "size" (int): number of bytes successfully transferred}
initialized (asyncio.Event): Event marking if the Session initialization has finished. Wait on this to wait for a connection.
alive (bool): Whether the session connection is currently alive
closed (asyncio.Event): Event that occurs when the session closes permanently
""" """
def __init__(self, message, stats): def __init__(self, message, stats):
self.stats = stats self.stats = stats
super().__init__(message)
class FileTransferCancelled(FileTransferError): class FileTransferCancelled(FileTransferError):
""" """

View File

@@ -4,11 +4,32 @@ from . import exceptions
from . import util from . import util
import asyncio import asyncio
import json import json
import importlib
import importlib.util
import shutil
# import urllib
# import urllib.request
import urllib.parse
old_parse = urllib.parse
# Default proxy handler uses OS defined no_proxy in order to be helpful. This is unhelpful for our usecase. Monkey patch out proxy getting functions, but don't effect the user's urllib instance.
spec = importlib.util.find_spec('urllib')
urllib = importlib.util.module_from_spec(spec)
spec.loader.exec_module(urllib)
spec = importlib.util.find_spec('urllib.request')
urllib.request = importlib.util.module_from_spec(spec)
spec.loader.exec_module(urllib.request)
urllib.parse = old_parse
urllib.request.getproxies_environment = lambda: {}
urllib.request.getproxies_registry = lambda: {}
urllib.request.getproxies_macosx_sysconf = lambda: {}
urllib.request.getproxies = lambda: {}
class Files(tunnel.Tunnel): class Files(tunnel.Tunnel):
def __init__(self, session, nodeid): def __init__(self, session, node):
super().__init__(session, nodeid, constants.Protocol.FILES) super().__init__(session, node.nodeid, constants.Protocol.FILES)
self.recorded = None self.recorded = None
self._node = node
self._request_id = 0 self._request_id = 0
self._request_queue = asyncio.Queue() self._request_queue = asyncio.Queue()
self._download_finished = asyncio.Event() self._download_finished = asyncio.Event()
@@ -16,6 +37,17 @@ class Files(tunnel.Tunnel):
self._current_request = None self._current_request = None
self._handle_requests_task = asyncio.create_task(self._handle_requests()) self._handle_requests_task = asyncio.create_task(self._handle_requests())
self._chunk_size = 65564 self._chunk_size = 65564
proxies = {}
if self._session._proxy is not None:
# We don't know which protocol the user is going to use, but we only need support one at a time, so just assume both
proxies = {
"http": self._session._proxy,
"https": self._session._proxy,
"no": ""
}
self._proxy_handler = urllib.request.ProxyHandler(proxies=proxies)
self._http_opener = urllib.request.build_opener(self._proxy_handler, urllib.request.HTTPSHandler(context=self._session._ssl_context))
def _get_request_id(self): def _get_request_id(self):
self._request_id = (self._request_id+1)%(2**32-1) self._request_id = (self._request_id+1)%(2**32-1)
@@ -68,6 +100,7 @@ class Files(tunnel.Tunnel):
Args: Args:
directory (str): Path to the directory you wish to list directory (str): Path to the directory you wish to list
timeout (int): duration in seconds to wait for a response before throwing an error
Returns: Returns:
list[~meshctrl.types.FilesLSItem]: The directory listing list[~meshctrl.types.FilesLSItem]: The directory listing
@@ -75,6 +108,7 @@ class Files(tunnel.Tunnel):
Raises: Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error from server :py:class:`~meshctrl.exceptions.ServerError`: Error from server
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out
""" """
data = await self._send_command({"action": "ls", "path": directory}, "ls", timeout=timeout) data = await self._send_command({"action": "ls", "path": directory}, "ls", timeout=timeout)
return data["dir"] return data["dir"]
@@ -101,10 +135,12 @@ class Files(tunnel.Tunnel):
Args: Args:
directory (str): Path of directory to create directory (str): Path of directory to create
timeout (int): duration in seconds to wait for a response before throwing an error
Raises: Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error from server :py:class:`~meshctrl.exceptions.ServerError`: Error from server
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out
Returns: Returns:
bool: True if directory was created bool: True if directory was created
@@ -121,16 +157,18 @@ class Files(tunnel.Tunnel):
async def rm(self, path, files, recursive=False, timeout=None): async def rm(self, path, files, recursive=False, timeout=None):
""" """
Create a directory on the device. This API doesn't error if the file doesn't exist. Remove a set of files or directories from the device. This API doesn't error if the file doesn't exist.
Args: Args:
path (str): Directory from which to delete files path (str): Directory from which to delete files
files (str|list[str]): File or files to remove from the directory files (str|list[str]): File or files to remove from the directory
recursive (bool): Whether to delete the files recursively recursive (bool): Whether to delete the files recursively
timeout (int): duration in seconds to wait for a response before throwing an error
Raises: Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error from server :py:class:`~meshctrl.exceptions.ServerError`: Error from server
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out
Returns: Returns:
str: Info about the files removed. Something along the lines of Delete: "/path/to/file", or 'Delete recursive: "/path/to/dir", n element(s) removed'. str: Info about the files removed. Something along the lines of Delete: "/path/to/file", or 'Delete recursive: "/path/to/dir", n element(s) removed'.
@@ -155,10 +193,12 @@ class Files(tunnel.Tunnel):
path (str): Directory from which to rename the file path (str): Directory from which to rename the file
name (str): File to rename name (str): File to rename
new_name (str): New name to give the file new_name (str): New name to give the file
timeout (int): duration in seconds to wait for a response before throwing an error
Raises: Raises:
:py:class:`~meshctrl.exceptions.ServerError`: Error from server :py:class:`~meshctrl.exceptions.ServerError`: Error from server
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out
Returns: Returns:
str: Info about file renamed. Something along the lines of 'Rename: "/path/to/file" to "newfile"'. str: Info about file renamed. Something along the lines of 'Rename: "/path/to/file" to "newfile"'.
@@ -173,6 +213,7 @@ class Files(tunnel.Tunnel):
return tasks[2].result() return tasks[2].result()
@util._check_socket
async def upload(self, source, target, name=None, timeout=None): async def upload(self, source, target, name=None, timeout=None):
''' '''
Upload a stream to a device. Upload a stream to a device.
@@ -181,10 +222,12 @@ class Files(tunnel.Tunnel):
source (io.IOBase): An IO instance from which to read the data. Must be open for reading. source (io.IOBase): An IO instance from which to read the data. Must be open for reading.
target (str): Path which to upload stream to on remote device target (str): Path which to upload stream to on remote device
name (str): Pass if target points at a directory instead of the file path. In that case, this will be the name of the file. name (str): Pass if target points at a directory instead of the file path. In that case, this will be the name of the file.
timeout (int): duration in seconds to wait for a response before throwing an error
Raises: Raises:
:py:class:`~meshctrl.exceptions.FileTransferError`: File transfer failed. Info available on the `stats` property :py:class:`~meshctrl.exceptions.FileTransferError`: File transfer failed. Info available on the `stats` property
:py:class:`~meshctrl.exceptions.FileTransferCancelled`: File transfer cancelled. Info available on the `stats` property :py:class:`~meshctrl.exceptions.FileTransferCancelled`: File transfer cancelled. Info available on the `stats` property
asyncio.TimeoutError: Command timed out
Returns: Returns:
dict: {result: bool whether upload succeeded, size: number of bytes uploaded} dict: {result: bool whether upload succeeded, size: number of bytes uploaded}
@@ -198,17 +241,26 @@ class Files(tunnel.Tunnel):
raise request["error"] raise request["error"]
return request["return"] return request["return"]
async def download(self, source, target, timeout=None): def _http_download(self, url, target, timeout):
response = self._http_opener.open(url, timeout=timeout)
shutil.copyfileobj(response, target)
@util._check_socket
async def download(self, source, target, skip_http_attempt=False, skip_ws_attempt=False, timeout=None):
''' '''
Download a file from a device into a writable stream. Download a file from a device into a writable stream.
Args: Args:
source (str): Path from which to download from device source (str): Path from which to download from device
target (io.IOBase): Stream to which to write data. If None, create new BytesIO which is both readable and writable. target (io.IOBase): Stream to which to write data. If None, create new BytesIO which is both readable and writable.
skip_http_attempt (bool): Meshcentral has a way to download files through http(s) instead of through the websocket. This method tends to be much faster than using the websocket, so we try it first. Setting this to True will skip that attempt and just use the established websocket connection.
skip_ws_attempt (bool): Like skip_http_attempt, except just throw an error if the http attempt fails instead of trying with the websocket
timeout (int): duration in seconds to wait for a response before throwing an error
Raises: Raises:
:py:class:`~meshctrl.exceptions.FileTransferError`: File transfer failed. Info available on the `stats` property :py:class:`~meshctrl.exceptions.FileTransferError`: File transfer failed. Info available on the `stats` property
:py:class:`~meshctrl.exceptions.FileTransferCancelled`: File transfer cancelled. Info available on the `stats` property :py:class:`~meshctrl.exceptions.FileTransferCancelled`: File transfer cancelled. Info available on the `stats` property
asyncio.TimeoutError: Command timed out
Returns: Returns:
dict: {result: bool whether download succeeded, size: number of bytes downloaded} dict: {result: bool whether download succeeded, size: number of bytes downloaded}
@@ -216,6 +268,29 @@ class Files(tunnel.Tunnel):
request_id = f"download_{self._get_request_id()}" request_id = f"download_{self._get_request_id()}"
data = { "action": 'download', "sub": 'start', "id": request_id, "path": source } data = { "action": 'download', "sub": 'start', "id": request_id, "path": source }
request = {"id": request_id, "data": data, "type": "download", "source": source, "target": target, "size": 0, "finished": asyncio.Event(), "errored": asyncio.Event(), "error": None} request = {"id": request_id, "data": data, "type": "download", "source": source, "target": target, "size": 0, "finished": asyncio.Event(), "errored": asyncio.Event(), "error": None}
if not skip_http_attempt:
start_pos = target.tell()
try:
params = urllib.parse.urlencode({
"c": self._authcookie["cookie"],
"m": self._node.mesh.meshid.split("/")[-1],
"n": self._node.nodeid.split("/")[-1],
"f": source
})
url = self._session.url.replace('/control.ashx', f"/devicefile.ashx?{params}")
url = url.replace("wss://", "https://").replace("ws://", "http://")
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self._http_download, url, target, timeout)
size = target.tell() - start_pos
return {"result": True, "size": size}
except* Exception as eg:
if skip_ws_attempt:
size = target.tell() - start_pos
excs = eg.exceptions + (exceptions.FileTransferError("Errored", {"result": False, "size": size}),)
raise ExceptionGroup("File download failed", excs)
target.seek(start_pos)
await self._request_queue.put(request) await self._request_queue.put(request)
await asyncio.wait_for(request["finished"].wait(), timeout) await asyncio.wait_for(request["finished"].wait(), timeout)
if request["error"] is not None: if request["error"] is not None:
@@ -230,7 +305,7 @@ class Files(tunnel.Tunnel):
return return
if cmd["reqid"] == self._current_request["id"]: if cmd["reqid"] == self._current_request["id"]:
if cmd["action"] == "uploaddone": if cmd["action"] == "uploaddone":
self._current_request["return"] = {"result": "success", "size": self._current_request["size"]} self._current_request["return"] = {"result": True, "size": self._current_request["size"]}
self._current_request["finished"].set() self._current_request["finished"].set()
elif cmd["action"] == "uploadstart": elif cmd["action"] == "uploadstart":
while True: while True:
@@ -252,7 +327,7 @@ class Files(tunnel.Tunnel):
if self._current_request["inflight"] == 0 and self._current_request["complete"]: if self._current_request["inflight"] == 0 and self._current_request["complete"]:
await self._message_queue.put(json.dumps({ "action": 'uploaddone', "reqid": self._current_request["id"]})) await self._message_queue.put(json.dumps({ "action": 'uploaddone', "reqid": self._current_request["id"]}))
elif cmd["action"] == "uploaderror": elif cmd["action"] == "uploaderror":
self._current_request["return"] = {"result": "canceled", "size": self._current_request["size"]} self._current_request["return"] = {"result": False, "size": self._current_request["size"]}
self._current_request["error"] = exceptions.FileTransferError("Errored", self._current_request["return"]) self._current_request["error"] = exceptions.FileTransferError("Errored", self._current_request["return"])
self._current_request["errored"].set() self._current_request["errored"].set()
self._current_request["finished"].set() self._current_request["finished"].set()
@@ -268,7 +343,7 @@ class Files(tunnel.Tunnel):
self._current_request["target"].write(data[4:]) self._current_request["target"].write(data[4:])
self._current_request["size"] += len(data)-4 self._current_request["size"] += len(data)-4
if (data[3] & 1) != 0: if (data[3] & 1) != 0:
self._current_request["return"] = {"result": "success", "size": self._current_request["size"]} self._current_request["return"] = {"result": True, "size": self._current_request["size"]}
self._current_request["finished"].set() self._current_request["finished"].set()
else: else:
await self._message_queue.put(json.dumps({ "action": 'download', "sub": 'ack', "id": self._current_request["id"] })) await self._message_queue.put(json.dumps({ "action": 'download', "sub": 'ack', "id": self._current_request["id"] }))
@@ -279,7 +354,7 @@ class Files(tunnel.Tunnel):
if cmd["sub"] == "start": if cmd["sub"] == "start":
await self._message_queue.put(json.dumps({ "action": 'download', "sub": 'startack', "id": self._current_request["id"] })) await self._message_queue.put(json.dumps({ "action": 'download', "sub": 'startack', "id": self._current_request["id"] }))
elif cmd["sub"] == "cancel": elif cmd["sub"] == "cancel":
self._current_request["return"] = {"result": "canceled", "size": self._current_request["size"]} self._current_request["return"] = {"result": False, "size": self._current_request["size"]}
self._current_request["error"] = exceptions.FileTransferCancelled("Cancelled", self._current_request["return"]) self._current_request["error"] = exceptions.FileTransferCancelled("Cancelled", self._current_request["return"])
self._current_request["errored"].set() self._current_request["errored"].set()
self._current_request["finished"].set() self._current_request["finished"].set()

View File

@@ -31,7 +31,7 @@ class Mesh(object):
domain (str|None): Domain on server to which device is connected. domain (str|None): Domain on server to which device is connected.
links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device group links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device group
''' '''
def __init__(self, meshid, session, creation=None, created_at=None, name=None, def __init__(self, meshid, session, creation=None, created_at=None, name=None,
mtype=None, meshtype=None, creatorid=None, desc=None, description=None, mtype=None, meshtype=None, creatorid=None, desc=None, description=None,
domain=None, creatorname=None, links=None, **kwargs): domain=None, creatorname=None, links=None, **kwargs):
self.meshid = meshid self.meshid = meshid
@@ -46,7 +46,7 @@ class Mesh(object):
if not isinstance(created_at, datetime.datetime) and created_at is not None: if not isinstance(created_at, datetime.datetime) and created_at is not None:
try: try:
created_at = datetime.datetime.fromtimestamp(created_at) created_at = datetime.datetime.fromtimestamp(created_at)
except OSError: except (OSError, ValueError):
# Meshcentral returns in miliseconds, while fromtimestamp, and most of python, expects the argument in seconds. Try seconds frist, then translate from ms if it fails. # Meshcentral returns in miliseconds, while fromtimestamp, and most of python, expects the argument in seconds. Try seconds frist, then translate from ms if it fails.
# This doesn't work for really early timestamps, but I don't expect that to be a problem here. # This doesn't work for really early timestamps, but I don't expect that to be a problem here.
created_at = datetime.datetime.fromtimestamp(created_at/1000.0) created_at = datetime.datetime.fromtimestamp(created_at/1000.0)
@@ -83,7 +83,7 @@ class Mesh(object):
Returns: Returns:
dict[str, ~meshctrl.types.AddUsersToDeviceGroupResponse]: Object showing which were added correctly and which were not, along with their result messages. str is userid to map response. dict[str, ~meshctrl.types.AddUsersToDeviceGroupResponse]: Object showing which were added correctly and which were not, along with their result messages. str is userid to map response.
Raises: Raises:
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
asyncio.TimeoutError: Command timed out asyncio.TimeoutError: Command timed out
''' '''
@@ -96,4 +96,4 @@ class Mesh(object):
def __repr__(self): def __repr__(self):
return f"Mesh(meshid={repr(self.meshid)}, session={repr(self._session)}, name={repr(self.name)}, description={repr(self.description)}, created_at={repr(self.created_at)}, "\ return f"Mesh(meshid={repr(self.meshid)}, session={repr(self._session)}, name={repr(self.name)}, description={repr(self.description)}, created_at={repr(self.created_at)}, "\
f"meshtype={repr(self.meshtype)}, domain={repr(self.domain)}, "\ f"meshtype={repr(self.meshtype)}, domain={repr(self.domain)}, "\
f"created_at={repr(self.created_at)}, creatorid={repr(self.creatorid)}, creatorname={repr(self.creatorname)}, links={repr(self.links)}, **{repr(self._extra_props)})" f"created_at={repr(self.created_at)}, creatorid={repr(self.creatorid)}, creatorname={repr(self.creatorname)}, links={repr(self.links)}, **{repr(self._extra_props)})"

File diff suppressed because it is too large Load Diff

View File

@@ -78,7 +78,6 @@ class Shell(tunnel.Tunnel):
read_bytes = 0 read_bytes = 0
while True: while True:
d = self._buffer.read1(length-read_bytes if length is not None else -1) d = self._buffer.read1(length-read_bytes if length is not None else -1)
# print(f"read: {d}")
read_bytes += len(d) read_bytes += len(d)
ret.append(d) ret.append(d)
if length is not None and read_bytes >= length: if length is not None and read_bytes >= length:
@@ -163,7 +162,6 @@ class SmartShell(object):
command += "\n" command += "\n"
await self._shell.write(command) await self._shell.write(command)
data = await self._shell.expect(self._regex, timeout=timeout) data = await self._shell.expect(self._regex, timeout=timeout)
print(repr(data))
return data[:self._compiled_regex.search(data).span()[0]] return data[:self._compiled_regex.search(data).span()[0]]
@property @property
@@ -178,14 +176,18 @@ class SmartShell(object):
def initialized(self): def initialized(self):
return self._shell.initialized return self._shell.initialized
@property
def _socket_open(self):
return self._shell._socket_open
async def close(self): async def close(self):
await self._init_task await asyncio.wait_for(self._init_task, 10)
return await self._shell.close() return await self._shell.close()
async def __aenter__(self): async def __aenter__(self):
await self._init_task
await self._shell.__aenter__() await self._shell.__aenter__()
await asyncio.wait_for(self._init_task, 10)
return self return self
async def __aexit__(self, *args): async def __aexit__(self, *args):
return await self._shell.__aexit__(*args) await self.close()

View File

@@ -4,6 +4,8 @@ import websockets.asyncio
import websockets.asyncio.client import websockets.asyncio.client
import asyncio import asyncio
import ssl import ssl
from python_socks.async_.asyncio import Proxy
import urllib
from . import exceptions from . import exceptions
from . import util from . import util
from . import constants from . import constants
@@ -43,26 +45,18 @@ class Tunnel(object):
async def _main_loop(self): async def _main_loop(self):
try: try:
authcookie = await self._session._send_command_no_response_id({ "action":"authcookie" }) self._authcookie = await self._session._send_command_no_response_id({ "action":"authcookie" })
options = {} options = {}
if self._session._ignore_ssl: if self._session._ssl_context is not None:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) options["ssl"] = self._session._ssl_context
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
options = { "ssl": ssl_context }
# Setup the HTTP proxy if needed if (len(self.node_id.split('/')) != 3):
# if (self._session._proxy != None): self.node_id = f"node/{self._session._currentDomain or ''}/{self.node_id}"
# options.agent = new https_proxy_agent(urllib.parse(this._proxy))
if (self.node_id.split('/') != 3) and (self._session._currentDomain is not None):
self.node_id = f"node/{self._session._currentDomain}/{self.node_id}"
self._tunnel_id = util._get_random_hex(6) self._tunnel_id = util._get_random_hex(6)
initialize_tunnel_response = await self._session._send_command({ "action": 'msg', "nodeid": self.node_id, "type": 'tunnel', "usage": 1, "value": '*/meshrelay.ashx?p=' + str(self._protocol) + '&nodeid=' + self.node_id + '&id=' + self._tunnel_id + '&rauth=' + authcookie["rcookie"] }, "initialize_tunnel") initialize_tunnel_response = await self._session._send_command({ "action": 'msg', "nodeid": self.node_id, "type": 'tunnel', "usage": 1, "value": '*/meshrelay.ashx?p=' + str(self._protocol) + '&nodeid=' + self.node_id + '&id=' + self._tunnel_id + '&rauth=' + self._authcookie["rcookie"] }, "initialize_tunnel")
if initialize_tunnel_response.get("result", None) != "OK": if initialize_tunnel_response.get("result", None) != "OK":
self._main_loop_error = exceptions.ServerError(initialize_tunnel_response.get("result", "Failed to initialize remote tunnel")) self._main_loop_error = exceptions.ServerError(initialize_tunnel_response.get("result", "Failed to initialize remote tunnel"))
self._socket_open.clear() self._socket_open.clear()
@@ -70,16 +64,10 @@ class Tunnel(object):
self.initialized.set() self.initialized.set()
return return
self.url = self._session.url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=' + str(self._protocol) + '&nodeid=' + self.node_id + '&id=' + self._tunnel_id + '&auth=' + authcookie["cookie"]) self.url = self._session.url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=' + str(self._protocol) + '&nodeid=' + self.node_id + '&id=' + self._tunnel_id + '&auth=' + self._authcookie["cookie"])
# headers = websockets.datastructures.Headers()
# if (self._password): async for websocket in websockets.asyncio.client.connect(self.url, proxy=self._session._proxy, process_exception=util._process_websocket_exception, **options):
# token = self._token if self._token else b""
# headers['x-meshauth'] = (base64.b64encode(self._user.encode()) + b',' + base64.b64encode(self._password.encode()) + token).decode()
# options["additional_headers"] = headers
async for websocket in websockets.asyncio.client.connect(self.url, process_exception=util._process_websocket_exception, **options):
self.alive = True self.alive = True
self._socket_open.set() self._socket_open.set()
try: try:
@@ -89,7 +77,6 @@ class Tunnel(object):
except* websockets.ConnectionClosed as e: except* websockets.ConnectionClosed as e:
self._socket_open.clear() self._socket_open.clear()
if not self.auto_reconnect: if not self.auto_reconnect:
self.alive = False
raise raise
except* Exception as eg: except* Exception as eg:
self.alive = False self.alive = False
@@ -104,4 +91,4 @@ class Tunnel(object):
await websocket.send(message) await websocket.send(message)
async def _listen_data_task(self, websocket): async def _listen_data_task(self, websocket):
raise NotImplementedError("Listen data not implemented") raise NotImplementedError("Listen data not implemented")

View File

@@ -9,6 +9,8 @@ import re
import websockets import websockets
import ssl import ssl
import functools import functools
import urllib
import python_socks
from . import exceptions from . import exceptions
def _encode_cookie(o, key): def _encode_cookie(o, key):
@@ -137,19 +139,31 @@ def compare_dict(dict1, dict2):
return False return False
def _check_socket(f): def _check_socket(f):
@functools.wraps(f) async def _check_errs(self):
async def wrapper(self, *args, **kwargs):
await self.initialized.wait()
if not self.alive and self._main_loop_error is not None: if not self.alive and self._main_loop_error is not None:
raise self._main_loop_error raise self._main_loop_error
elif not self.alive: elif not self.alive and self.initialized.is_set():
raise exceptions.SocketError("Socket Closed") raise exceptions.SocketError("Socket Closed")
return await f(self, *args, **kwargs)
@functools.wraps(f)
async def wrapper(self, *args, **kwargs):
try:
await asyncio.wait_for(self.initialized.wait(), 10)
await _check_errs(self)
await asyncio.wait_for(self._socket_open.wait(), 10)
finally:
await _check_errs(self)
return await f(self, *args, **kwargs)
return wrapper return wrapper
def _process_websocket_exception(exc): def _process_websocket_exception(exc):
tmp = websockets.asyncio.client.process_exception(exc) tmp = websockets.asyncio.client.process_exception(exc)
# SSLVerification error is a subclass of OSError, but doesn't make sense no retry, so we need to handle it separately. # SSLVerification error is a subclass of OSError, but doesn't make sense to retry, so we need to handle it separately.
if isinstance(exc, (ssl.SSLCertVerificationError, TimeoutError)): if isinstance(exc, (ssl.SSLCertVerificationError, TimeoutError)):
return exc return exc
return tmp if isinstance(exc, python_socks._errors.ProxyError):
return None
# Proxy errors show up like this now, and it's default to error out. Handle explicitly.
if isinstance(exc, websockets.exceptions.InvalidProxyMessage):
return None
return tmp

3
tests/.gitignore vendored
View File

@@ -1 +1,2 @@
/data /data
/environment/scripts/meshcentral/users.json

View File

@@ -4,7 +4,10 @@ import subprocess
import time import time
import json import json
import atexit import atexit
import pytest try:
import pytest
except:
pass
import requests import requests
thisdir = os.path.abspath(os.path.dirname(__file__)) thisdir = os.path.abspath(os.path.dirname(__file__))
@@ -37,16 +40,16 @@ class Agent(object):
self._clienturl = clienturl self._clienturl = clienturl
self._dockerurl = dockerurl self._dockerurl = dockerurl
r = requests.post(f"{self._clienturl}/add-agent", json={"url": f"{self._dockerurl}", "meshid": self.meshid}) r = requests.post(f"{self._clienturl}/add-agent", json={"url": f"{self._dockerurl}", "meshid": self.meshid})
self.nodeid = r.json()["id"] agent_json = r.json()
self.nodeid = agent_json["id"]
self.nodehex = agent_json["hex"]
def __enter__(self): def __enter__(self):
return self return self
def __exit__(self, exc_t, exc_v, exc_tb): def __exit__(self, exc_t, exc_v, exc_tb):
try: requests.post(f"{self._clienturl}/remove-agent/{self.nodeid}")
requests.post("{self._clienturl}/remove-agent/{self.nodeid}")
except:
pass
class TestEnvironment(object): class TestEnvironment(object):
def __init__(self): def __init__(self):
@@ -54,15 +57,26 @@ class TestEnvironment(object):
self._subp = None self._subp = None
self.mcurl = "wss://localhost:8086" self.mcurl = "wss://localhost:8086"
self.clienturl = "http://localhost:5000" self.clienturl = "http://localhost:5000"
self._dockerurl = "host.docker.internal:8086" self.dockerurl = "host.docker.internal:8086"
self.proxyurl = "http://localhost:3128"
def __enter__(self): def __enter__(self):
global _docker_process global _docker_process
if _docker_process is not None: if _docker_process is not None:
self._subp = _docker_process self._subp = _docker_process
return self return self
self._subp = _docker_process = subprocess.Popen(["docker", "compose", "up", "--build", "--force-recreate", "--no-deps"], stdout=subprocess.DEVNULL, cwd=thisdir) # Destroy the env in case it wasn't killed correctly last time.
timeout = 30 subprocess.check_call(["docker", "compose", "down"], stdout=subprocess.DEVNULL, cwd=thisdir)
self._subp = _docker_process = subprocess.Popen(["docker", "compose", "up", "--build", "--force-recreate", "--no-deps"], cwd=thisdir)
if not self._wait_for_meshcentral():
self.__exit__(None, None, None)
raise Exception("Failed to create docker instance")
if not self._wait_for_client_server():
self.__exit__(None, None, None)
raise Exception("Failed to create client server")
return self
def _wait_for_meshcentral(self, timeout=30):
start = time.time() start = time.time()
while time.time() - start < timeout: while time.time() - start < timeout:
try: try:
@@ -79,16 +93,43 @@ class TestEnvironment(object):
pass pass
time.sleep(1) time.sleep(1)
else: else:
self.__exit__(None, None, None) return False
raise Exception("Failed to create docker instance") return True
return self
def _wait_for_client_server(self, timeout=30):
start = time.time()
while time.time() - start < timeout:
try:
data = subprocess.check_output(["docker", "inspect", "meshctrl-client", "--format='{{json .State.Health}}'"], cwd=thisdir, stderr=subprocess.DEVNULL)
# docker outputs for humans, not computers. This is the easiest way to chop off the ends
data = json.loads(data.strip()[1:-1])
except Exception as e:
time.sleep(1)
continue
try:
if data["Status"] == "healthy":
break
except:
pass
time.sleep(1)
else:
return False
return True
def __exit__(self, exc_t, exc_v, exc_tb): def __exit__(self, exc_t, exc_v, exc_tb):
pass pass
def create_agent(self, meshid): def create_agent(self, meshid):
return Agent(meshid, self.mcurl, self.clienturl, self._dockerurl) return Agent(meshid, self.mcurl, self.clienturl, self.dockerurl)
# Restart our docker instances, to test reconnect code.
def restart_mesh(self):
subprocess.check_call(["docker", "container", "restart", "meshctrl-meshcentral"], stdout=subprocess.DEVNULL, cwd=thisdir)
assert self._wait_for_meshcentral(), "Failed to restart docker instance"
def restart_proxy(self):
subprocess.check_call(["docker", "container", "restart", "meshctrl-squid"], stdout=subprocess.DEVNULL, cwd=thisdir)
def _kill_docker_process(): def _kill_docker_process():
if _docker_process is not None: if _docker_process is not None:
@@ -97,10 +138,13 @@ def _kill_docker_process():
atexit.register(_kill_docker_process) atexit.register(_kill_docker_process)
@pytest.fixture(scope="session") try:
def env(): @pytest.fixture(scope="session")
with TestEnvironment() as e: def env():
yield e with TestEnvironment() as e:
yield e
except:
pass
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,4 +1,4 @@
FROM python:3.12 FROM python:3.13
WORKDIR /usr/local/app WORKDIR /usr/local/app
# Install the application dependencies # Install the application dependencies

View File

@@ -9,6 +9,8 @@ services:
image: client image: client
build: build:
dockerfile: client.dockerfile dockerfile: client.dockerfile
sysctls:
net.ipv6.conf.all.disable_ipv6: 1
ports: ports:
- 5000:5000 - 5000:5000
depends_on: depends_on:
@@ -19,15 +21,21 @@ services:
# # mongodb data-directory - A must for data persistence # # mongodb data-directory - A must for data persistence
# - ./meshcentral/mongodb_data:/data/db # - ./meshcentral/mongodb_data:/data/db
networks: networks:
- meshctrl - meshctrl
healthcheck:
test: curl --fail http://localhost:5000/ || exit 1
interval: 5s
timeout: 120s
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
meshcentral: meshcentral:
restart: always restart: always
container_name: meshctrl-meshcentral container_name: meshctrl-meshcentral
# use the official meshcentral container # use the official meshcentral container
image: meshcentral image: meshcentral
sysctls:
net.ipv6.conf.all.disable_ipv6: 1
build: build:
dockerfile: meshcentral.dockerfile dockerfile: meshcentral.dockerfile
ports: ports:
@@ -49,4 +57,23 @@ services:
healthcheck: healthcheck:
test: curl -k --fail https://localhost:443/ || exit 1 test: curl -k --fail https://localhost:443/ || exit 1
interval: 5s interval: 5s
timeout: 120s timeout: 120s
squid:
image: ubuntu/squid:latest
restart: unless-stopped
container_name: meshctrl-squid
sysctls:
net.ipv6.conf.all.disable_ipv6: 1
ports:
- 3128:3128
networks:
- meshctrl
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./config/squid/conf.d:/etc/squid/conf.d
- ./config/squid/squid.conf:/etc/squid/squid.conf

View File

@@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@@ -0,0 +1,21 @@
# Logs are managed by logrotate on Debian
logfile_rotate 0
acl to_ipv6 dst ipv6
acl from_ipv6 src ipv6
acl to_ipv4 dst ipv4
acl from_ipv4 src ipv4
#acl all src all
acl Safe_ports port 8086
acl SSS_ports port 8086
http_access allow to_ipv4
http_access allow from_ipv4
http_access deny to_ipv6
http_access deny from_ipv6
http_access allow all
debug_options ALL,0 85,2 88,2
# Set max_filedescriptors to avoid using system's RLIMIT_NOFILE. See LP: #1978272
max_filedescriptors 1024

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
FROM ghcr.io/ylianst/meshcentral:latest FROM ghcr.io/ylianst/meshcentral:1.1.56
RUN apk add curl RUN apk add curl
RUN apk add python3 RUN apk add python3
WORKDIR /opt/meshcentral/ WORKDIR /opt/meshcentral/
COPY ./scripts/meshcentral ./scripts COPY ./scripts/meshcentral ./scripts
COPY ./meshcentral/data /opt/meshcentral/meshcentral-data COPY ./config/meshcentral/data /opt/meshcentral/meshcentral-data
CMD ["python3", "/opt/meshcentral/scripts/create_users.py"] COPY ./config/meshcentral/overrides /opt/meshcentral/meshcentral
ENTRYPOINT ["python3", "/opt/meshcentral/scripts/create_users.py"]

View File

@@ -53,7 +53,7 @@ def add_agent():
time.sleep(.1) time.sleep(.1)
else: else:
raise Exception(f"Failed to start agent: {text}") raise Exception(f"Failed to start agent: {text}")
return {"id": agent_id} return {"id": agent_id, "hex": agent_hex}
@api.route('/remove-agent/<agentid>', methods=['POST']) @api.route('/remove-agent/<agentid>', methods=['POST'])
def remove_agent(agentid): def remove_agent(agentid):
@@ -62,7 +62,7 @@ def remove_agent(agentid):
@api.route('/', methods=['GET']) @api.route('/', methods=['GET'])
def slash(): def slash():
return [_["id"] for _ in agents] return [value["id"] for key, value in agents.items()]
if __name__ == '__main__': if __name__ == '__main__':
api.run() api.run()

View File

@@ -7,9 +7,9 @@ thisdir = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(thisdir, "users.json")) as infile: with open(os.path.join(thisdir, "users.json")) as infile:
users = json.load(infile) users = json.load(infile)
for username, password in users.items(): for username, password in users.items():
subprocess.check_output(["node", "/opt/meshcentral/meshcentral", "--createaccount", username, "--pass", password, "--name", username]) print(subprocess.check_output(["node", "/opt/meshcentral/meshcentral", "--createaccount", username, "--pass", password, "--name", username]))
subprocess.check_output(["node", "/opt/meshcentral/meshcentral", "--adminaccount", "admin"]) print(subprocess.check_output(["node", "/opt/meshcentral/meshcentral", "--adminaccount", "admin"]))
subprocess.call(["bash", "/opt/meshcentral/startup.sh"]) subprocess.call(["bash", "/opt/meshcentral/entrypoint.sh"])

View File

@@ -1 +0,0 @@
{"admin": "3U6zP4iIes5ISH15XxjYLjJcCdw9jU0m", "privileged": "aiIO0zLMGsU7++FYVDNxhlpYlZ1andRB", "unprivileged": "Cz9OMV1wkVd9pXdWi4lkBAAu6TMt43MA"}

View File

@@ -1,6 +1,6 @@
requests requests
pytest-asyncio pytest-asyncio
cffi==1.17.1 cffi==1.17.1
cryptography==43.0.3
pycparser==2.22 pycparser==2.22
websockets==13.1 cryptography~=46.0.5
websockets~=16.0.0

View File

@@ -5,9 +5,10 @@ import meshctrl
import requests import requests
import io import io
import random import random
import time
async def test_commands(env): async def test_commands(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session: async with meshctrl.Session("wss://" + env.dockerurl, user="admin", password=env.users["admin"], ignore_ssl=True, proxy=env.proxyurl) as admin_session:
mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10) mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10)
try: try:
with env.create_agent(mesh.short_meshid) as agent: with env.create_agent(mesh.short_meshid) as agent:
@@ -52,8 +53,20 @@ async def test_commands(env):
finally: finally:
assert (await admin_session.remove_device_group(mesh.meshid, timeout=10)), "Failed to remove device group" assert (await admin_session.remove_device_group(mesh.meshid, timeout=10)), "Failed to remove device group"
async def test_os_proxy_bypass():
os.environ["no_proxy"] = "*"
import urllib
import urllib.request
os_proxies = urllib.request.getproxies()
meshctrl_proxies = meshctrl.files.urllib.request.getproxies()
print(f"os_proxies: {os_proxies}")
print(f"meshctrl_proxies: {meshctrl_proxies}")
assert meshctrl_proxies.get("no", None) == None, "Meshctrl is using system proxies"
assert os_proxies.get("no", None) == "*", "System is using meshctrl proxies"
assert os_proxies != meshctrl_proxies, "Override didn't work"
async def test_upload_download(env): async def test_upload_download(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session: async with meshctrl.Session("wss://" + env.dockerurl, user="admin", password=env.users["admin"], ignore_ssl=True, proxy=env.proxyurl) as admin_session:
mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10) mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10)
try: try:
with env.create_agent(mesh.short_meshid) as agent: with env.create_agent(mesh.short_meshid) as agent:
@@ -69,7 +82,7 @@ async def test_upload_download(env):
else: else:
break break
randdata = random.randbytes(2000000) randdata = random.randbytes(20000000)
upfilestream = io.BytesIO(randdata) upfilestream = io.BytesIO(randdata)
downfilestream = io.BytesIO() downfilestream = io.BytesIO()
@@ -78,7 +91,7 @@ async def test_upload_download(env):
async with admin_session.file_explorer(agent.nodeid) as files: async with admin_session.file_explorer(agent.nodeid) as files:
r = await files.upload(upfilestream, f"{pwd}/test", timeout=5) r = await files.upload(upfilestream, f"{pwd}/test", timeout=5)
print("\ninfo files_upload: {}\n".format(r)) print("\ninfo files_upload: {}\n".format(r))
assert r["result"] == "success", "Upload failed" assert r["result"] == True, "Upload failed"
assert r["size"] == len(randdata), "Uploaded wrong number of bytes" assert r["size"] == len(randdata), "Uploaded wrong number of bytes"
for f in await files.ls(pwd, timeout=5): for f in await files.ls(pwd, timeout=5):
if f["n"] == "test" and f["t"] == meshctrl.constants.FileType.FILE: if f["n"] == "test" and f["t"] == meshctrl.constants.FileType.FILE:
@@ -95,10 +108,23 @@ async def test_upload_download(env):
else: else:
raise Exception("Uploaded file not found") raise Exception("Uploaded file not found")
r = await files.download(f"{pwd}/test", downfilestream, timeout=5) start = time.perf_counter()
r = await files.download(f"{pwd}/test", downfilestream, skip_ws_attempt=True, timeout=5)
print("\ninfo files_download: {}\n".format(r)) print("\ninfo files_download: {}\n".format(r))
assert r["result"] == "success", "Domnload failed" assert r["result"] == True, "Download failed"
assert r["size"] == len(randdata), "Downloaded wrong number of bytes" assert r["size"] == len(randdata), "Downloaded wrong number of bytes"
print(f"http download time: {time.perf_counter()-start}")
downfilestream.seek(0)
assert downfilestream.read() == randdata, "Got wrong data back"
downfilestream.seek(0)
start = time.perf_counter()
r = await files.download(f"{pwd}/test", downfilestream, skip_http_attempt=True, timeout=20)
print("\ninfo files_download: {}\n".format(r))
assert r["result"] == True, "Download failed"
assert r["size"] == len(randdata), "Downloaded wrong number of bytes"
print(f"ws download time: {time.perf_counter()-start}")
downfilestream.seek(0) downfilestream.seek(0)
assert downfilestream.read() == randdata, "Got wrong data back" assert downfilestream.read() == randdata, "Got wrong data back"

View File

@@ -8,16 +8,39 @@ import ssl
import requests import requests
async def test_sanity(env): async def test_sanity(env):
async with meshctrl.session.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as s: async with meshctrl.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as s:
async with asyncio.TaskGroup() as tg:
ping_task = tg.create_task(s.ping(timeout=10))
print("\ninfo ping: {}\n".format(ping_task.result()))
print("\ninfo user_info: {}\n".format(await s.user_info())) print("\ninfo user_info: {}\n".format(await s.user_info()))
print("\ninfo server_info: {}\n".format(await s.server_info())) print("\ninfo server_info: {}\n".format(await s.server_info()))
pass pass
async def test_proxy(env):
async with meshctrl.Session("wss://" + env.dockerurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True, proxy=env.proxyurl) as s:
pass
async def test_ssl(env): async def test_ssl(env):
try: try:
async with meshctrl.session.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=False) as s: async with meshctrl.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=False) as s:
pass pass
except* ssl.SSLCertVerificationError: except* ssl.SSLCertVerificationError:
pass pass
else: else:
raise Exception("Invalid SSL certificate accepted") raise Exception("Invalid SSL certificate accepted")
async def test_urlparse():
# This tests the url port adding necessitated by python-socks. Our test environment doesn't use 443, so this is just a quick sanity test.
try:
async with meshctrl.Session("wss://localhost", user="unprivileged", password="Not a real password", ignore_ssl=True) as s:
pass
except* asyncio.TimeoutError:
#We're not running a server, so timeout is our expected outcome
pass
# This tests our check for wss/ws url schemes
try:
async with meshctrl.Session("https://localhost", user="unprivileged", password="Not a real password", ignore_ssl=True) as s:
pass
except* ValueError:
pass

View File

@@ -5,11 +5,13 @@ import meshctrl
import requests import requests
import random import random
import io import io
import traceback
import time
thisdir = os.path.dirname(os.path.realpath(__file__)) thisdir = os.path.dirname(os.path.realpath(__file__))
async def test_admin(env): async def test_admin(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\ async with meshctrl.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\
meshctrl.session.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session: meshctrl.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session:
admin_users = await admin_session.list_users(timeout=10) admin_users = await admin_session.list_users(timeout=10)
print("\ninfo list_users: {}\n".format(admin_users)) print("\ninfo list_users: {}\n".format(admin_users))
try: try:
@@ -29,27 +31,71 @@ async def test_admin(env):
assert len(no_sessions.keys()) == 0, "non-admin has admin acess" assert len(no_sessions.keys()) == 0, "non-admin has admin acess"
assert len(admin_users) == len(env.users.keys()), "Admin cannot see correct number of users" assert len(admin_users) == len(env.users.keys()), "Admin cannot see correct number of users"
assert len(admin_sessions) == 2, "Admin cannot see correct number of oser sessions" assert len(admin_sessions) == 2, "Admin cannot see correct number of user sessions"
async def test_auto_reconnect(env):
async with meshctrl.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True, auto_reconnect=True) as admin_session:
env.restart_mesh()
await asyncio.sleep(10)
await admin_session.ping(timeout=10)
# As above, but with proxy
async with meshctrl.Session("wss://" + env.dockerurl, user="admin", password=env.users["admin"], ignore_ssl=True, auto_reconnect=True, proxy=env.proxyurl) as admin_session:
env.restart_mesh()
for i in range(3):
try:
await admin_session.ping(timeout=10)
except* Exception as e:
print("".join(traceback.format_exception(e)))
pass
else:
break
else:
raise Exception("Failed to reconnect")
env.restart_proxy()
for i in range(3):
try:
await admin_session.ping(timeout=10)
except* Exception as e:
print("".join(traceback.format_exception(e)))
pass
else:
break
else:
raise Exception("Failed to reconnect")
async def test_users(env): async def test_users(env):
try: try:
async with meshctrl.session.Session(env.mcurl[3:], user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session: async with meshctrl.Session(env.mcurl[3:], user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session:
pass pass
except* ValueError: except* ValueError:
pass pass
else: else:
raise Exception("Connected with bad URL") raise Exception("Connected with bad URL")
try: try:
async with meshctrl.session.Session(env.mcurl, user="admin", ignore_ssl=True) as admin_session: async with meshctrl.Session(env.mcurl, user="admin", ignore_ssl=True) as admin_session:
pass pass
except* meshctrl.exceptions.MeshCtrlError: except* meshctrl.exceptions.MeshCtrlError:
pass pass
else: else:
raise Exception("Connected with no password") raise Exception("Connected with no password")
async with meshctrl.session.Session(env.mcurl+"/", user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\
meshctrl.session.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session,\ start = time.time()
meshctrl.session.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as unprivileged_session: try:
async with meshctrl.Session(env.mcurl, user="admin", password="The wrong password", ignore_ssl=True) as admin_session:
pass
except* meshctrl.exceptions.ServerError as eg:
assert str(eg.exceptions[0]) == "Invalid Auth" or eg.exceptions[0].message == "Invalid Auth", "Didn't get invalid auth message"
assert time.time() - start < 10, "Invalid auth wasn't raised until after timeout"
pass
else:
raise Exception("Connected with bad password")
async with meshctrl.Session(env.mcurl+"/", user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\
meshctrl.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session,\
meshctrl.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as unprivileged_session:
assert len(await admin_session.list_users(timeout=10)) == 3, "Wrong number of users" assert len(await admin_session.list_users(timeout=10)) == 3, "Wrong number of users"
@@ -74,17 +120,17 @@ async def test_users(env):
assert len(await admin_session.list_users(timeout=10)) == 3, "Failed to remove user" assert len(await admin_session.list_users(timeout=10)) == 3, "Failed to remove user"
async def test_login_token(env): async def test_login_token(env):
async with meshctrl.session.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as s: async with meshctrl.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as s:
token = await s.add_login_token("test", expire=1, timeout=10) token = await s.add_login_token("test", expire=1, timeout=10)
print("\ninfo add_login_token: {}\n".format(token)) print("\ninfo add_login_token: {}\n".format(token))
async with meshctrl.session.Session(env.mcurl, user=token["tokenUser"], password=token["tokenPass"], ignore_ssl=True) as s2: async with meshctrl.Session(env.mcurl, user=token["tokenUser"], password=token["tokenPass"], ignore_ssl=True) as s2:
assert (await s2.user_info())["_id"] == (await s.user_info())["_id"], "Login token logged into wrong account" assert (await s2.user_info())["_id"] == (await s.user_info())["_id"], "Login token logged into wrong account"
# Wait for the login token to expire # Wait for the login token to expire
await asyncio.sleep(65) await asyncio.sleep(65)
try: try:
async with meshctrl.session.Session(env.mcurl, user=token["tokenUser"], password=token["tokenPass"], ignore_ssl=True) as s2: async with meshctrl.Session(env.mcurl, user=token["tokenUser"], password=token["tokenPass"], ignore_ssl=True) as s2:
pass pass
except: except:
pass pass
@@ -94,7 +140,7 @@ async def test_login_token(env):
token = await s.add_login_token("test2", timeout=10) token = await s.add_login_token("test2", timeout=10)
token2 = await s.add_login_token("test3", timeout=10) token2 = await s.add_login_token("test3", timeout=10)
print("\ninfo add_login_token_no_expire: {}\n".format(token)) print("\ninfo add_login_token_no_expire: {}\n".format(token))
async with meshctrl.session.Session(env.mcurl, user=token["tokenUser"], password=token["tokenPass"], ignore_ssl=True) as s2: async with meshctrl.Session(env.mcurl, user=token["tokenUser"], password=token["tokenPass"], ignore_ssl=True) as s2:
assert (await s2.user_info())["_id"] == (await s.user_info())["_id"], "Login token logged into wrong account" assert (await s2.user_info())["_id"] == (await s.user_info())["_id"], "Login token logged into wrong account"
r = await s.list_login_tokens(timeout=10) r = await s.list_login_tokens(timeout=10)
@@ -107,9 +153,9 @@ async def test_login_token(env):
assert len(await s.remove_login_token([token2["name"]], timeout=10)) == 0, "Residual login tokens" assert len(await s.remove_login_token([token2["name"]], timeout=10)) == 0, "Residual login tokens"
async def test_mesh_device(env): async def test_mesh_device(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\ async with meshctrl.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\
meshctrl.session.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session,\ meshctrl.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session,\
meshctrl.session.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as unprivileged_session: meshctrl.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as unprivileged_session:
# Test creating a mesh # Test creating a mesh
mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10) mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10)
print("\ninfo add_device_group: {}\n".format(mesh)) print("\ninfo add_device_group: {}\n".format(mesh))
@@ -157,21 +203,24 @@ async def test_mesh_device(env):
assert r[0].description == "New description", "Description either failed to change, or was changed by a user without permission to do so" assert r[0].description == "New description", "Description either failed to change, or was changed by a user without permission to do so"
with env.create_agent(mesh.short_meshid) as agent: # There once was a bug that occured whenever running run_commands with multiple meshes. We need to add devices to both meshes to be sure that bug is squashed.
with env.create_agent(mesh.short_meshid) as agent,\
env.create_agent(mesh.short_meshid) as agent2,\
env.create_agent(mesh2.short_meshid) as agent3:
# Test agent added to device group being propagated correctly # Test agent added to device group being propagated correctly
# Create agent isn't so good at waiting for the agent to show in the sessions. Give it a couple seconds to appear. # Create agent isn't so good at waiting for the agent to show in the sessions. Give it a couple seconds to appear.
for i in range(3): for i in range(3):
try: try:
r = await admin_session.list_devices(timeout=10) r = await admin_session.list_devices(timeout=10)
print("\ninfo list_devices: {}\n".format(r)) print("\ninfo list_devices: {}\n".format(r))
assert len(r) == 1, "Incorrect number of agents connected" assert len(r) == 3, "Incorrect number of agents connected"
except: except:
if i == 2: if i == 2:
raise raise
await asyncio.sleep(1) await asyncio.sleep(1)
else: else:
break break
assert len(await privileged_session.list_devices(timeout=10)) == 1, "Incorrect number of agents connected" assert len(await privileged_session.list_devices(timeout=10)) == 2, "Incorrect number of agents connected"
assert len(await unprivileged_session.list_devices(timeout=10)) == 0, "Unprivileged account has access to agent it should not" assert len(await unprivileged_session.list_devices(timeout=10)) == 0, "Unprivileged account has access to agent it should not"
r = await admin_session.list_devices(details=True, timeout=10) r = await admin_session.list_devices(details=True, timeout=10)
@@ -183,6 +232,9 @@ async def test_mesh_device(env):
r = await admin_session.list_devices(meshid=mesh.meshid, timeout=10) r = await admin_session.list_devices(meshid=mesh.meshid, timeout=10)
print("\ninfo list_devices_meshid: {}\n".format(r)) print("\ninfo list_devices_meshid: {}\n".format(r))
r = await admin_session.device_info(agent.nodeid, timeout=10)
print("\ninfo admin_device_info: {}\n".format(r))
# Test editing device info propagating correctly # Test editing device info propagating correctly
assert await admin_session.edit_device(agent.nodeid, name="new_name", description="New Description", tags="device", consent=meshctrl.constants.ConsentFlags.all, timeout=10), "Failed to edit device info" assert await admin_session.edit_device(agent.nodeid, name="new_name", description="New Description", tags="device", consent=meshctrl.constants.ConsentFlags.all, timeout=10), "Failed to edit device info"
@@ -191,12 +243,49 @@ async def test_mesh_device(env):
assert await admin_session.edit_device(agent.nodeid, consent=meshctrl.constants.ConsentFlags.none, timeout=10), "Failed to edit device info" assert await admin_session.edit_device(agent.nodeid, consent=meshctrl.constants.ConsentFlags.none, timeout=10), "Failed to edit device info"
# Test run_commands # Test run_commands
r = await admin_session.run_command(agent.nodeid, "ls", timeout=10) r = await admin_session.run_command([agent.nodeid, agent2.nodeid], "ls", timeout=10)
print("\ninfo run_command: {}\n".format(r)) print("\ninfo run_command: {}\n".format(r))
assert "meshagent" in r[agent.nodeid]["result"], "ls gave incorrect data" assert "meshagent" in r[agent.nodeid]["result"], "ls gave incorrect data"
assert "meshagent" in r[agent2.nodeid]["result"], "ls gave incorrect data"
assert "Run commands completed." not in r[agent.nodeid]["result"], "Didn't parse run command ending correctly"
assert "Run commands completed." not in r[agent2.nodeid]["result"], "Didn't parse run command ending correctly"
assert "meshagent" in (await privileged_session.run_command(agent.nodeid, "ls", timeout=10))[agent.nodeid]["result"], "ls gave incorrect data" assert "meshagent" in (await privileged_session.run_command(agent.nodeid, "ls", timeout=10))[agent.nodeid]["result"], "ls gave incorrect data"
# Test run commands with ndividual device permissions # Test run_commands ignore output
r = await admin_session.run_command([agent.nodeid, agent2.nodeid], "ls", ignore_output=True, timeout=10)
print("\ninfo run_command ignore_output: {}\n".format(r))
assert r[agent.nodeid]["result"] == '', "Ignore output returned an output"
assert r[agent2.nodeid]["result"] == '', "Ignore output returned an output"
# Test run_commands missing device
try:
await admin_session.run_command([agent.nodeid, "notanid"], "ls", timeout=10)
except* (meshctrl.exceptions.ServerError, ValueError):
pass
else:
raise Exception("Run command on a device that doesn't exist did not raise an exception")
# Test run_console_command
r = await admin_session.run_console_command([agent.nodeid, agent2.nodeid], "info", timeout=10)
print("\ninfo run_console_command: {}\n".format(r))
assert agent.nodeid in r[agent.nodeid]["result"], "Run console command gave bad response"
assert agent2.nodeid in r[agent2.nodeid]["result"], "Run console command gave bad response"
# Test run_console_command ignore output
r = await admin_session.run_console_command([agent.nodeid, agent2.nodeid], "info", timeout=10, ignore_output=True)
print("\ninfo run_console_command ignore_output: {}\n".format(r))
assert r[agent.nodeid]["result"] == '', "Ignore output returned an output"
assert r[agent2.nodeid]["result"] == '', "Ignore output returned an output"
# Test run_commands missing device
try:
await admin_session.run_console_command([agent.nodeid, "notanid"], "info", timeout=10)
except* (meshctrl.exceptions.ServerError, ValueError):
pass
else:
raise Exception("Run console command on a device that doesn't exist did not raise an exception")
# Test run commands with individual device permissions
try: try:
await unprivileged_session.run_command(agent.nodeid, "ls", timeout=10) await unprivileged_session.run_command(agent.nodeid, "ls", timeout=10)
except* (meshctrl.exceptions.ServerError, ValueError): except* (meshctrl.exceptions.ServerError, ValueError):
@@ -211,7 +300,7 @@ async def test_mesh_device(env):
else: else:
raise Exception("Unprivileged user has access to device it should not") raise Exception("Unprivileged user has access to device it should not")
assert (await admin_session.add_users_to_device((await unprivileged_session.user_info())["_id"], agent.nodeid, meshctrl.constants.MeshRights.norights)), "Failed to add user to device" assert (await admin_session.add_users_to_device((await unprivileged_session.user_info())["_id"], agent.nodeid, meshctrl.constants.DeviceRights.norights)), "Failed to add user to device"
try: try:
await unprivileged_session.run_command(agent.nodeid, "ls", ignore_output=True, timeout=10) await unprivileged_session.run_command(agent.nodeid, "ls", ignore_output=True, timeout=10)
@@ -222,19 +311,21 @@ async def test_mesh_device(env):
# Test getting individual device info # Test getting individual device info
r = await unprivileged_session.device_info(agent.nodeid, timeout=10) r = await unprivileged_session.device_info(agent.nodeid, timeout=10)
print("\ninfo device_info: {}\n".format(r)) print("\ninfo unprivileged_device_info: {}\n".format(r))
# This device info includes the mesh ID of the device, even though the user doesn't have acces to that mesh. That's odd. # This device info includes the mesh ID of the device, even though the user doesn't have acces to that mesh. That's odd.
# assert r.meshid is None, "Individual device is exposing its meshid" # assert r.meshid is None, "Individual device is exposing its meshid"
assert r.links[(await unprivileged_session.user_info())["_id"]]["rights"] == meshctrl.constants.DeviceRights.norights, "Unprivileged user has too many rights!" assert r.links[(await unprivileged_session.user_info())["_id"]]["rights"] == meshctrl.constants.DeviceRights.norights, "Unprivileged user has too many rights!"
assert (await admin_session.add_users_to_device([(await unprivileged_session.user_info())["_id"]], agent.nodeid, meshctrl.constants.DeviceRights.remotecontrol|meshctrl.constants.DeviceRights.agentconsole|meshctrl.constants.DeviceRights.remotecommands)), "Failed to modify user's permissions" assert (await admin_session.add_users_to_device([(await unprivileged_session.user_info())["_id"]], agent.nodeid, meshctrl.constants.DeviceRights.fullrights)), "Failed to modify user's permissions"
assert (await unprivileged_session.device_info(agent.nodeid, timeout=10)).links[(await unprivileged_session.user_info())["_id"]]["rights"] == meshctrl.constants.DeviceRights.remotecontrol|meshctrl.constants.DeviceRights.agentconsole|meshctrl.constants.DeviceRights.remotecommands, "Adding permissions did not update unprivileged user." assert (await unprivileged_session.device_info(agent.nodeid, timeout=10)).links[(await unprivileged_session.user_info())["_id"]]["rights"] == meshctrl.constants.DeviceRights.fullrights, "Adding permissions did not update unprivileged user."
# For now, this expects no response. If we ever figure out why the server isn't sending console information te us when it should, fix this. # For now, this expects no response. If we ever figure out why the server isn't sending console information to us when it should, fix this.
# assert "meshagent" in (await unprivileged_session.run_command(agent.nodeid, "ls", timeout=10))[agent.nodeid]["result"], "ls gave incorrect data" # assert "meshagent" in (await unprivileged_session.run_command(agent.nodeid, "ls", timeout=10))[agent.nodeid]["result"], "ls gave incorrect data"
# Meshcentral has a 10 second cache on user perms.
#await asyncio.sleep(15)
await unprivileged_session.run_command(agent.nodeid, "ls", timeout=10) await unprivileged_session.run_command(agent.nodeid, "ls", timeout=10)
assert await admin_session.move_to_device_group(agent.nodeid, mesh2.meshid, timeout=5), "Failed to move mesh to new device group" assert await admin_session.move_to_device_group(agent.nodeid, mesh2.meshid, timeout=5), "Failed to move mesh to new device group"
@@ -255,20 +346,29 @@ async def test_mesh_device(env):
except: except:
raise Exception("Failed to run command on device after it was moved to a new mesh while having individual device permissions") raise Exception("Failed to run command on device after it was moved to a new mesh while having individual device permissions")
r = await admin_session.remove_users_from_device_group((await privileged_session.user_info())["_id"], mesh.meshid, timeout=10) r = await admin_session.remove_users_from_device_group((await privileged_session.user_info())["_id"], mesh.meshid, timeout=10)
print("\ninfo remove_users_from_device_group: {}\n".format(r)) print("\ninfo remove_users_from_device_group: {}\n".format(r))
assert (await admin_session.remove_users_from_device(agent.nodeid, (await unprivileged_session.user_info())["_id"], timeout=10)), "Failed to remove user from device" assert (r[(await privileged_session.user_info())["_id"]]["success"]), "Failed to remove user from device group"
assert (r[(await privileged_session.user_info())["_id"]]["success"]), "Failed to remove user from devcie group" await admin_session.remove_devices(agent2.nodeid, timeout=10)
try:
await admin_session.device_info(agent2.nodeid, timeout=10)
except ValueError:
pass
else:
raise Exception("Device not deleted")
assert (await admin_session.remove_users_from_device(agent.nodeid, (await unprivileged_session.user_info())["_id"], timeout=10)), "Failed to remove user from device"
assert (await admin_session.remove_device_group(mesh.meshid, timeout=10)), "Failed to remove device group" assert (await admin_session.remove_device_group(mesh.meshid, timeout=10)), "Failed to remove device group"
assert (await admin_session.remove_device_group(mesh2.name, isname=True, timeout=10)), "Failed to remove device group" assert (await admin_session.remove_device_group(mesh2.name, isname=True, timeout=10)), "Failed to remove device group by name"
assert not (await admin_session.add_users_to_device_group((await privileged_session.user_info())["_id"], mesh.meshid, rights=meshctrl.constants.MeshRights.fullrights, timeout=5))[(await privileged_session.user_info())["_id"]]["success"], "Added user to device group which doesn't exist?" assert not (await admin_session.add_users_to_device_group((await privileged_session.user_info())["_id"], mesh.meshid, rights=meshctrl.constants.MeshRights.fullrights, timeout=5))[(await privileged_session.user_info())["_id"]]["success"], "Added user to device group which doesn't exist?"
async def test_user_groups(env): async def test_user_groups(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\ async with meshctrl.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\
meshctrl.session.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session,\ meshctrl.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session,\
meshctrl.session.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as unprivileged_session: meshctrl.Session(env.mcurl, user="unprivileged", password=env.users["unprivileged"], ignore_ssl=True) as unprivileged_session:
user_group = await admin_session.add_user_group("test", description="aoeu") user_group = await admin_session.add_user_group("test", description="aoeu")
print("\ninfo add_user_group: {}\n".format(user_group)) print("\ninfo add_user_group: {}\n".format(user_group))
@@ -294,7 +394,7 @@ async def test_user_groups(env):
assert await admin_session.remove_user_group(user_group2.id.split("/")[-1]) assert await admin_session.remove_user_group(user_group2.id.split("/")[-1])
async def test_events(env): async def test_events(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session: async with meshctrl.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session:
await admin_session.list_events() await admin_session.list_events()
mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10) mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10)
try: try:
@@ -310,7 +410,7 @@ async def test_events(env):
await asyncio.sleep(1) await asyncio.sleep(1)
else: else:
break break
async with meshctrl.session.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session: async with meshctrl.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session:
# assert len(await privileged_session.list_events()) == 0, "non-admin user has access to admin events" # assert len(await privileged_session.list_events()) == 0, "non-admin user has access to admin events"
@@ -337,8 +437,8 @@ async def test_events(env):
assert (await admin_session.remove_device_group(mesh.meshid, timeout=10)), "Failed to remove device group" assert (await admin_session.remove_device_group(mesh.meshid, timeout=10)), "Failed to remove device group"
async def test_interuser(env): async def test_interuser(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\ async with meshctrl.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session,\
meshctrl.session.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session: meshctrl.Session(env.mcurl, user="privileged", password=env.users["privileged"], ignore_ssl=True) as privileged_session:
got_message = asyncio.Event() got_message = asyncio.Event()
async def _(): async def _():
async for message in admin_session.events({"action": "interuser"}): async for message in admin_session.events({"action": "interuser"}):
@@ -361,7 +461,7 @@ async def test_interuser(env):
tg.create_task(asyncio.wait_for(got_message.wait(), 5)) tg.create_task(asyncio.wait_for(got_message.wait(), 5))
async def test_session_files(env): async def test_session_files(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session: async with meshctrl.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session:
mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10) mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10)
try: try:
with env.create_agent(mesh.short_meshid) as agent: with env.create_agent(mesh.short_meshid) as agent:
@@ -378,7 +478,7 @@ async def test_session_files(env):
break break
pwd = (await admin_session.run_command(agent.nodeid, "pwd", timeout=10))[agent.nodeid]["result"].strip() pwd = (await admin_session.run_command(agent.nodeid, "pwd", timeout=10))[agent.nodeid]["result"].strip()
randdata = random.randbytes(2000000) randdata = random.randbytes(20000000)
upfilestream = io.BytesIO(randdata) upfilestream = io.BytesIO(randdata)
downfilestream = io.BytesIO() downfilestream = io.BytesIO()
os.makedirs(os.path.join(thisdir, "data"), exist_ok=True) os.makedirs(os.path.join(thisdir, "data"), exist_ok=True)
@@ -387,20 +487,18 @@ async def test_session_files(env):
r = await admin_session.upload(agent.nodeid, upfilestream, f"{pwd}/test", timeout=5) r = await admin_session.upload(agent.nodeid, upfilestream, f"{pwd}/test", timeout=5)
print("\ninfo files_upload: {}\n".format(r)) print("\ninfo files_upload: {}\n".format(r))
assert r["result"] == "success", "Upload failed" assert r["result"] == True, "Upload failed"
assert r["size"] == len(randdata), "Uploaded wrong number of bytes" assert r["size"] == len(randdata), "Uploaded wrong number of bytes"
r = await admin_session.upload_file(agent.nodeid, os.path.join(thisdir, "data", "test"), f"{pwd}/test2", timeout=5) r = await admin_session.upload_file(agent.nodeid, os.path.join(thisdir, "data", "test"), f"{pwd}/test2", timeout=5)
print("\ninfo files_upload: {}\n".format(r)) print("\ninfo files_upload: {}\n".format(r))
assert r["result"] == "success", "Upload failed" assert r["result"] == True, "Upload failed"
assert r["size"] == len(randdata), "Uploaded wrong number of bytes" assert r["size"] == len(randdata), "Uploaded wrong number of bytes"
s = await admin_session.download(agent.nodeid, f"{pwd}/test", timeout=5) s = await admin_session.download(agent.nodeid, f"{pwd}/test", timeout=5)
s.seek(0)
assert s.read() == randdata, "Downloaded bad data" assert s.read() == randdata, "Downloaded bad data"
await admin_session.download(agent.nodeid, f"{pwd}/test", downfilestream, timeout=5) await admin_session.download(agent.nodeid, f"{pwd}/test", downfilestream, timeout=5)
downfilestream.seek(0)
assert downfilestream.read() == randdata, "Downloaded bad data" assert downfilestream.read() == randdata, "Downloaded bad data"
await admin_session.download_file(agent.nodeid, f"{pwd}/test2", os.path.join(thisdir, "data", "test"), timeout=5) await admin_session.download_file(agent.nodeid, f"{pwd}/test2", os.path.join(thisdir, "data", "test"), timeout=5)
@@ -410,7 +508,7 @@ async def test_session_files(env):
r = await admin_session.upload_file(agent.nodeid, os.path.join(thisdir, "data", "test"), f"{pwd}/test2", unique_file_tunnel=True, timeout=5) r = await admin_session.upload_file(agent.nodeid, os.path.join(thisdir, "data", "test"), f"{pwd}/test2", unique_file_tunnel=True, timeout=5)
assert r["result"] == "success", "Upload failed" assert r["result"] == True, "Upload failed"
assert r["size"] == len(randdata), "Uploaded wrong number of bytes" assert r["size"] == len(randdata), "Uploaded wrong number of bytes"
await admin_session.download_file(agent.nodeid, f"{pwd}/test2", os.path.join(thisdir, "data", "test"), unique_file_tunnel=True, timeout=5) await admin_session.download_file(agent.nodeid, f"{pwd}/test2", os.path.join(thisdir, "data", "test"), unique_file_tunnel=True, timeout=5)

View File

@@ -5,7 +5,7 @@ import meshctrl
import requests import requests
async def test_shell(env): async def test_shell(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session: async with meshctrl.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session:
mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10) mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10)
try: try:
with env.create_agent(mesh.short_meshid) as agent: with env.create_agent(mesh.short_meshid) as agent:
@@ -40,7 +40,7 @@ async def test_shell(env):
async def test_smart_shell(env): async def test_smart_shell(env):
async with meshctrl.session.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session: async with meshctrl.Session(env.mcurl, user="admin", password=env.users["admin"], ignore_ssl=True) as admin_session:
mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10) mesh = await admin_session.add_device_group("test", description="This is a test group", amtonly=False, features=0, consent=0, timeout=10)
try: try:
with env.create_agent(mesh.short_meshid) as agent: with env.create_agent(mesh.short_meshid) as agent: