Compare commits

..

9 Commits
0.0.1 ... 0.0.9

Author SHA1 Message Date
Josiah Baldwin
f0e09c0082 Doc fix 2024-12-02 13:41:16 -08:00
Josiah Baldwin
184ce3ef3e Added Session to the __init__ file, and changed docs and test accordingly 2024-12-02 13:39:40 -08:00
Josiah Baldwin
33680dab5d Updated __init__ imports 2024-12-02 13:02:15 -08:00
Josiah Baldwin
05f1bae04d Changed pypi name to libmeshctrl because meshctrl is taken 2024-12-02 12:40:42 -08:00
Josiah Baldwin
b0b89b89e6 Fixed install_requires 2024-12-02 12:20:58 -08:00
Josiah Baldwin
fdc2b11afd Added note that proxy is not yet implemented 2024-12-02 11:59:54 -08:00
Josiah Baldwin
4ed332ca4c Fixed readme link 2024-12-02 11:52:52 -08:00
Josiah Baldwin
5f0f6a0ff9 Changed to RST README only 2024-12-02 11:47:45 -08:00
Josiah Baldwin
c576eae48b Fixed some doc links 2024-12-02 11:33:56 -08:00
12 changed files with 92 additions and 53 deletions

View File

@@ -1,2 +0,0 @@
# meshctrl
Libmeshctrl implementation in python

View File

