forked from Narcissus/pylibmeshctrl
First real commit, everything implemented
This commit is contained in:
@@ -14,3 +14,11 @@ except PackageNotFoundError: # pragma: no cover
|
||||
__version__ = "unknown"
|
||||
finally:
|
||||
del version, PackageNotFoundError
|
||||
|
||||
from . import session
|
||||
from . import constants
|
||||
from . import shell
|
||||
from . import tunnel
|
||||
from . import util
|
||||
from . import files
|
||||
from . import exceptions
|
||||
@@ -41,7 +41,6 @@ class UserRights(enum.IntFlag):
|
||||
class MeshRights(enum.IntFlag):
|
||||
"""
|
||||
Bitwise flags for mesh rights
|
||||
Pulls double duty as rights for a connected device
|
||||
"""
|
||||
#: Give user no rights
|
||||
norights = 0
|
||||
@@ -81,6 +80,40 @@ class MeshRights(enum.IntFlag):
|
||||
#: All rights
|
||||
fullrights = 0xFFFFFFFF
|
||||
|
||||
@document_enum
|
||||
class DeviceRights(enum.IntFlag):
|
||||
"""
|
||||
Bitwise flags for device rights
|
||||
Piggy backs on rights for a mesh, but has differet "all" rights.
|
||||
"""
|
||||
#: Give user no rights
|
||||
norights = 0
|
||||
#: Remote control access
|
||||
remotecontrol = MeshRights.remotecontrol
|
||||
#: Agent console access
|
||||
agentconsole = MeshRights.agentconsole
|
||||
serverfiles = MeshRights.serverfiles
|
||||
#: Wake device from sleep
|
||||
wakedevices = MeshRights.wakedevices
|
||||
#: Add notes to the device/mesh
|
||||
notes = MeshRights.notes
|
||||
#: Only view the desktop; no control
|
||||
desktopviewonly = MeshRights.desktopviewonly
|
||||
#: No terminal access
|
||||
noterminal = MeshRights.noterminal
|
||||
#: No file access
|
||||
nofiles = MeshRights.nofiles
|
||||
#: No AMT access
|
||||
noamt = MeshRights.noamt
|
||||
limiteddesktop = MeshRights.limiteddesktop
|
||||
limitedevents = MeshRights.limitedevents
|
||||
chatnotify = MeshRights.chatnotify
|
||||
uninstall = MeshRights.uninstall
|
||||
#: Allow to send commands to the device
|
||||
remotecommands = MeshRights.remotecommands
|
||||
#: All rights
|
||||
fullrights = remotecontrol|agentconsole|serverfiles|wakedevices|notes|chatnotify|uninstall|remotecommands
|
||||
|
||||
@document_enum
|
||||
class ConsentFlags(enum.IntFlag):
|
||||
none = 0
|
||||
@@ -129,4 +162,130 @@ class Icon(enum.IntEnum):
|
||||
htpc = enum.auto()
|
||||
router = enum.auto()
|
||||
embedded = enum.auto()
|
||||
virtual = enum.auto()
|
||||
virtual = enum.auto()
|
||||
|
||||
@document_enum
|
||||
class AgentType(enum.IntEnum):
|
||||
"""
|
||||
Which type of agent this is. Taken from meshcentral.js obj.meshAgentsArchitectureNumbers
|
||||
"""
|
||||
UNKNOWN = 0
|
||||
CONSOLE_WIN_X86_32 = 1
|
||||
CONSOLE_WIN_X86_64 = 2
|
||||
SERVICE_WIN_X86_32 = 3
|
||||
SERVICE_WIN_X86_64 = 4
|
||||
SERVICE_LINUX_X86_32 = 5
|
||||
SERVICE_LINUX_X86_64 = 6
|
||||
SERVICE_LINUX_MIPS = 7
|
||||
SERVICE_LINUX_XEN_X86_32 = 8
|
||||
SERVICE_LINUX_ARM5 = 9
|
||||
SERVICE_LINUX_ARM_PLUGPC = 10
|
||||
SERVICE_MACOS_X86_32 = 11
|
||||
SERVICE_ANDROID_X86_32 = 12
|
||||
SERVICE_ANDROID_POGOPLUG = 13
|
||||
SERVICE_ANDROID_APK = 14
|
||||
SERVICE_LINUX_POKY_x86_32 = 15
|
||||
SERVICE_MACOS_X86_64 = 16
|
||||
SERVICE_CHROMEOS = 17
|
||||
SERVICE_LINUX_POKY_x86_64 = 18
|
||||
SERVICE_LINUX_X86_32_NOKVM = 19
|
||||
SERVICE_LINUX_X86_64_NOKVM = 20
|
||||
CONSOLE_WIN_MINICORE_X86_32 = 21
|
||||
SERVICE_WIN_MINICORE_X86_32 = 22
|
||||
SERVICE_NODEJS = 23
|
||||
SERVICE_LINUX_ARM_LINARO = 24
|
||||
SERVICE_LINUX_ARM_HARDFLOAT = 25
|
||||
SERVICE_LINUX_ARM64 = 26
|
||||
SERVICE_LINUX_ARM_HARDFLOAT_2 = 27
|
||||
SERVICE_LINUX_MIPS24KC = 28
|
||||
SERVICE_MACOS_ARM64 = 29
|
||||
SERVICE_FREEBSD_X86_64 = 30
|
||||
SERVICE_LINUX_ARM64_2 = 32
|
||||
SERVICE_OPENWRT_X86_64 = 33
|
||||
ASSISTANT_LINUX = 34 # This is labeled as "windows" in meshcentral.js, but its properties indicate it is for linux.
|
||||
SERVICE_LINUX_ARMADA370_HARDFLOAT = 35
|
||||
SERVICE_OPENWRT_X86_64_2 = 36
|
||||
SERVICE_OPENBSD_X86_64 = 37
|
||||
SERVICE_LINUX_MIPSEL24KC = 40
|
||||
SERVICE_LINUX_CORTEX_A53 = 41
|
||||
CONSOLE_WIN_ARM64 = 42
|
||||
SERVICE_WIN_ARM64 = 43
|
||||
SERVICE_WIN_X86_32_UNSIGNED = 10003
|
||||
SERVICE_WIN_X86_64_UNSIGNED = 10004
|
||||
SERVICE_MACOS_UNIVERSAL_64 = 10005
|
||||
ASSISTANT_WINDOWS = 10006
|
||||
COMMAND_WIN_X86_32 = 11000
|
||||
COMMAND_WIN_X86_64 = 11001
|
||||
|
||||
@document_enum
|
||||
class MeshType(enum.IntEnum):
|
||||
"""
|
||||
Which type of Mesh this is.
|
||||
"""
|
||||
#: AMT devices only
|
||||
AMT = 1
|
||||
#: Controllable using an agent
|
||||
AGENT = 2
|
||||
#: Control only local devices; no agent
|
||||
LOCAL = 3
|
||||
|
||||
|
||||
@document_enum
|
||||
class AgentCapabilities(enum.IntFlag):
|
||||
"""
|
||||
Flags of capabilities an agent can have. Taken from meshagent.h MeshCommand_AuthInfo_CapabilitiesMask from meshagent repo
|
||||
"""
|
||||
|
||||
#: Can control the desktop
|
||||
DESKTOP = enum.auto()
|
||||
#: Can use a terminal, or `~meshctrl.shell.Shell` in our case
|
||||
TERMINAL = enum.auto()
|
||||
#: Can use a files tunnel, or `~meshctrl.files.Files` in our case
|
||||
FILES = enum.auto()
|
||||
# ???
|
||||
CONSOLE = enum.auto()
|
||||
#: Agent can use the javascript core. This should be set for any recent agents, older ones might not have it set
|
||||
JAVASCRIPT = enum.auto()
|
||||
#: Device was created in a temporary manner, and will be destroyed once it disconnects
|
||||
TEMPORARY = enum.auto()
|
||||
#: Agent is using the recovery core
|
||||
RECOVERY = enum.auto()
|
||||
#: Reserved for future use
|
||||
RESERVED = enum.auto()
|
||||
#: Agent can handle compressed streams (?)
|
||||
COMPRESSION = enum.auto()
|
||||
|
||||
@document_enum
|
||||
class InteruserScope(enum.StrEnum):
|
||||
"""
|
||||
String constants used to determine the scope of a received :py:class:`~meshctrl.types.InteruserMessage`
|
||||
"""
|
||||
|
||||
#: The message was sent to your username
|
||||
user = enum.auto()
|
||||
|
||||
#: The message was sent to this specific session.
|
||||
session = enum.auto()
|
||||
|
||||
@document_enum
|
||||
class Protocol(enum.IntEnum):
|
||||
"""
|
||||
Protocol to use for a tunnel. There are others, but these are what we implement.
|
||||
"""
|
||||
#: Terminal tunnel protocol
|
||||
TERMINAL = 1
|
||||
#: File explorer tunnel protocol
|
||||
FILES = 5
|
||||
|
||||
@document_enum
|
||||
class FileType(enum.IntEnum):
|
||||
"""
|
||||
Type numbers used for file types on meshcentral agent.
|
||||
"""
|
||||
#: Root drive (Windows)
|
||||
DRIVE = 1
|
||||
#: Directory
|
||||
DIRECTORY = 2
|
||||
#: File
|
||||
FILE = 3
|
||||
|
||||
|
||||
349
src/meshctrl/device.py
Normal file
349
src/meshctrl/device.py
Normal file
@@ -0,0 +1,349 @@
|
||||
from . import constants
|
||||
import datetime
|
||||
|
||||
class Device(object):
|
||||
'''
|
||||
Object to represent a device. This object is a rough wrapper; it is not guarunteed to be up to date with the state on the server, for instance.
|
||||
|
||||
Args:
|
||||
nodeid (str): id of the device on the server
|
||||
session (~meshctrl.session.Session): Parent session used to run commands
|
||||
agent (~meshctrl.types.Agent|dict|None): Information about the agent. Meshcentral returns this data in an unreadable way, so if the dict doesn't match :py:class:`~meshctrl.types.Agent`, we will attempt to convert to our format.
|
||||
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.
|
||||
tags (list[str]|None): tags associated with device.
|
||||
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.
|
||||
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.
|
||||
meshtype (~meshctrl.constants.MeshType|None): Type of mesh this device is connected to. Also accepted as mtype.
|
||||
meshname (str|None): Name of the mesh to which this device is connected. Also accepted as groupname.
|
||||
domain (str|None): Domain on server to which device is connected.
|
||||
host (str): reachable hostname of device. Not meaningful for agent meshes.
|
||||
ip (str): IP from which device connected.
|
||||
connected: (bool): Whether the device is currently connected. Also accepted as conn.
|
||||
powered_on (bool): Whether the device is currently powered on. Also accepted as pwr.
|
||||
os_description (str|None): Description of the underlying OS. Also accepted as osdesc.
|
||||
lastaddr (str|None): IP from which the agent most recently connected. This may be set even if ip is not.
|
||||
lastconnect (datetime.Datetime|int|None): Last time at which the agent was connected to the server
|
||||
links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device,
|
||||
details (dict[str, dict]|None): Extra details about the device. These are not well defined, but are filled by calling :py:meth:`~meshctrl.session.Session.list_devices` with `details=True`.
|
||||
|
||||
Returns:
|
||||
:py:class:`Device`: Object representing a device on the meshcentral server.
|
||||
|
||||
Attributes:
|
||||
nodeid (str): id of the device on the server
|
||||
agent (~meshctrl.types.Agent|dict|None): Information about the agent. Meshcentral returns this data in an unreadable way, so if the dict doesn't match :py:class:`~meshctrl.types.Agent`, we will attempt to convert to our format.
|
||||
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.
|
||||
tags (list[str]): tags associated with device.
|
||||
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
|
||||
mesh (~meshctrl.mesh.Mesh|None): Mesh object under which this device exists. Is None for individual device access.
|
||||
meshtype (~meshctrl.constants.MeshType|None): Type of mesh this device is connected to. Also accepted as mtype.
|
||||
meshname (str|None): Name of the mesh to which this device is connected. Also accepted as groupname.
|
||||
domain (str|None): Domain on server to which device is connected.
|
||||
host (str): reachable hostname of device. Not meaningful for agent meshes.
|
||||
ip (str): IP from which device connected.
|
||||
connected: (bool): Whether the device is currently connected. Also accepted as conn.
|
||||
powered_on (bool): Whether the device is currently powered on. Also accepted as pwr.
|
||||
os_description (str|None): Description of the underlying OS. Also accepted as osdesc.
|
||||
lastaddr (str|None): IP from which the agent most recently connected. This may be set even if ip is not.
|
||||
lastconnect (datetime.Datetime|None): Last time at which the agent was connected to the server
|
||||
links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device
|
||||
details (dict[str, dict]): Extra details about the device. These are not well defined, but are filled by calling :py:meth:`~meshctrl.session.Session.list_devices` with `details=True`.
|
||||
'''
|
||||
def __init__(self, nodeid, session, agent=None,
|
||||
name=None, desc=None, description=None,
|
||||
tags=None,
|
||||
agct=None, created_at=None,
|
||||
rname=None, computer_name=None, icon=constants.Icon.desktop,
|
||||
mesh=None, mtype=None, meshtype=None, groupname=None, meshname=None,
|
||||
domain=None, host=None, ip=None, conn=None, connected=None,
|
||||
pwr=None, powered_on=None,
|
||||
osdesc=None, os_description=None, lastaddr=None, lastconnect=None,
|
||||
links=None, details=None, **kwargs):
|
||||
self.nodeid = nodeid
|
||||
self._session = session
|
||||
if links is None:
|
||||
links = {}
|
||||
self.links = links
|
||||
if ("ver" in agent):
|
||||
agent = {
|
||||
"version": agent["ver"],
|
||||
"id": agent["id"],
|
||||
"capabilities": agent["caps"]
|
||||
}
|
||||
self.agent = agent
|
||||
self.name = name
|
||||
self.computer_name = computer_name if computer_name is not None else rname
|
||||
self.icon = icon
|
||||
self.mesh = mesh
|
||||
self.meshtype = meshtype if meshtype is not None else mtype
|
||||
self.meshname = meshname if meshname is not None else groupname
|
||||
self.domain = domain
|
||||
self.host = host
|
||||
self.ip = ip
|
||||
self.connected = bool(connected if connected is not None else conn)
|
||||
self.powered_on = bool(powered_on if powered_on is not None else pwr)
|
||||
self.description = description if description is not None else desc
|
||||
self.os_description = os_description if os_description is not None else osdesc
|
||||
self.tags = tags if tags 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
|
||||
if not isinstance(created_at, datetime.datetime) and created_at is not None:
|
||||
try:
|
||||
created_at = datetime.datetime.fromtimestamp(created_at)
|
||||
except OSError:
|
||||
# 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.
|
||||
created_at = datetime.datetime.fromtimestamp(created_at/1000.0)
|
||||
|
||||
self.created_at = created_at
|
||||
|
||||
if not isinstance(lastconnect, datetime.datetime) and lastconnect is not None:
|
||||
try:
|
||||
lastconnect = datetime.datetime.fromtimestamp(lastconnect)
|
||||
except OSError:
|
||||
# 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.
|
||||
lastconnect = datetime.datetime.fromtimestamp(lastconnect/1000.0)
|
||||
|
||||
self.lastconnect = lastconnect
|
||||
self.lastaddr = lastaddr
|
||||
|
||||
# In case meshcentral gives us props we don't understand, store them here.
|
||||
self._extra_props = kwargs
|
||||
|
||||
async def add_users(self, userids, rights=None, timeout=None):
|
||||
'''
|
||||
Add a user to an existing node
|
||||
|
||||
Args:
|
||||
userids (str|list[str]): Unique user id(s)
|
||||
rights (~meshctrl.constants.DeviceRights): Bitwise mask for the rights on the given device
|
||||
timeout (int): duration in milliseconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
bool: True on success, raise otherwise
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.add_users_to_device(userids, self.nodeid, domain=self.domain, rights=rights, timeout=timeout)
|
||||
|
||||
async def remove_users(self, userids, timeout=None):
|
||||
'''
|
||||
Remove users from an this node
|
||||
|
||||
Args:
|
||||
userids (str|list[str]): Unique user id(s)
|
||||
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.ServerError`: Error text from server if there is a failure
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.remove_users_from_device(self.nodeid, userids, timeout=timeout)
|
||||
|
||||
async def move_to_device_group(self, meshid, isname=False, timeout=None):
|
||||
'''
|
||||
Move this device another device group
|
||||
|
||||
Args:
|
||||
meshid (str): Unique mesh id
|
||||
isname (bool): treat "meshid" as a name instead of an id
|
||||
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.ServerError`: Error text from server if there is a failure
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.remove_users_from_device(self.nodeid, meshid, isname=isname, timeout=timeout)
|
||||
|
||||
async def info(self, timeout=None):
|
||||
'''
|
||||
Get all info for this device. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences.
|
||||
|
||||
Args:
|
||||
timeout (int): duration in seconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
~meshctrl.device.Device: Object representing the state of the device. This will be a new device, it will not update this device.
|
||||
|
||||
Raises:
|
||||
ValueError: `Invalid device id` if device is not found
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.device_info(self.nodeid, timeout=timeout)
|
||||
|
||||
async def edit(self, name=None, description=None, tags=None, icon=None, consent=None, timeout=None):
|
||||
'''
|
||||
Edit properties of this device
|
||||
|
||||
Args:
|
||||
name (str): New name for device
|
||||
description (str): New description for device
|
||||
tags (str|list[str]]): New tags for device
|
||||
icon (~meshctrl.constants.Icon): New icon for device
|
||||
consent (~meshctrl.constants.ConsentFlags): New consent flags for device
|
||||
timeout (int): duration in seconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
bool: True if successful, raise otherwise
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.edit_device(self.nodeid, name=name, description=description, tags=tags, icon=icon, consent=consent, timeout=timeout)
|
||||
|
||||
async def run_command(self, command, powershell=False, runasuser=False, runasuseronly=False, ignore_output=False, timeout=None):
|
||||
'''
|
||||
Run a command on this device. WARNING: Non namespaced call. Calling this function again before it returns may cause unintended consequences.
|
||||
|
||||
Args:
|
||||
command (str): Command to run
|
||||
powershell (bool): Use powershell to run command. Only available on Windows.
|
||||
runasuser (bool): Attempt to run as a user instead of the root permissions given to the agent. Fall back to root if we cannot.
|
||||
ignore_output (bool): Don't bother trying to get the output. Every device will return an empty string for its result.
|
||||
runasuseronly (bool): Error if we cannot run the command as the logged in user.
|
||||
timeout (int): duration in seconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
~meshctrl.types.RunCommandResponse: Output of command
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
ValueError: `Invalid device id` if device is not found
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return (await self._session.run_command(self.nodeid, command, powershell=False, runasuser=False, runasuseronly=False, ignore_output=False, timeout=None))[self.nodeid]
|
||||
|
||||
async def shell(self):
|
||||
'''
|
||||
Get a terminal shell on this device
|
||||
|
||||
Returns:
|
||||
:py:class:`~meshctrl.shell.Shell`: Newly created :py:class:`~meshctrl.shell.Shell`
|
||||
'''
|
||||
return await self._session.shell(self.nodeid)
|
||||
|
||||
|
||||
async def smart_shell(self, regex):
|
||||
'''
|
||||
Get a smart terminal shell on this device
|
||||
|
||||
Args:
|
||||
regex (regex): Regex to watch for to signify that the shell is ready for new input.
|
||||
|
||||
Returns:
|
||||
:py:class:`~meshctrl.shell.SmartShell`: Newly created :py:class:`~meshctrl.shell.SmartShell`
|
||||
'''
|
||||
return await self._session.smart_shell(self.nodeid, regex)
|
||||
|
||||
|
||||
async def wake(self, timeout=None):
|
||||
'''
|
||||
Wake up this device
|
||||
|
||||
Args:
|
||||
timeout (int): duration in seconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
|
||||
return await self._session.wake_devices(self.nodeid, timeout=timeout)
|
||||
|
||||
async def reset(self, timeout=None):
|
||||
'''
|
||||
Reset device
|
||||
|
||||
Args:
|
||||
nodeids (str|list[str]): Unique ids of nodes which to reset
|
||||
timeout (int): duration in seconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.reset_devices(self.nodeid, timeout=timeout)
|
||||
|
||||
async def sleep(self, timeout=None):
|
||||
'''
|
||||
Sleep device
|
||||
|
||||
Args:
|
||||
timeout (int): duration in seconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.sleep_devices(self.nodeid, timeout=timeout)
|
||||
|
||||
async def power_off(self, timeout=None):
|
||||
''' Power off device
|
||||
|
||||
Args:
|
||||
timeout (int): duration in seconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.power_off_devices(self.nodeid, timeout=timeout)
|
||||
|
||||
@property
|
||||
def short_nodeid(self):
|
||||
'''
|
||||
nodeid without "node/" or the included domain
|
||||
'''
|
||||
return self.nodeid.split("/")[-1]
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
'''
|
||||
Alias to "nodeid" to be consistent accross types.
|
||||
'''
|
||||
return self.nodeid
|
||||
|
||||
def __str__(self):
|
||||
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"tags={self.tags} 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}>"
|
||||
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)}, "\
|
||||
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"connected={repr(self.connected)}, powered_on={repr(self.powered_on)}, os_description={repr(self.os_description)}, links={repr(self.links)}, **{repr(self._extra_props)})"
|
||||
@@ -14,4 +14,23 @@ class SocketError(MeshCtrlError):
|
||||
"""
|
||||
Represents an error in the websocket
|
||||
"""
|
||||
pass
|
||||
|
||||
class FileTransferError(MeshCtrlError):
|
||||
"""
|
||||
Represents a failed file transfer
|
||||
|
||||
Attributes:
|
||||
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):
|
||||
self.stats = stats
|
||||
|
||||
class FileTransferCancelled(FileTransferError):
|
||||
"""
|
||||
Represents a canceled file transfer
|
||||
"""
|
||||
pass
|
||||
@@ -1,4 +1,308 @@
|
||||
from . import tunnel
|
||||
from . import constants
|
||||
from . import exceptions
|
||||
from . import util
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
class Files(tunnel.Tunnel):
|
||||
pass
|
||||
def __init__(self, session, nodeid):
|
||||
super().__init__(session, nodeid, constants.Protocol.FILES)
|
||||
self.recorded = None
|
||||
self._request_id = 0
|
||||
self._request_queue = asyncio.Queue()
|
||||
self._download_finished = asyncio.Event()
|
||||
self._download_finished.set()
|
||||
self._current_request = None
|
||||
self._handle_requests_task = asyncio.create_task(self._handle_requests())
|
||||
self._chunk_size = 65564
|
||||
|
||||
def _get_request_id(self):
|
||||
self._request_id = (self._request_id+1)%(2**32-1)
|
||||
return self._request_id
|
||||
|
||||
async def close(self):
|
||||
self._handle_requests_task.cancel()
|
||||
try:
|
||||
await self._handle_requests_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
await super().close()
|
||||
|
||||
async def _handle_requests(self):
|
||||
try:
|
||||
while True:
|
||||
request = await self._request_queue.get()
|
||||
self._current_request = request
|
||||
self._download_finished = request["finished"]
|
||||
await self._message_queue.put(json.dumps(request["data"]))
|
||||
await request["finished"].wait()
|
||||
self._current_request = None
|
||||
|
||||
except asyncio.CancelledError:
|
||||
while True:
|
||||
try:
|
||||
request = self._request_queue.get_nowait()
|
||||
request["error"] = exceptions.SocketError("Socket Closed")
|
||||
request["errored"].set()
|
||||
request["finished"].set()
|
||||
except asyncio.QueueEmpty:
|
||||
break
|
||||
raise
|
||||
|
||||
|
||||
@util._check_socket
|
||||
async def _send_command(self, data, name, timeout=None):
|
||||
request_id = f"meshctrl_{name}_{self._get_request_id()}"
|
||||
request = {"id": request_id, "data": data, "return": None, "type": name, "finished": asyncio.Event(), "errored":asyncio.Event(), "error": None}
|
||||
await self._request_queue.put(request)
|
||||
|
||||
await asyncio.wait_for(request["finished"].wait(), timeout=timeout)
|
||||
if request["error"] is not None:
|
||||
raise request["error"]
|
||||
return request["return"]
|
||||
|
||||
async def ls(self, directory, timeout=None):
|
||||
"""
|
||||
Return a directory listing from the device
|
||||
|
||||
Args:
|
||||
directory (str): Path to the directory you wish to list
|
||||
|
||||
Returns:
|
||||
list[~meshctrl.types.FilesLSItem]: The directory listing
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.ServerError`: Error from server
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
"""
|
||||
data = await self._send_command({"action": "ls", "path": directory}, "ls", timeout=timeout)
|
||||
return data["dir"]
|
||||
|
||||
async def _listen_for_pass(self, tasks):
|
||||
async for event in self._session.events({"event": {"etype": "node", "action": "agentlog"}}):
|
||||
if not event["event"]["msg"].startswith("Started"):
|
||||
self._current_request["return"] = event["event"]["msg"]
|
||||
self._current_request["finished"].set()
|
||||
tasks[1].cancel()
|
||||
break
|
||||
|
||||
async def _listen_for_error(self, tasks):
|
||||
async for event in self._session.events({"action":"msg", "type":"console"}):
|
||||
self._current_request["error"] = exceptions.ServerError(event["value"])
|
||||
self._current_request["errored"].set()
|
||||
self._current_request["finished"].set()
|
||||
tasks[0].cancel()
|
||||
break
|
||||
|
||||
async def mkdir(self, directory, timeout=None):
|
||||
"""
|
||||
Create a directory on the device
|
||||
|
||||
Args:
|
||||
directory (str): Path of directory to create
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.ServerError`: Error from server
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
|
||||
Returns:
|
||||
bool: True if directory was created
|
||||
"""
|
||||
tasks = []
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
tasks.append(tg.create_task(asyncio.wait_for(self._listen_for_pass(tasks), timeout)))
|
||||
tasks.append(tg.create_task(asyncio.wait_for(self._listen_for_error(tasks), timeout)))
|
||||
tasks.append(tg.create_task(self._send_command({"action": "mkdir", "path": directory}, "mkdir", timeout=timeout)))
|
||||
|
||||
|
||||
|
||||
return tasks[2].result().startswith("Create folder")
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
path (str): Directory from which to delete files
|
||||
files (str|list[str]): File or files to remove from the directory
|
||||
recursive (bool): Whether to delete the files recursively
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.ServerError`: Error from server
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
|
||||
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'.
|
||||
"""
|
||||
if isinstance(files, str):
|
||||
files = [files]
|
||||
tasks = []
|
||||
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
tasks.append(tg.create_task(asyncio.wait_for(self._listen_for_pass(tasks), timeout)))
|
||||
tasks.append(tg.create_task(asyncio.wait_for(self._listen_for_error(tasks), timeout)))
|
||||
tasks.append(tg.create_task(self._send_command({"action": "rm", "delfiles": files, "rec": recursive, "path": path}, "rm", timeout=timeout)))
|
||||
|
||||
|
||||
return tasks[2].result()
|
||||
|
||||
async def rename(self, path, name, new_name, timeout=None):
|
||||
"""
|
||||
Rename a file or folder on the device. This API doesn't error if the file doesn't exist.
|
||||
|
||||
Args:
|
||||
path (str): Directory from which to rename the file
|
||||
name (str): File to rename
|
||||
new_name (str): New name to give the file
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.ServerError`: Error from server
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
|
||||
Returns:
|
||||
str: Info about file renamed. Something along the lines of 'Rename: "/path/to/file" to "newfile"'.
|
||||
"""
|
||||
tasks = []
|
||||
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
tasks.append(tg.create_task(asyncio.wait_for(self._listen_for_pass(tasks), timeout)))
|
||||
tasks.append(tg.create_task(asyncio.wait_for(self._listen_for_error(tasks), timeout)))
|
||||
tasks.append(tg.create_task(self._send_command({"action": "rename", "path": path, "oldname": name, "newname": new_name}, "rename", timeout=timeout)))
|
||||
|
||||
|
||||
return tasks[2].result()
|
||||
|
||||
async def upload(self, source, target, name=None, timeout=None):
|
||||
'''
|
||||
Upload a stream to a device.
|
||||
|
||||
Args:
|
||||
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
|
||||
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.
|
||||
|
||||
Raises:
|
||||
: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
|
||||
|
||||
Returns:
|
||||
dict: {result: bool whether upload succeeded, size: number of bytes uploaded}
|
||||
'''
|
||||
request_id = f"upload_{self._get_request_id()}"
|
||||
data = { "action": 'upload', "reqid": request_id, "path": target, "name": name}
|
||||
request = {"id": request_id, "data": data, "type": "upload", "source": source, "target": target, "name": name, "size": 0, "complete": False, "inflight": 0, "finished": asyncio.Event(), "errored":asyncio.Event(), "error": None}
|
||||
await self._request_queue.put(request)
|
||||
await asyncio.wait_for(request["finished"].wait(), timeout)
|
||||
if request["error"] is not None:
|
||||
raise request["error"]
|
||||
return request["return"]
|
||||
|
||||
async def download(self, source, target, timeout=None):
|
||||
'''
|
||||
Download a file from a device into a writable stream.
|
||||
|
||||
Args:
|
||||
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.
|
||||
|
||||
Raises:
|
||||
: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
|
||||
|
||||
Returns:
|
||||
dict: {result: bool whether download succeeded, size: number of bytes downloaded}
|
||||
'''
|
||||
request_id = f"download_{self._get_request_id()}"
|
||||
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}
|
||||
await self._request_queue.put(request)
|
||||
await asyncio.wait_for(request["finished"].wait(), timeout)
|
||||
if request["error"] is not None:
|
||||
raise request["error"]
|
||||
return request["return"]
|
||||
|
||||
async def _handle_upload(self, data):
|
||||
cmd = None
|
||||
try:
|
||||
cmd = json.loads(data)
|
||||
except:
|
||||
return
|
||||
if cmd["reqid"] == self._current_request["id"]:
|
||||
if cmd["action"] == "uploaddone":
|
||||
self._current_request["return"] = {"result": "success", "size": self._current_request["size"]}
|
||||
self._current_request["finished"].set()
|
||||
elif cmd["action"] == "uploadstart":
|
||||
while True:
|
||||
data = self._current_request["source"].read(self._chunk_size)
|
||||
if len(data) == 0:
|
||||
self._current_request["complete"] = True
|
||||
if self._current_request["inflight"] == 0:
|
||||
await self._message_queue.put(json.dumps({ "action": 'uploaddone', "reqid": self._current_request["id"]}))
|
||||
break
|
||||
else:
|
||||
self._current_request["size"] += len(data)
|
||||
if data[0] == 0 or data[0] == 123:
|
||||
data = b'\0' + data
|
||||
await self._message_queue.put(data)
|
||||
self._current_request["inflight"] += 1
|
||||
await asyncio.sleep(0)
|
||||
elif cmd["action"] == "uploadack":
|
||||
self._current_request["inflight"] -= 1
|
||||
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"]}))
|
||||
elif cmd["action"] == "uploaderror":
|
||||
self._current_request["return"] = {"result": "canceled", "size": self._current_request["size"]}
|
||||
self._current_request["error"] = exceptions.FileTransferError("Errored", self._current_request["return"])
|
||||
self._current_request["errored"].set()
|
||||
self._current_request["finished"].set()
|
||||
|
||||
async def _handle_download(self, data):
|
||||
cmd = None
|
||||
try:
|
||||
cmd = json.loads(data)
|
||||
except:
|
||||
pass
|
||||
if cmd is None:
|
||||
if len(data) > 4:
|
||||
self._current_request["target"].write(data[4:])
|
||||
self._current_request["size"] += len(data)-4
|
||||
if (data[3] & 1) != 0:
|
||||
self._current_request["return"] = {"result": "success", "size": self._current_request["size"]}
|
||||
self._current_request["finished"].set()
|
||||
else:
|
||||
await self._message_queue.put(json.dumps({ "action": 'download', "sub": 'ack', "id": self._current_request["id"] }))
|
||||
else:
|
||||
if cmd["action"] == "download":
|
||||
if cmd["id"] != self._current_request["id"]:
|
||||
return
|
||||
if cmd["sub"] == "start":
|
||||
await self._message_queue.put(json.dumps({ "action": 'download', "sub": 'startack', "id": self._current_request["id"] }))
|
||||
elif cmd["sub"] == "cancel":
|
||||
self._current_request["return"] = {"result": "canceled", "size": self._current_request["size"]}
|
||||
self._current_request["error"] = exceptions.FileTransferCancelled("Cancelled", self._current_request["return"])
|
||||
self._current_request["errored"].set()
|
||||
self._current_request["finished"].set()
|
||||
|
||||
async def _handle_action(self, data):
|
||||
self._current_request["return"] = json.loads(data)
|
||||
self._download_finished.set()
|
||||
|
||||
|
||||
async def _listen_data_task(self, websocket):
|
||||
async for message in websocket:
|
||||
if self.initialized.is_set():
|
||||
if message[0] == 123 and self._current_request is not None and self._current_request["type"] not in ("upload", "download"):
|
||||
await self._handle_action(message)
|
||||
elif self._current_request is not None and self._current_request["type"] == "upload":
|
||||
await self._handle_upload(message)
|
||||
elif self._current_request is not None and self._current_request["type"] == "download":
|
||||
await self._handle_download(message)
|
||||
else:
|
||||
self.recorded = False
|
||||
if message == "cr":
|
||||
self.recorded = True
|
||||
|
||||
await self._message_queue.put(f"{self._protocol}".encode())
|
||||
self.alive = True
|
||||
self.initialized.set()
|
||||
99
src/meshctrl/mesh.py
Normal file
99
src/meshctrl/mesh.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from . import constants
|
||||
import datetime
|
||||
|
||||
class Mesh(object):
|
||||
'''
|
||||
Object to represent a device mesh. This object is a rough wrapper; it is not guarunteed to be up to date with the state on the server, for instance.
|
||||
|
||||
Args:
|
||||
meshid (str): id of the device mesh on the server
|
||||
session (~meshctrl.session.Session): Parent session used to run commands
|
||||
created_at (datetime.Datetime|int): Time at which mesh mas created. Also accepted as creation.
|
||||
name (str|None): Mesh name as it is shown on the meshcentral server
|
||||
description (str|None): Mesh description as it is shown on the meshcentral server. Also accepted as desc.
|
||||
meshtype (~meshctrl.constants.MeshType|None): Type of mesh this device is connected to. Also accepted as mtype.
|
||||
creatorid (str): User id of the user who created the mesh.
|
||||
creatorname (str): Display name of the user who created the mesh.
|
||||
domain (str|None): Domain on server to which device is connected.
|
||||
links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device group
|
||||
|
||||
Returns:
|
||||
:py:class:`Mesh`: Object representing a device group on the meshcentral server.
|
||||
|
||||
Attributes:
|
||||
meshid (str): id of the device mesh on the server
|
||||
created_at (datetime.Datetime): Time at which mesh mas created.
|
||||
name (str|None): Mesh name as it is shown on the meshcentral server
|
||||
description (str|None): Mesh description as it is shown on the meshcentral server
|
||||
meshtype (~meshctrl.constants.MeshType|None): Type of mesh this is.
|
||||
creatorid (str|None): User id of the user who created the mesh.
|
||||
creatorname (str|None): Display name of the user who created the mesh.
|
||||
domain (str|None): Domain on server to which device is connected.
|
||||
links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device group
|
||||
'''
|
||||
def __init__(self, meshid, session, creation=None, created_at=None, name=None,
|
||||
mtype=None, meshtype=None, creatorid=None, desc=None, description=None,
|
||||
domain=None, creatorname=None, links=None, **kwargs):
|
||||
self.meshid = meshid
|
||||
self._session = session
|
||||
if links is None:
|
||||
links = {}
|
||||
self.links = links
|
||||
self.name = name
|
||||
self.meshtype = meshtype if meshtype is not None else mtype
|
||||
self.description = description if description is not None else desc
|
||||
created_at = created_at if created_at is not None else creation
|
||||
if not isinstance(created_at, datetime.datetime) and created_at is not None:
|
||||
try:
|
||||
created_at = datetime.datetime.fromtimestamp(created_at)
|
||||
except OSError:
|
||||
# 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.
|
||||
created_at = datetime.datetime.fromtimestamp(created_at/1000.0)
|
||||
self.created_at = created_at
|
||||
self.creatorid = creatorid
|
||||
self.creatorname = creatorname
|
||||
self.domain = domain
|
||||
# In case meshcentral gives us props we don't understand, store them here.
|
||||
self._extra_props = kwargs
|
||||
|
||||
@property
|
||||
def short_meshid(self):
|
||||
'''
|
||||
meshid without "mesh/" or the included domain
|
||||
'''
|
||||
return self.meshid.split("/")[-1]
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
'''
|
||||
Alias to "meshid" to be consistent accross types.
|
||||
'''
|
||||
return self.meshid
|
||||
|
||||
async def add_users(self, userids, rights=0, timeout=None):
|
||||
'''
|
||||
Add a user to an existing mesh
|
||||
|
||||
Args:
|
||||
userids (str|list[str]): Unique user id(s)
|
||||
rights (~meshctrl.constants.MeshRights): Bitwise mask for the rights to give to the users
|
||||
timeout (int): duration in milliseconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
dict[str, ~meshctrl.types.AddUsersToDeviceGroupResponse]: Object showing which were added correctly and which were not, along with their result messages. str is userid to map response.
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.add_users_to_device_group(userids, self.meshid, isname=False, domain=self.domain, rights=rights, timeout=timeout)
|
||||
|
||||
def __str__(self):
|
||||
return f"<Mesh: meshid={self.meshid} name={self.name} description={self.description} created_at={self.created_at} "\
|
||||
f"meshtype={self.meshtype} domain={self.domain} "\
|
||||
f"created_at={self.created_at} creatorid={self.creatorid} creatorname={self.creatorname} links={self.links}>"
|
||||
def __repr__(self):
|
||||
return f"Mesh(meshid={repr(self.meshid)}, session={repr(self._session)}, name={repr(self.name)}, description={repr(self.description)}, created_at={repr(self.created_at)}, "\
|
||||
f"meshtype={repr(self.meshtype)}, domain={repr(self.domain)}, "\
|
||||
f"created_at={repr(self.created_at)}, creatorid={repr(self.creatorid)}, creatorname={repr(self.creatorname)}, links={repr(self.links)}, **{repr(self._extra_props)})"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,191 @@
|
||||
from . import tunnel
|
||||
from . import constants
|
||||
from . import util
|
||||
import io
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
import asyncio
|
||||
|
||||
class _BufferPipe(io.BufferedRandom):
|
||||
'''Class to approximate an os pipe in software. Feel like I'm an idiot and just can't find out how to do this with beffered readers, but here we are...'''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
_buffer = io.BytesIO()
|
||||
super().__init__(_buffer, *args, **kwargs)
|
||||
self._read_pointer = 0
|
||||
self._write_pointer = 0
|
||||
|
||||
def peek(self, *args, **kwargs):
|
||||
self.seek(self._read_pointer)
|
||||
d = super().peek(*args, **kwargs)
|
||||
self._read_pointer = self.tell()
|
||||
return d
|
||||
|
||||
def read(self, *args, **kwargs):
|
||||
self.seek(self._read_pointer)
|
||||
d = super().read(*args, **kwargs)
|
||||
self._read_pointer = self.tell()
|
||||
return d
|
||||
|
||||
def read1(self, *args, **kwargs):
|
||||
self.seek(self._read_pointer)
|
||||
d = super().read1(*args, **kwargs)
|
||||
self._read_pointer = self.tell()
|
||||
return d
|
||||
|
||||
def write(self, *args, **kwargs):
|
||||
self.seek(self._write_pointer)
|
||||
d = super().write(*args, **kwargs)
|
||||
self._write_pointer = self.tell()
|
||||
return d
|
||||
|
||||
class Shell(tunnel.Tunnel):
|
||||
pass
|
||||
def __init__(self, session, nodeid):
|
||||
super().__init__(session, nodeid, constants.Protocol.TERMINAL)
|
||||
self.recorded = None
|
||||
self._buffer = _BufferPipe()
|
||||
|
||||
class SmartShell(tunnel.Tunnel):
|
||||
pass
|
||||
|
||||
@util._check_socket
|
||||
async def write(self, command):
|
||||
"""
|
||||
Write to the shell
|
||||
|
||||
Args:
|
||||
command (str): Command to send
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
return await self._message_queue.put(command.encode("utf-8"))
|
||||
|
||||
@util._check_socket
|
||||
async def read(self, length=None, block=True, timeout=None):
|
||||
"""
|
||||
Read data from the shell
|
||||
|
||||
Args:
|
||||
length (int): Number of bytes to read. None == read until closed or timeout occurs.
|
||||
block (bool): block until n bytes are available or timeout occurs. If not, read at most until no data is returned. This may return an indeterminate amount of data.
|
||||
timeout (int): Milliseconds to wait for data. None == read until `length` bytes are read, or shell is closed.
|
||||
|
||||
Returns:
|
||||
str: Data read. In the case of timeout, this will return all data read up to the timeout
|
||||
"""
|
||||
start = time.time()
|
||||
ret = []
|
||||
read_bytes = 0
|
||||
while True:
|
||||
d = self._buffer.read1(length-read_bytes if length is not None else -1)
|
||||
# print(f"read: {d}")
|
||||
read_bytes += len(d)
|
||||
ret.append(d)
|
||||
if length is not None and read_bytes >= length:
|
||||
break
|
||||
if timeout is not None and time.time() - start >= timeout:
|
||||
break
|
||||
if not block and not len(d):
|
||||
break
|
||||
await asyncio.sleep(0)
|
||||
return b"".join(ret).decode("utf-8")
|
||||
|
||||
@util._check_socket
|
||||
async def expect(self, regex, timeout=None):
|
||||
"""
|
||||
Read data from the shell until `regex` is seen
|
||||
|
||||
Args:
|
||||
regex (str|re.Pattern): Regex to check for match
|
||||
timeout (int): Milliseconds to wait for data. None == read until `length` bytes are read, or shell is closed.
|
||||
|
||||
Returns:
|
||||
str: Data read.
|
||||
|
||||
Raises:
|
||||
asyncio.TimeoutError: Regex not matched within timeout
|
||||
"""
|
||||
start = time.time()
|
||||
read_bytes = 0
|
||||
if not isinstance(regex, re.Pattern):
|
||||
regex = re.compile(regex)
|
||||
while True:
|
||||
d = self._buffer.peek().decode("utf-8")
|
||||
match = regex.search(d)
|
||||
if match is not None:
|
||||
read_bytes = match.span()[1]
|
||||
break
|
||||
if timeout is not None and time.time() - start >= timeout:
|
||||
raise asyncio.TimeoutError
|
||||
await asyncio.sleep(0)
|
||||
return await self.read(read_bytes)
|
||||
|
||||
async def _listen_data_task(self, websocket):
|
||||
async for message in websocket:
|
||||
if self.initialized.is_set():
|
||||
if message.startswith(b'{"ctrlChannel":"102938","type":"'):
|
||||
try:
|
||||
ctrl_cmd = json.loads(message)
|
||||
# Skip control commands, like ping/pong
|
||||
if ctrl_cmd.get("type", None) is not None:
|
||||
return
|
||||
except:
|
||||
pass
|
||||
self._buffer.write(message)
|
||||
else:
|
||||
self.recorded = False
|
||||
if message == "cr":
|
||||
self.recorded = True
|
||||
|
||||
# Seems like we could use self.write here, but it won't have been initialized yet, so the socket check will fail.
|
||||
await self._message_queue.put(f"{self._protocol}".encode())
|
||||
self.alive = True
|
||||
self.initialized.set()
|
||||
|
||||
|
||||
|
||||
class SmartShell(object):
|
||||
def __init__(self, shell, regex):
|
||||
self._shell = shell
|
||||
self._regex = regex
|
||||
self._compiled_regex = re.compile(self._regex)
|
||||
self._init_task = asyncio.create_task(self._init())
|
||||
|
||||
async def _init(self):
|
||||
# This comes twice. Test this for sanity. Seems meshcentral does some aliases when it logs in. Could be wrong on windows.
|
||||
await self._shell.expect(self._regex)
|
||||
await self._shell.expect(self._regex)
|
||||
|
||||
|
||||
@util._check_socket
|
||||
async def send_command(self, command, timeout=None):
|
||||
if not command.endswith("\n"):
|
||||
command += "\n"
|
||||
await self._shell.write(command)
|
||||
data = await self._shell.expect(self._regex, timeout=timeout)
|
||||
print(repr(data))
|
||||
return data[:self._compiled_regex.search(data).span()[0]]
|
||||
|
||||
@property
|
||||
def alive(self):
|
||||
return self._shell.alive
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
return self._shell.closed
|
||||
|
||||
@property
|
||||
def initialized(self):
|
||||
return self._shell.initialized
|
||||
|
||||
async def close(self):
|
||||
await self._init_task
|
||||
return await self._shell.close()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self._init_task
|
||||
await self._shell.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
return await self._shell.__aexit__(*args)
|
||||
|
||||
@@ -1,2 +1,107 @@
|
||||
import websockets
|
||||
import websockets.datastructures
|
||||
import websockets.asyncio
|
||||
import websockets.asyncio.client
|
||||
import asyncio
|
||||
import ssl
|
||||
from . import exceptions
|
||||
from . import util
|
||||
from . import constants
|
||||
|
||||
class Tunnel(object):
|
||||
pass
|
||||
def __init__(self, session, node_id, protocol):
|
||||
self._session = session
|
||||
self.node_id = node_id
|
||||
self._protocol = protocol
|
||||
self._tunnel_id = None
|
||||
self.url = None
|
||||
self._socket_open = asyncio.Event()
|
||||
self._main_loop_error = None
|
||||
self.initialized = asyncio.Event()
|
||||
self.alive = False
|
||||
self.closed = asyncio.Event()
|
||||
self._main_loop_task = asyncio.create_task(self._main_loop())
|
||||
|
||||
self._message_queue = asyncio.Queue()
|
||||
self._send_task = None
|
||||
self._listen_task = None
|
||||
|
||||
async def close(self):
|
||||
self._main_loop_task.cancel()
|
||||
try:
|
||||
await self._main_loop_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
# If we take more than 10 seconds to establish a tunnel, something is up.
|
||||
await asyncio.wait_for(self.initialized.wait(), 10)
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_t, exc_v, exc_tb):
|
||||
await self.close()
|
||||
|
||||
async def _main_loop(self):
|
||||
try:
|
||||
authcookie = await self._session._send_command_no_response_id({ "action":"authcookie" })
|
||||
|
||||
options = {}
|
||||
if self._session._ignore_ssl:
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
options = { "ssl": ssl_context }
|
||||
|
||||
# Setup the HTTP proxy if needed
|
||||
# if (self._session._proxy != None):
|
||||
# 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)
|
||||
|
||||
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")
|
||||
|
||||
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._socket_open.clear()
|
||||
self.closed.set()
|
||||
self.initialized.set()
|
||||
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"])
|
||||
|
||||
# headers = websockets.datastructures.Headers()
|
||||
|
||||
# if (self._password):
|
||||
# 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._socket_open.set()
|
||||
try:
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
tg.create_task(self._listen_data_task(websocket))
|
||||
tg.create_task(self._send_data_task(websocket))
|
||||
except* websockets.ConnectionClosed as e:
|
||||
self._socket_open.clear()
|
||||
if not self.auto_reconnect:
|
||||
self.alive = False
|
||||
raise
|
||||
except* Exception as eg:
|
||||
self.alive = False
|
||||
self._socket_open.clear()
|
||||
self._main_loop_error = eg
|
||||
self.closed.set()
|
||||
self.initialized.set()
|
||||
|
||||
async def _send_data_task(self, websocket):
|
||||
while True:
|
||||
message = await self._message_queue.get()
|
||||
await websocket.send(message)
|
||||
|
||||
async def _listen_data_task(self, websocket):
|
||||
raise NotImplementedError("Listen data not implemented")
|
||||
191
src/meshctrl/types.py
Normal file
191
src/meshctrl/types.py
Normal file
@@ -0,0 +1,191 @@
|
||||
'''
|
||||
This module attempts to define the various structures of objects returned by meshcentral calls.
|
||||
These types are expected, but not guarunteed, as the meshcentral API is not well defined.
|
||||
'''
|
||||
|
||||
import typing
|
||||
import collections.abc
|
||||
from . import constants
|
||||
|
||||
class UserLink(typing.TypedDict):
|
||||
'''
|
||||
Represents a link to a user account
|
||||
'''
|
||||
|
||||
rights: constants.MeshRights|constants.DeviceRights
|
||||
'''User's rights on the containing object'''
|
||||
|
||||
name: str
|
||||
'''Username of the user this link references'''
|
||||
|
||||
class ListUsersResponseItem(typing.TypedDict):
|
||||
'''
|
||||
Item contained in response from :py:func:`~meshctrl.session.Session.list_users`
|
||||
'''
|
||||
|
||||
_id: str
|
||||
'''User's ID'''
|
||||
|
||||
name: str
|
||||
'''User's username'''
|
||||
|
||||
creation: int
|
||||
'''When the user was created. UTC timestamp'''
|
||||
|
||||
links: dict[str, list[UserLink]]
|
||||
'''Set of links connected to this user. str is the ID of the thing referenced, the type of which is hinted at by the beginning of the string. These describe numerous things, the enumeration of which is beyond this documentation'''
|
||||
|
||||
class AddUsersToDeviceGroupResponse(typing.TypedDict):
|
||||
'''
|
||||
Response proffered when a user is added to a device group
|
||||
'''
|
||||
|
||||
success: bool
|
||||
'''Whether the user was added successfully'''
|
||||
|
||||
message: str
|
||||
'''Any interesting information about adding the user. "Added user {username}" on success, otherwise defined by server'''
|
||||
|
||||
class RemoveUsersFormDeviceGroupResponse(AddUsersToDeviceGroupResponse):
|
||||
'''
|
||||
Response proffered when a user is removed from a device group
|
||||
'''
|
||||
pass
|
||||
|
||||
class DeviceGroup(typing.TypedDict):
|
||||
'''
|
||||
Device group information
|
||||
'''
|
||||
|
||||
meshid: str
|
||||
'''ID of the created device group'''
|
||||
|
||||
links: dict[str, list[UserLink]]
|
||||
'''Users of this device group, and their rights'''
|
||||
|
||||
class Agent(typing.TypedDict):
|
||||
'''
|
||||
Information about an agent running on a machine
|
||||
'''
|
||||
|
||||
version: int
|
||||
'''Agent version'''
|
||||
|
||||
id: constants.AgentType
|
||||
'''Type of agent this is'''
|
||||
|
||||
capabilities: constants.AgentCapabilities
|
||||
'''Capabilities of this agent. This can change over time based on the connection state of the agent'''
|
||||
|
||||
class AddDeviceGroupResponse(typing.TypedDict):
|
||||
'''
|
||||
Response proffered when device group is added
|
||||
'''
|
||||
|
||||
meshid: str
|
||||
'''ID of the created device group'''
|
||||
|
||||
links: dict[str, list[UserLink]]
|
||||
'''Users of this device group, and their rights'''
|
||||
|
||||
class RunCommandResponse(typing.TypedDict):
|
||||
'''
|
||||
Response item from run_command execution
|
||||
'''
|
||||
|
||||
complete: bool
|
||||
'''Whether the command completed correctly'''
|
||||
|
||||
command: str
|
||||
'''The command which was run'''
|
||||
|
||||
result: str
|
||||
'''Output of command'''
|
||||
|
||||
class AddUsersToUserGroupResponse(typing.TypedDict):
|
||||
'''
|
||||
Response item from add_users_to_user_group execution
|
||||
'''
|
||||
|
||||
success: bool
|
||||
'''Whether the user was added successfully'''
|
||||
|
||||
message: str
|
||||
'''Message from server if user was not added correctly'''
|
||||
|
||||
class LoginToken(typing.TypedDict):
|
||||
'''
|
||||
Login token created on the server
|
||||
'''
|
||||
|
||||
name: str
|
||||
'''Name of the token'''
|
||||
|
||||
tokenUser: str
|
||||
'''Username substitute of the token'''
|
||||
|
||||
tokenPass: str
|
||||
'''Password substitute for the token'''
|
||||
|
||||
created: int
|
||||
'''UTC timestamp representing when the token was created. In milliseconds'''
|
||||
|
||||
expire: int
|
||||
'''UTC timestamp representing when the token expires. In milliseconds. 0 means no expirery.'''
|
||||
|
||||
class RetrievedLoginToken(typing.TypedDict):
|
||||
'''
|
||||
Login token created on the server and retrieved. This will not include the password.
|
||||
'''
|
||||
|
||||
name: str
|
||||
'''Name of the token'''
|
||||
|
||||
tokenUser: str
|
||||
'''Username substitute of the token'''
|
||||
|
||||
created: int
|
||||
'''UTC timestamp representing when the token was created. In milliseconds'''
|
||||
|
||||
expire: int
|
||||
'''UTC timestamp representing when the token expires. In milliseconds. 0 means no expirery.'''
|
||||
|
||||
class InteruserMessage(typing.TypedDict):
|
||||
'''
|
||||
Message received from another user through interuser messaging
|
||||
'''
|
||||
|
||||
action: "interuser"
|
||||
'''Identifier of event type'''
|
||||
|
||||
sessionid: str
|
||||
'''Session from which the message originated'''
|
||||
|
||||
data: str
|
||||
'''Any data the user sent'''
|
||||
|
||||
scope: constants.InteruserScope
|
||||
'''"user" if the message was sent to your username, "session" if it was sent to this specific session.'''
|
||||
|
||||
class FilesLSItem(typing.TypedDict):
|
||||
'''
|
||||
Dict representing a file or directory on a mesh device, as returned from meshcentral server
|
||||
'''
|
||||
|
||||
n: str
|
||||
'''Name of file or dir'''
|
||||
|
||||
d: str
|
||||
'''UTC timestamp for when the file/directory was edited'''
|
||||
|
||||
t: constants.FileType
|
||||
'''Type of file'''
|
||||
|
||||
dt: typing.Optional[str]
|
||||
'''Drive type, in the case t == :py:const:`~meshctrl.constants.FileType.DRIVE`'''
|
||||
|
||||
s: typing.Optional[int]
|
||||
'''Size of the file if t == :py:const:`~meshctrl.constants.FileType.FILE`'''
|
||||
|
||||
f: typing.Optional[int]
|
||||
'''Free bytes on the drive, if t == :py:const:`~meshctrl.constants.FileType.DRIVE`'''
|
||||
77
src/meshctrl/user_group.py
Normal file
77
src/meshctrl/user_group.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from . import constants
|
||||
import datetime
|
||||
|
||||
class UserGroup(object):
|
||||
'''
|
||||
Object to represent a user group. This object is a rough wrapper; it is not guarunteed to be up to date with the state on the server, for instance.
|
||||
|
||||
Args:
|
||||
ugrpid (str): id of the user group on the server
|
||||
session (~meshctrl.session.Session): Parent session used to run commands
|
||||
name (str|None): Mesh name as it is shown on the meshcentral server
|
||||
description (str|None): Mesh description as it is shown on the meshcentral server. Also accepted as desc.
|
||||
domain (str|None): Domain on server to which device is connected.
|
||||
links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device group
|
||||
|
||||
Returns:
|
||||
:py:class:`Mesh`: Object representing a device group on the meshcentral server.
|
||||
|
||||
Attributes:
|
||||
ugrpid (str): id of the device mesh on the server
|
||||
name (str|None): Mesh name as it is shown on the meshcentral server
|
||||
description (str|None): Mesh description as it is shown on the meshcentral server
|
||||
domain (str|None): Domain on server to which device is connected.
|
||||
links (dict[str, ~meshctrl.types.UserLink]|None): Collection of links for the device group
|
||||
'''
|
||||
def __init__(self, ugrpid, session, name=None,
|
||||
desc=None, description=None,
|
||||
domain=None, links=None, **kwargs):
|
||||
self.ugrpid = ugrpid
|
||||
self._session = session
|
||||
if links is None:
|
||||
links = {}
|
||||
self.links = links
|
||||
self.name = name
|
||||
self.description = description if description is not None else desc
|
||||
self.domain = domain
|
||||
# In case meshcentral gives us props we don't understand, store them here.
|
||||
self._extra_props = kwargs
|
||||
|
||||
@property
|
||||
def short_ugrpid(self):
|
||||
'''
|
||||
ugrpid without "ugrp/" or the included domain
|
||||
'''
|
||||
return self.ugrpid.split("/")[-1]
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
'''
|
||||
Alias to "ugrpid" to be consistent accross types.
|
||||
'''
|
||||
return self.ugrpid
|
||||
|
||||
async def add_users(self, userids, timeout=None):
|
||||
'''
|
||||
Add a user to an existing mesh
|
||||
|
||||
Args:
|
||||
userids (str|list[str]): Unique user id(s)
|
||||
rights (~meshctrl.constants.MeshRights): Bitwise mask for the rights to give to the users
|
||||
timeout (int): duration in milliseconds to wait for a response before throwing an error
|
||||
|
||||
Returns:
|
||||
dict[str, ~meshctrl.types.AddUsersToDeviceGroupResponse]: Object showing which were added correctly and which were not, along with their result messages. str is userid to map response.
|
||||
|
||||
Raises:
|
||||
:py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure
|
||||
asyncio.TimeoutError: Command timed out
|
||||
'''
|
||||
return await self._session.add_users_to_user_group(userids, self.groupid, isname=False, domain=self.domain, timeout=timeout)
|
||||
|
||||
def __str__(self):
|
||||
return f"<UserGroup: ugrpid={self.ugrpid} name={self.name} description={self.description} "\
|
||||
f"domain={self.domain} links={self.links}>"
|
||||
def __repr__(self):
|
||||
return f"UserGroup(ugrpid={repr(self.ugrpid)}, session={repr(self._session)}, name={repr(self.name)}, description={repr(self.description)}, "\
|
||||
f"domain={repr(self.domain)}, links={repr(self.links)}, **{repr(self._extra_props)})"
|
||||
@@ -3,13 +3,36 @@ import time
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
import json
|
||||
import base64
|
||||
import asyncio
|
||||
import collections
|
||||
import re
|
||||
import websockets
|
||||
import ssl
|
||||
import functools
|
||||
from . import exceptions
|
||||
|
||||
def _encode_cookie(o, key):
|
||||
o["time"] = int(time.time()); # Add the cookie creation time
|
||||
iv = secrets.token_bytes(12)
|
||||
key = AESGCM(key)
|
||||
crypted = key.encrypt(iv, json.dumps(o), None)
|
||||
return base64.b64encode(crypted).replace("+", '@').replace("/", '$');
|
||||
return base64.b64encode(crypted).replace(b"+", b'@').replace(b"/", b'$').decode("utf-8")
|
||||
|
||||
def _check_amt_password(p):
|
||||
return (len(p) > 7) and\
|
||||
(re.search(r"\d",p) is not None) and\
|
||||
(re.search(r"[a-z]",p) is not None) and\
|
||||
(re.search(r"[A-Z]",p) is not None) and\
|
||||
(re.search(r"\W",p) is not None)
|
||||
|
||||
def _get_random_amt_password():
|
||||
p = ""
|
||||
while not _check_amt_password(p):
|
||||
p = b"@".join(base64.b64encode(secrets.token_bytes(9)).split(b'/')).decode("utf-8")
|
||||
return p
|
||||
|
||||
def _get_random_hex(count):
|
||||
return secrets.token_bytes(count).hex();
|
||||
|
||||
class Eventer(object):
|
||||
"""
|
||||
@@ -56,7 +79,7 @@ class Eventer(object):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def emit(self, event, data):
|
||||
async def emit(self, event, data):
|
||||
"""
|
||||
Emit `event` with `data`. All subscribed functions will be called (order is nonsensical).
|
||||
|
||||
@@ -65,11 +88,68 @@ class Eventer(object):
|
||||
data (object): Data to pass to all the bound functions
|
||||
"""
|
||||
for f in self._onces.get(event, []):
|
||||
f(data)
|
||||
await f(data)
|
||||
try:
|
||||
del self._onces[event]
|
||||
except KeyError:
|
||||
pass
|
||||
for f in self._ons.get(event, []):
|
||||
f(data)
|
||||
|
||||
await f(data)
|
||||
|
||||
def compare_dict(dict1, dict2):
|
||||
try:
|
||||
if dict1 == dict2:
|
||||
return True
|
||||
for key, val in dict1.items():
|
||||
if key not in dict2:
|
||||
return False
|
||||
|
||||
if type(val) is dict:
|
||||
if not compare_dict(val, dict2[key]):
|
||||
return False
|
||||
elif type(val) is set:
|
||||
for v in val:
|
||||
for v2 in dict2[key]:
|
||||
try:
|
||||
if compare_dict(v, v2):
|
||||
break
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
elif isinstance(val, collections.abc.Iterable):
|
||||
# We don't want strings to match other iterables, so check that
|
||||
if isinstance(val, str) or isinstance(dict2[key], str):
|
||||
if not isinstance(val, type(dict2[key])):
|
||||
return False
|
||||
try:
|
||||
if (len(val) != len(dict2[key])):
|
||||
return False
|
||||
for i, v in enumerate(val):
|
||||
if not compare_dict(v, dict2[key][i]):
|
||||
return False
|
||||
except Exception as e:
|
||||
return False
|
||||
elif (dict2[key] != val):
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _check_socket(f):
|
||||
@functools.wraps(f)
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
await self.initialized.wait()
|
||||
if not self.alive and self._main_loop_error is not None:
|
||||
raise self._main_loop_error
|
||||
elif not self.alive:
|
||||
raise exceptions.SocketError("Socket Closed")
|
||||
return await f(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
def _process_websocket_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.
|
||||
if isinstance(exc, (ssl.SSLCertVerificationError, TimeoutError)):
|
||||
return exc
|
||||
return tmp
|
||||
|
||||
Reference in New Issue
Block a user