#!/usr/bin/env python3
# The MIT License (MIT)
#
# Copyright (c) 2013-2018 <see AUTHORS.txt>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
:mod:`ts3.query`
================
This module contains a high-level API for the TeamSpeak 3 *Server Query* and
*Client Query plugin*.
.. versionchanged:: 2.0.0
The :class:`TS3Connection` class has been renamed to
:class:`TS3ServerConnection`.
.. versionadded:: 2.0.0
The :class:`TS3ClientConnection` class has been added.
"""
# std
import re
import time
import socket
import telnetlib
import logging
import warnings
from urllib.parse import urlparse
# third party
import paramiko
# local
from .common import TS3Error
from .response import TS3Response, TS3QueryResponse, TS3Event
from .query_builder import TS3QueryBuilder
try:
TimeoutError
except NameError:
TimeoutError = OSError
__all__ = [
"TS3InvalidCommandError",
"TS3QueryError",
"TS3TimeoutError",
"TS3TransportError",
"TS3BaseConnection",
"TS3ServerConnection",
"TS3ClientConnection"]
LOG = logging.getLogger(__name__)
[docs]class TS3InvalidCommandError(TS3Error, ValueError):
"""
Raised if a :class:`TS3QueryBuilder` is constructed with an unknown
command.
:seealso: :attr:`TS3BaseConnection.COMMAND_SET`
"""
def __init__(self, cmd, valid_cmds):
#: The unknown command.
self.cmd = cmd
#: A set with all allowed (known) commands.
self.valid_cmds = valid_cmds
return None
def __str__(self):
tmp = "The command '{}' is unknown.".format(self.cmd)
return tmp
[docs]class TS3QueryError(TS3Error):
"""
Raised, if the error code of the response was not 0.
"""
def __init__(self, resp):
#: The :class:`TS3Response` instance with the response data.
self.resp = resp
return None
def __str__(self):
tmp = "error id {}: {}".format(
self.resp.error["id"], self.resp.error["msg"])
return tmp
[docs]class TS3TimeoutError(TS3Error, TimeoutError):
"""
Raised, if a response or event could not be received due to a *timeout*.
"""
def __str__(self):
tmp = "Could not receive data from the server within the timeout."
return tmp
[docs]class TS3TransportError(TS3Error):
"""
Raised if something goes wrong on the transport level, e.g. a connection
cannot be established or has already been closed.
:seealso: :class:`TS3Transport`
"""
def running_timeout(timeout):
"""Helper to enforce a 'global' timeout::
timeout = running_timeout(timeout)
while True:
line = conn.recv(timeout())
:raises TS3TimeoutError:
"""
start = time.time()
def remaining():
"""Returns the remaining time until *timeout* seconds have passed."""
if timeout is None:
return None
dt = time.time() - start
if dt > timeout:
raise TS3TimeoutError()
return timeout - dt
return remaining
class TS3Transport(object):
"""
The TS3 server supports the *telnet* and *ssh* protocols. This class defines
an abstract interface which is used by the :class:`TS3BaseConnection` to
perform requests.
TS3Transport instances are used for **exactly one connection**.
.. note::
The transport adapter is only used internally and not part of the
public API.
"""
def connect(self, host, port, timeout, **kargs):
"""
Connect to the TS3 query service at the specificed address (host, port).
:raises TS3TimeoutError:
:raises TS3TransportError:
"""
raise NotImplementedError()
def close(self):
"""Closes the connection, never fails."""
raise NotImplementedError()
def read_line(self, timeout):
"""
Blocks until a line has been received (delimted by ``b\n\r``) or the
timeout expired.
:raises TS3TimeoutError:
:raises TS3TransportError:
"""
raise NotImplementedError()
def send_line(self, data):
"""
Send a line of data to the TS3 query service. The delimiter is added
automatic.
:raises TS3TransportError:
"""
raise NotImplementedError()
class TS3TelnetTransport(TS3Transport):
"""An adapter for the telnet protocol using :class:`telnetlib.Telnet`."""
def fileno(self):
return self._conn.fileno()
def connect(self, host, port, timeout, **kargs):
try:
self._conn = telnetlib.Telnet(host, port, timeout)
except OSError as err:
raise TS3TransportError() from err
except TimeoutError as err:
raise TS3TimeoutError() from err
return None
def close(self):
self._conn.close()
return None
def read_line(self, timeout):
try:
return self._conn.read_until(b"\n\r", timeout=timeout)
except (EOFError, OSError) as err:
raise TS3TransportError() from err
def send_line(self, data):
try:
return self._conn.write(data + b"\n\r")
except OSError as err:
raise TS3TransportError() from err
class TS3SSHTransport(TS3Transport):
"""An adapter for the SSH protocol using :class:`paramiko.SSHClient`."""
def fileno(self):
return self._channel.fileno()
def connect(self, host, port, timeout, **kargs):
client = paramiko.SSHClient()
# Load the host key and warn if not provided.
if "host_key" in kargs:
client.load_host_keys(kargs["host_key"])
else:
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
warnings.warn(
"You should provide a 'host_key' to improve security.", Warning
)
client.connect(
host, port=port,
username=kargs["username"], password=kargs["password"]
)
self._client = client
self._channel = client.invoke_shell("raw")
self._rbuffer = b""
return None
def close(self):
self._client.close()
return None
def read_line(self, timeout):
timeout = running_timeout(timeout)
delimiter = b"\n\r"
# Wait until the delimiter can be found in the line buffer.
while True:
eol = self._rbuffer.find(delimiter)
if eol != -1:
break
self._channel.settimeout(timeout())
try:
self._rbuffer += self._channel.recv(4096)
except socket.timeout as err:
raise TS3TimeoutError() from err
# include the delimiter
eol += len(delimiter)
line, self._rbuffer = self._rbuffer[:eol], self._rbuffer[eol:]
return line
def send_line(self, data):
self._channel.send(data + b"\n\r")
return None
[docs]class TS3BaseConnection(object):
"""
The TS3 query client.
This class provides only the methods to **handle** the connection to a
TeamSpeak 3 query service. For a more convenient interface, use the
:class:`TS3ServerConnection` or :class:`TS3ClientConnection` class.
Note, that this class supports the *with* statement::
with TS3BaseConnection("ssh://serveradmin:Z0YxRb7u@localhost:10022") as ts3conn:
ts3conn.exec_("use", sid=1)
# You can also use an equal try-finally construct.
ts3conn = TS3BaseConnection()
try:
ts3conn.open_uri("telnet://serveradmin:Z0YxRb7u@localhost:10011")
ts3conn.exec_("use", sid=1)
finally:
ts3conn.close()
.. warning::
* This class is **not thread safe**.
* Do **not reuse** already connected instances.
.. versionchanged:: 2.0.0
* The *send()* method has been removed, use :meth:`exec_`,
:meth:`query` instead.
* SSH support
* The :meth:`open_uri` method.
"""
#: The length of the greeting. This is the number of lines returned by
#: the query service after successful connection.
#:
#: For example, the TS3 Server Query returns these lines upon connection::
#:
#: b'TS3\n\r'
#: b'Welcome to the [...] on a specific command.\n\r'
GREETING_LENGTH = None
#: A set with all known commands.
COMMAND_SET = set()
def __init__(self, uri=None, tp_args=None):
"""
If *host* and *port* are provided, the connection will be established
before the constructor returns.
.. seealso:: :meth:`open`
"""
self._transport = None
# A buffer for the lines in a query response.
self._resp_buffer = None
# The number of queries for which we have not received a response yet.
self._num_pending_queries = 0
# The undelivered events. These events are returned, the next time
# *wait_for_event()* is called.
self._event_queue = list()
#: The hostname of the query service to which this client is connected.
self._host = None
if uri is not None:
self.open_uri(uri, tp_args=tp_args)
return None
# *Simple* get and set methods
# ------------------------------------------------
[docs] def is_connected(self):
"""
:return:
True, if the client is currently connected.
:rtype:
bool
"""
return self._transport is not None
@property
def host(self):
"""The hostname of the host of the query service."""
return self._host
# Networking
# ------------------------------------------------
[docs] def open(self, host, port, timeout=None, protocol="telnet", tp_args=None):
"""
Connect to the TS3 query service.
.. code-block:: python
# Connect using telnet.
ts3conn.open("localhost", 10011)
# Connect using ssh.
ts3conn.open("localhost", 10022, protocol="ssh", tp_args={
"username": "serveradmin", "password": "123456"
})
:arg str host:
The hostname
:arg int port:
The listening port of the service.
:arg int timeout:
If not *None*, an exception is raised if the connection cannot be
established within *timeout* seconds.
:arg str protocol:
The protocol to be used. The TS3 server supports *ssh* and *telnet*,
while the client only supports *telnet*.
:arg dict tp_args:
A dictionary with parameters that are passed to the
:meth:`~TS3Transport.connect` method of the used transport. The
SSH protocol for example requires a *username* and *password*.
:raises TS3TransportError:
If the client is already connected or the connection cannot be
established.
:raises TS3TimeoutError:
If the connection cannot be established within the specified
*timeout*.
:seealso: :meth:`open_uri`
"""
if self.is_connected():
raise TS3TransportError("Already connected.")
# Choose the transport adapter.
if protocol == "telnet":
tp = TS3TelnetTransport()
elif protocol == "ssh":
tp = TS3SSHTransport()
else:
raise ValueError("The protocol must be 'ssh' or 'telnet'.")
# Conenct to the query service.
tp_args = tp_args or dict()
tp.connect(host, port, timeout, **tp_args)
# Skip the greeting.
for i in range(self.GREETING_LENGTH):
tp.read_line(timeout=timeout)
self._transport = tp
self._num_pending_queries = 0
self._resp_buffer = list()
self._event_queue = list()
self._host = host
LOG.info("Created connection to {}:{}.".format(host, port))
return self
[docs] def open_uri(self, uri, timeout=None, tp_args=None):
"""
The same as :meth:`open`, but the host, port, username, password, ...
are encoded compact in a URI.
.. code-block:: python
>>> ts3conn.open_uri("telnet://my.server.com:10011")
>>> ts3conn.open_uri("ssh://serveradmin@123456@my.server.com:10022")
"""
p = urlparse(uri)
host = p.hostname
port = p.port
protocol = p.scheme
tp_args = tp_args or dict()
tp_args["username"] = p.username
tp_args["password"] = p.password
return self.open(host, port, timeout, protocol, tp_args)
[docs] def close(self, timeout=None):
"""
Sends the ``quit`` command and closes the telnet connection.
"""
if self.is_connected():
try:
self._transport.close()
finally:
self._transport = None
self._num_pending_queries = 0
self._resp_buffer = list()
self._event_queue = list()
self._host = None
LOG.debug("Disconnected client.")
return None
[docs] def fileno(self):
"""
:return:
The fileno() of the socket object used internally.
:rtype:
int
"""
return self._transport.fileno()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, tb):
self.close()
return None
def __del__(self):
self.close()
return None
# Receiving
# -------------------------
def _recv(self, timeout=None):
"""
Blocks, until a message (response or event) has been received.
If an event is received it is appended to the :attr:`_event_queue` and
returned.
If a query response is received, it is only returned (but not cachd).
:arg float timeout:
The maximum time in seconds waited for a response a event.
:type timeout:
None or float
:rtype: TS3Event or TS3QueryResponse
:returns:
A TS3Event or TS3QueryResponse
:raises TS3TimeoutError:
:raises TS3RecvError:
"""
timeout = running_timeout(timeout)
while True:
try:
data = self._transport.read_line(timeout=timeout())
# Catch socket and telnet errors
except TS3TransportError as err:
self.close()
raise
# Handle the receives message.
else:
if not data:
raise TS3TimeoutError()
elif data.startswith(b"notify"):
event = TS3Event(data)
self._event_queue.append(event)
return event
elif data.startswith(b"error"):
self._resp_buffer.append(data)
data = b"".join(self._resp_buffer)
self._resp_buffer = list()
resp = TS3QueryResponse(data)
self._num_pending_queries -= 1
return resp
else:
self._resp_buffer.append(data)
return None
[docs] def wait_for_event(self, timeout=None):
"""
Blocks until an event is received or the *timeout* exceeds. The next
received event is returned.
A simple event loop looks like this:
.. code-block:: python3
ts3conn.query("servernotifyregister", event="server").exec()
while True:
ts3conn.send_keepalive()
try:
event = ts3conn.wait_for_event(timeout=540)
except TS3TimeoutError:
pass
else:
# Handle the received event here ...
:arg timeout:
The maximum number of seconds waited for the next event.
:type timeout:
None or float
:rtype: TS3Event
:returns:
The next received ts3 event.
:raises TS3TimeoutError:
:raises TS3RecvError:
"""
timeout = running_timeout(timeout)
while not self._event_queue:
self._recv(timeout=timeout())
return self._event_queue.pop(0) if self._event_queue else None
def _wait_for_resp(self, timeout=None):
"""
Waits for the response to the last issued query.
:arg timeout:
The maximum number of seconds waited for the query response.
:type timeout:
None or int
:raises TS3TimeoutError:
:raises TS3ResponseRecvError:
:raises TS3QueryError:
"""
assert self._num_pending_queries
timeout = running_timeout(timeout)
while True:
resp = self._recv(timeout=timeout())
if isinstance(resp, TS3QueryResponse):
break
if resp.error["id"] != "0":
raise TS3QueryError(resp)
return resp
# Sending
# -------------------------
[docs] def send_keepalive(self):
"""
Sends an empty query to the query service in order to prevent automatic
disconnect. Make sure to call it at least once in 5 minutes.
"""
self._transport.send_line(b" ")
return None
[docs] def exec_(self, cmd, *options, **params):
"""
Sends a command to the TS3 server and returns the response. Check out the :meth:`query`
method if you want to make use of pipelining and more control.
.. code-block:: python
# use sid=1
ts3conn.exec_("use", sid=1)
# clientlist -uid -away -groups
resp = ts3conn.exec_("clientlist", "uid", "away", "groups")
:arg str cmd:
A TS3 command
:arg options:
The options of a command without a leading minus,
e.g. ``'uid'``, ``'away'``.
:arg params:
Some parameters (key, value pairs) which modify the
command, e.g. ``sid=1``.
:rtype: TS3QueryResponse
:returns:
A object which contains all information about the response.
:seealso: :meth:`wait_for_resp`
:versionadded: 2.0.0
"""
q = self.query(cmd, *options, **params)
return q.fetch()
[docs] def query(self, cmd, *options, **params):
"""
.. note::
The :meth:`query` method is great if you want to **fetch** data
from the server or want to **pipeline** parameters on the same
command.
If you are only interested in getting the command executed, then
you are probably better off using :meth:`exec_`.
Returns a new :class:`~ts3.query_builder.TS3QueryBuilder` object with the
first pipe being initialised with the *options* and *params*::
# serverlist
q = ts3conn.query("serverlist")
# clientlist -uid -away -groups
q = ts3conn.query("clientlist", "uid", "away", "groups")
# clientdbfind pattern=ScP
q = ts3conn.query("clientdbfind", pattern="ScP")
# clientdbfind pattern=FPMPSC6MXqXq751dX7BKV0JniSo= -uid
q = ts3conn.query("clientdbfind", "uid", pattern="FPMPSC6MXqXq751dX7BKV0JniSo")
# clientkick reasonid=5 reasonmsg=Go\saway! clid=1|clid=2|clid=3
q = ts3conn.query("clientkick", reasonid=5, reasonmsg="Go away!")\\
.pipe(clid=1).pipe(clid=2).pipe(clid=3)
# channelmove cid=16 cpid=1 order=0
q = ts3conn.query("channelmove", cid=16, cpid=1, order=0)
# sendtextmessage targetmode=2 target=12 msg=Hello\sWorld!
q = ts3conn.query("sendtextmessage", targetmode=2, target=12, msg="Hello World!")
Queries are **executed** once the
:meth:`~ts3.query_builder.TS3QueryBuilder.fetch`,
:meth:`~ts3.query_builder.TS3QueryBuilder.first`
or :meth:`~ts3.query_builder.TS3QueryBuilder.all` is invoked::
# Returns a TS3Response object.
resp = q.fetch()
# Returns the first item in the response or *None*.
resp = q.first()
# Returns a list with all items in the response rather
# than a TS3Response object.
resp = q.all()
:arg options:
All initial options in the first pipe.
:arg params:
All initial parameters (key value pairs) in the first pipe.
:rtype: TS3QueryBuilder
:returns:
A query builder initialised with the *options* and *params*.
:versionadded: 2.0.0
"""
if cmd not in self.COMMAND_SET:
raise TS3InvalidCommandError(cmd, self.COMMAND_SET)
return TS3QueryBuilder(ts3conn=self, cmd=cmd).pipe(*options, **params)
[docs] def exec_query(self, query, timeout=None):
"""
Sends the *query* to the server, waits and returns for the response.
:arg TS3QueryBuilder query:
The query which should be executed.
:rtype: TS3QueryResponse
:returns:
A object which contains all information about the response.
:seealso: :meth:`wait_for_resp`
:versionadded: 2.0.0
"""
q = query.compile()
LOG.debug("Sending query: '%s'.", q)
q = q.encode()
self._transport.send_line(q)
# To identify the response when we receive it.
self._num_pending_queries += 1
return self._wait_for_resp(timeout=timeout)
[docs]class TS3ServerConnection(TS3BaseConnection):
"""
Use this class to connect to a **TS3 Server**::
with TS3ServerConnection("localhost") as tsconn:
ts3conn.exec_("login", client_login_name="serveradmin", client_login_password="MyStupidPassword")
ts3conn.exec_("use")
ts3conn.exec_("clientkick", clid=1)
resp = ts3conn.query("serverlist").all()
"""
#: The typical TS3 Server greeting::
#:
#: b'TS3\n\r'
#: b'Welcome to the [...] on a specific command.\n\r'
GREETING_LENGTH = 2
#: All server query commands as returned by the *help* command,
#: excluding *quit*. Use :meth:`close` instead.
COMMAND_SET = frozenset([
"help",
"login",
"logout",
"version",
"hostinfo",
"instanceinfo",
"instanceedit",
"bindinglist",
"use",
"serverlist",
"serveridgetbyport",
"serverdelete",
"servercreate",
"serverstart",
"serverstop",
"serverprocessstop",
"serverinfo",
"serverrequestconnectioninfo",
"servertemppasswordadd",
"servertemppassworddel",
"servertemppasswordlist",
"serveredit",
"servergrouplist",
"servergroupadd",
"servergroupdel",
"servergroupcopy",
"servergrouprename",
"servergrouppermlist",
"servergroupaddperm",
"servergroupdelperm",
"servergroupaddclient",
"servergroupdelclient",
"servergroupclientlist",
"servergroupsbyclientid",
"servergroupautoaddperm",
"servergroupautodelperm",
"serversnapshotcreate",
"serversnapshotdeploy",
"servernotifyregister",
"servernotifyunregister",
"sendtextmessage",
"logview",
"logadd",
"gm",
"channellist",
"channelinfo",
"channelfind",
"channelmove",
"channelcreate",
"channeldelete",
"channeledit",
"channelgrouplist",
"channelgroupadd",
"channelgroupdel",
"channelgroupcopy",
"channelgrouprename",
"channelgroupaddperm",
"channelgrouppermlist",
"channelgroupdelperm",
"channelgroupclientlist",
"setclientchannelgroup",
"channelpermlist",
"channeladdperm",
"channeldelperm",
"clientlist",
"clientinfo",
"clientfind",
"clientedit",
"clientdblist",
"clientdbinfo",
"clientdbfind",
"clientdbedit",
"clientdbdelete",
"clientgetids",
"clientgetdbidfromuid",
"clientgetnamefromuid",
"clientgetnamefromdbid",
"clientsetserverquerylogin",
"clientupdate",
"clientmove",
"clientkick",
"clientpoke",
"clientpermlist",
"clientaddperm",
"clientdelperm",
"channelclientpermlist",
"channelclientaddperm",
"channelclientdelperm",
"permissionlist",
"permidgetbyname",
"permoverview",
"permget",
"permfind",
"permreset",
"privilegekeylist",
"privilegekeyadd",
"privilegekeydelete",
"privilegekeyuse",
"messagelist",
"messageadd",
"messagedel",
"messageget",
"messageupdateflag",
"complainlist",
"complainadd",
"complaindelall",
"complaindel",
"banclient",
"banlist",
"banadd",
"bandel",
"bandelall",
"ftinitupload",
"ftinitdownload",
"ftlist",
"ftgetfilelist",
"ftgetfileinfo",
"ftstop",
"ftdeletefile",
"ftcreatedir",
"ftrenamefile",
"customsearch",
"custominfo",
"whoami"
])
[docs] def open(self, host, port, timeout=None, protocol="telnet", tp_args=None):
super().open(host, port, timeout, protocol, tp_args)
# Try to log in. Only SSH requires authentication during the connection
# handshake.
username = tp_args.get("username")
password = tp_args.get("password")
if username and password:
self.exec_(
"login", client_login_name=username,
client_login_password=password
)
LOG.info("Logged in as '%s'.", username)
return None
[docs]class TS3ClientConnection(TS3BaseConnection):
"""
Use this class if you want to connect to a **TS3 Client**::
with TS3ClientConnection("localhost") as tsconn:
ts3conn.exec_("auth", apikey="AAAA-BBBB-CCCC-DDDD-EEEE")
ts3conn.exec_("use")
"""
#: The typical TS3 Server greeting::
#:
#: b'TS3 Client\n\r'
#: b'Welcome to the TeamSpeak 3 ClientQuery interface [...].\n\r'
#: b'Use the "auth" command to authenticate yourself. [...].\n\r'
#: b'selected schandlerid=1\n\r'
GREETING_LENGTH = 4
#: All client query commands as returned by the *help* command,
#: excluding *quit*. Use :meth:`close` instead.
COMMAND_SET = frozenset([
"help",
"quit",
"use",
"auth",
"banadd",
"banclient",
"bandelall",
"bandel",
"banlist",
"channeladdperm",
"channelclientaddperm",
"channelclientdelperm",
"channelclientlist",
"channelclientpermlist",
"channelconnectinfo",
"channelcreate",
"channeldelete",
"channeldelperm",
"channeledit",
"channelgroupadd",
"channelgroupaddperm",
"channelgroupclientlist",
"channelgroupdel",
"channelgroupdelperm",
"channelgrouplist",
"channelgrouppermlist",
"channellist",
"channelmove",
"channelpermlist",
"channelvariable",
"clientaddperm",
"clientdbdelete",
"clientdbedit",
"clientdblist",
"clientdelperm",
"clientgetdbidfromuid",
"clientgetids",
"clientgetnamefromdbid",
"clientgetnamefromuid",
"clientgetuidfromclid",
"clientkick",
"clientlist",
"clientmove",
"clientmute",
"clientunmute",
"clientnotifyregister",
"clientnotifyunregister",
"clientpermlist",
"clientpoke",
"clientupdate",
"clientvariable",
"complainadd",
"complaindelall",
"complaindel",
"complainlist",
"currentschandlerid",
"ftcreatedir",
"ftdeletefile",
"ftgetfileinfo",
"ftgetfilelist",
"ftinitdownload",
"ftinitupload",
"ftlist",
"ftrenamefile",
"ftstop",
"hashpassword",
"messageadd",
"messagedel",
"messageget",
"messagelist",
"messageupdateflag",
"permoverview",
"sendtextmessage",
"serverconnectinfo",
"serverconnectionhandlerlist",
"servergroupaddclient",
"servergroupadd",
"servergroupaddperm",
"servergroupclientlist",
"servergroupdelclient",
"servergroupdel",
"servergroupdelperm",
"servergrouplist",
"servergrouppermlist",
"servergroupsbyclientid",
"servervariable",
"setclientchannelgroup",
"tokenadd",
"tokendelete",
"tokenlist",
"tokenuse",
"verifychannelpassword",
"verifyserverpassword",
"whoami"
])