@@ -29,15 +29,59 @@
| |
=============
meshctrl meshctrl
============= ========
Library for remotely interacting with a
`MeshCentral <https://meshcentral.com/>`__ server instance
Libmeshctrl implementation in python Installation
------------
pip install meshctrl
Usage
-----
This module is implemented as a primarily asynchronous library
(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
initialized before interacting with the server. The preferred way to do
this is to use the async context manager pattern:
.. code:: python
import meshctrl
async with meshctrl.Session(url, **options):
print(await session.list_users())
...
However, if you prefer to instantiate the object yourself, you can
simply use the `initialized <https://pylibmeshctrl.readthedocs.io/en/latest/api/meshctrl.html#meshctrl.session.Session.initialized>`__ property:
.. code:: python
session = meshctrl.Session(url, **options)
await session.initialized.wait()
Note that, in this case, you will be rquired to clean up tho session
using its `close <https://pylibmeshctrl.readthedocs.io/en/latest/api/meshctrl.html#meshctrl.session.Session.close>`__ method.
Session Parameters
------------------
``url``: URL of meshcentral server to connect to. Should start with
either "ws://" or "wss://".
``options``: optional parameters. Described at `Read the
Docs <https://pylibmeshctrl.readthedocs.io/en/latest/api/meshctrl.html#module-meshctrl.session>`__
API
---
API is documented in the `API
Docs <https://pylibmeshctrl.readthedocs.io/en/latest/api/meshctrl.html>`__
This is a library for interacting with a Mesh Central instance programatically. Written in python.
.. _pyscaffold-notes: .. _pyscaffold-notes:

Binary file not shown.

View File

@@ -4,24 +4,19 @@
# https://setuptools.pypa.io/en/latest/references/keywords.html # https://setuptools.pypa.io/en/latest/references/keywords.html
[metadata] [metadata]
name = meshctrl name = libmeshctrl
description = Add a short description here! description = Python package for interacting with a Meshcentral server instance
author = Josiah Baldwin author = Josiah Baldwin
author_email = jbaldwin8889@gmail.com author_email = jbaldwin8889@gmail.com
license = MIT license = MIT
license_files = LICENSE.txt license_files = LICENSE.txt
long_description = file: README.rst long_description = file: README.rst
long_description_content_type = text/x-rst; charset=UTF-8 long_description_content_type = text/x-rst; charset=UTF-8
url = https://github.com/pyscaffold/pyscaffold/ url = https://github.com/HuFlungDu/pylibmeshctrl/
# Add here related links, for example: # Add here related links, for example:
project_urls = project_urls =
Documentation = https://pyscaffold.org/ Documentation = https://pylibmeshctrl.readthedocs.io/
# Source = https://github.com/pyscaffold/pyscaffold/ Source = https://github.com/HuFlungDu/pylibmeshctrl/
# Changelog = https://pyscaffold.org/en/latest/changelog.html
# Tracker = https://github.com/pyscaffold/pyscaffold/issues
# Conda-Forge = https://anaconda.org/conda-forge/pyscaffold
# Download = https://pypi.org/project/PyScaffold/#files
# Twitter = https://twitter.com/PyScaffold
# Change if running only on Windows, Mac or Linux (comma-separated) # Change if running only on Windows, Mac or Linux (comma-separated)
platforms = any platforms = any
@@ -41,14 +36,16 @@ package_dir =
=src =src
# Require a min/specific Python version (comma-separated conditions) # Require a min/specific Python version (comma-separated conditions)
# python_requires = >=3.8 python_requires = >=3.8
# Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0. # Add here dependencies of your project (line-separated), e.g. requests>=2.2,<3.0.
# Version specifiers like >=2.2,<3.0 avoid problems due to API changes in # Version specifiers like >=2.2,<3.0 avoid problems due to API changes in
# new major versions. This works if the required packages follow Semantic Versioning. # new major versions. This works if the required packages follow Semantic Versioning.
# For more information, check out https://semver.org/. # For more information, check out https://semver.org/.
install_requires = install_requires =
importlib-metadata; python_version<"3.8" importlib-metadata
cryptography>=43.0.3
websockets>=13.1
[options.packages.find] [options.packages.find]

View File

@@ -15,10 +15,13 @@ except PackageNotFoundError: # pragma: no cover
finally: finally:
del version, PackageNotFoundError del version, PackageNotFoundError
from . import session from .session import Session
from . import constants from . import constants
from . import shell from . import shell
from . import tunnel from . import tunnel
from . import util from . import util
from . import files from . import files
from . import exceptions from . import exceptions
from . import device
from . import mesh
from . import user_group

View File

@@ -22,9 +22,6 @@ 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

View File

@@ -28,7 +28,7 @@ class Session(object):
domain (str): Domain to connect to domain (str): Domain to connect to
password (str): Password with which to connect. Can also be password generated from token. password (str): Password with which to connect. Can also be password generated from token.
loginkey (str|bytes): Key from already handled login. Overrides username/password. loginkey (str|bytes): Key from already handled login. Overrides username/password.
proxy (str): "url:port" to use for proxy server proxy (str): "url:port" to use for proxy server NOTE: This is currently not implemented due to a limitation of the undersying websocket library. Upvote the issue if you find this important.
token (str): Login token. This appears to be superfluous token (str): Login token. This appears to be superfluous
ignore_ssl (bool): Ignore SSL errors ignore_ssl (bool): Ignore SSL errors

View File

@@ -1 +1 @@
{"admin": "3U6zP4iIes5ISH15XxjYLjJcCdw9jU0m", "privileged": "aiIO0zLMGsU7++FYVDNxhlpYlZ1andRB", "unprivileged": "Cz9OMV1wkVd9pXdWi4lkBAAu6TMt43MA"} {"admin": "vt9BbctCg59vcuxKPh8v2tbDjudjwyeX", "privileged": "BWl1vhSe0j0lBfoAkx1JLXLBOwIWc0st", "unprivileged": "vCuatfTQGq8bL2pxdvrNzF+Dc4xBq+5Z"}

View File

@@ -7,7 +7,7 @@ import io
import random import random
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(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:
@@ -53,7 +53,7 @@ async def test_commands(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_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(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:

View File

@@ -8,14 +8,14 @@ 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:
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_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

View File

@@ -8,8 +8,8 @@ import io
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:
@@ -34,22 +34,22 @@ async def test_admin(env):
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,\ 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:
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 +74,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 +94,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 +107,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))
@@ -266,9 +266,9 @@ async def test_mesh_device(env):
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 +294,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 +310,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 +337,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 +361,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:

View File

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