forked from Narcissus/pylibmeshctrl
Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cefd24a9d | ||
|
|
cbc1f9223f | ||
|
|
3fa1ca2e32 | ||
|
|
ee812220fb | ||
|
|
002f652c8c | ||
|
|
0b09f64821 | ||
|
|
12a3040f89 | ||
|
|
e0694f980c | ||
|
|
61053549f2 | ||
|
|
fb3d043431 | ||
|
|
c13985739b | ||
|
|
db1914c87b | ||
|
|
b0d071d87f | ||
|
|
3bcedf5610 | ||
|
|
9c7a8c39b0 | ||
|
|
7ba6989325 | ||
|
|
748e39d5b4 | ||
|
|
6dae40eb40 | ||
|
|
c7d628716e | ||
|
|
1f9979ddd1 | ||
| d4b9524814 | |||
|
|
bc1db8f2b3 | ||
|
|
403c0cd0ec | ||
|
|
0b0029563a | ||
|
|
0b32896c88 | ||
|
|
2304810ee6 | ||
|
|
4cda54ab60 | ||
|
|
87fad5aa13 | ||
|
|
6daaa91758 | ||
|
|
078e07cb4f | ||
|
|
0e569ae0cb | ||
|
|
62fdc79aeb | ||
|
|
c450ad7a96 | ||
|
|
891f7bfc12 | ||
|
|
4953d85cdc | ||
|
|
f5c6e96597 | ||
|
|
428a1b31c7 | ||
|
|
16f3f99427 | ||
|
|
d21450e463 | ||
|
|
9e08a1af49 | ||
|
|
e9de43420e | ||
|
|
fcdf8add53 | ||
|
|
163b776dfc | ||
|
|
04c8f622de | ||
|
|
ccb5f1eb40 | ||
|
|
ce2cf2bfe1 | ||
|
|
a3b4962e7f | ||
|
|
5947e48c5b | ||
|
|
31a8f00cd0 | ||
|
|
871d36b334 | ||
|
|
59fb1f104e | ||
|
|
9bd3e10ed7 | ||
|
|
28e1d94ab9 | ||
|
|
51325a89d3 | ||
|
|
97dff80222 | ||
|
|
8da445348b | ||
|
|
ab1fba5cc1 | ||
|
|
34a80cdda7 | ||
|
|
fcf523dd62 | ||
|
|
9a1311167d | ||
|
|
c2319fcf29 | ||
|
|
4d1c25a35c | ||
|
|
e226fff8dd | ||
|
|
a07b0f129a | ||
|
|
64dc5eccdf | ||
|
|
1a7714663a | ||
|
|
0a59edd19a | ||
|
|
f8600b09fe | ||
|
|
351f425ce5 | ||
|
|
77e76aeb7c | ||
|
|
5393321f7b | ||
|
|
79554ebad6 | ||
|
|
1dbcd012ec | ||
|
|
ace6884991 | ||
|
|
61eebf1532 | ||
|
|
fcfeac21a8 | ||
|
|
19d10ee050 | ||
|
|
0c9ebf0ff2 | ||
|
|
2556e72a73 | ||
|
|
cda5f610a1 | ||
|
|
564d466ff9 | ||
|
|
125e6ac6ac | ||
|
|
1b849473bb | ||
|
|
df25652ba6 | ||
|
|
9668e4d507 | ||
|
|
fe4c2fe874 | ||
|
|
bb7cf17cd3 | ||
|
|
6919da4a42 | ||
|
|
ff120490fa | ||
|
|
d9991156f6 | ||
|
|
4fea858fbc | ||
|
|
3b4a18b379 | ||
|
|
c072d6012a | ||
|
|
0ee2e2dc94 | ||
|
|
f2d9fcd295 | ||
|
|
7456743709 | ||
|
|
07b828a150 | ||
|
|
cd7a356eb5 | ||
|
|
5ee2c8edf3 | ||
|
|
d3d5b87287 | ||
|
|
18eb2de5b6 | ||
|
|
ec23ba458d | ||
|
|
a3c721318d | ||
|
|
4eda4e6c08 | ||
|
|
ab2a4c40bc | ||
|
|
0a657cee48 | ||
|
|
03441161b2 | ||
|
|
24adf3baa5 | ||
|
|
1adaccabc0 | ||
|
|
20843dbea7 | ||
|
|
af6c020506 | ||
|
|
b870aa25bd | ||
|
|
c63604f624 | ||
|
|
f0e09c0082 | ||
|
|
184ce3ef3e | ||
|
|
33680dab5d |
@@ -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:
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ Contributors
|
|||||||
============
|
============
|
||||||
|
|
||||||
* Josiah Baldwin <jbaldwin8889@gmail.com>
|
* Josiah Baldwin <jbaldwin8889@gmail.com>
|
||||||
|
* Daan Selen <https://github.com/DaanSelen>
|
||||||
@@ -2,7 +2,84 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Version 0.1
|
version 1.3.2
|
||||||
===========
|
=============
|
||||||
|
|
||||||
Create
|
Improvments:
|
||||||
|
* 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
|
||||||
|
|||||||
13
README.rst
13
README.rst
@@ -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
|
||||||
------------------
|
------------------
|
||||||
|
|||||||
@@ -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
|
cryptography~=44.0.1
|
||||||
pycparser==2.22
|
pycparser~=2.22
|
||||||
websockets==13.1
|
websockets~=15.0.0
|
||||||
enum_tools
|
enum_tools
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -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~=44.0.1
|
||||||
websockets>=13.1
|
websockets~=15.0.0
|
||||||
|
python-socks[asyncio]~=2.5.3
|
||||||
|
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -56,7 +58,7 @@ class Device(object):
|
|||||||
'''
|
'''
|
||||||
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,
|
||||||
@@ -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)
|
||||||
@@ -292,6 +295,23 @@ class Device(object):
|
|||||||
'''
|
'''
|
||||||
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
|
||||||
@@ -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)})"
|
||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
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
|
return tmp
|
||||||
1
tests/.gitignore
vendored
1
tests/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/data
|
/data
|
||||||
|
/environment/scripts/meshcentral/users.json
|
||||||
@@ -37,16 +37,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 +54,23 @@ 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")
|
||||||
|
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 +87,23 @@ 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 __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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ 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
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
meshcentral:
|
meshcentral:
|
||||||
restart: always
|
restart: always
|
||||||
@@ -50,3 +50,20 @@ services:
|
|||||||
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
|
||||||
|
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
|
||||||
4
tests/environment/config/meshcentral/overrides/.gitignore
vendored
Normal file
4
tests/environment/config/meshcentral/overrides/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Ignore everything in this directory
|
||||||
|
*
|
||||||
|
# Except this file
|
||||||
|
!.gitignore
|
||||||
11
tests/environment/config/squid/conf.d/meshctrl.conf
Normal file
11
tests/environment/config/squid/conf.d/meshctrl.conf
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Logs are managed by logrotate on Debian
|
||||||
|
logfile_rotate 0
|
||||||
|
|
||||||
|
acl all src all
|
||||||
|
acl Safe_ports port 8086
|
||||||
|
acl SSS_ports port 8086
|
||||||
|
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
|
||||||
9350
tests/environment/config/squid/squid.conf
Normal file
9350
tests/environment/config/squid/squid.conf
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
|||||||
FROM ghcr.io/ylianst/meshcentral:latest
|
FROM ghcr.io/ylianst/meshcentral:1.1.50
|
||||||
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"]
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"])
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"admin": "3U6zP4iIes5ISH15XxjYLjJcCdw9jU0m", "privileged": "aiIO0zLMGsU7++FYVDNxhlpYlZ1andRB", "unprivileged": "Cz9OMV1wkVd9pXdWi4lkBAAu6TMt43MA"}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
requests
|
requests
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
cffi==1.17.1
|
cffi==1.17.1
|
||||||
cryptography==43.0.3
|
cryptography~=44.0.1
|
||||||
pycparser==2.22
|
pycparser==2.22
|
||||||
websockets==13.1
|
websockets~=15.0.0
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,36 @@ 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 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")
|
||||||
|
|
||||||
|
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_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 +287,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 +298,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 +333,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"
|
||||||
|
|
||||||
|
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 (r[(await privileged_session.user_info())["_id"]]["success"]), "Failed to remove user from devcie group"
|
|
||||||
|
|
||||||
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 +381,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 +397,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 +424,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 +448,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 +465,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 +474,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 +495,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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user