Source code for toxiccore.client

# -*- coding: utf-8 -*-

# Copyright 2015, 2017-2018 Juca Crispim <juca@poraodojuca.net>

# This file is part of toxicbuild.

# toxicbuild is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# toxicbuild is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with toxicbuild. If not, see <http://www.gnu.org/licenses/>.

import asyncio
from collections import OrderedDict
import json
import ssl
import traceback

from .exceptions import ToxicClientException, BadJsonData
from .socks import read_stream, write_stream
from .utils import LoggerMixin


__doc__ = """This module implements a base client for basic
tcp communication, reading and writing json data.

Usage:
``````

.. code-block:: python

    host = 'somehost.net'
    port = 1234
    async with BaseToxicClient(host, port):
        await client.write({'hello': 'world'})
        json_response = await client.get_response()

"""


[docs]class BaseToxicClient(LoggerMixin): """ Base client for communication with toxicbuild servers. """ READ_STREAM_FN = read_stream WRITE_STREAM_FN = write_stream def __init__(self, host, port, use_ssl=False, validate_cert=True, **ssl_kw): """:para host: The host to connect :param port: The port that the server is listening. :param use_ssl: Indicates if we should use a secure connection. :param validate_cert: Indicates if we should validate the ssl cert used by the server. :param ssl_kw: Named arguments to ``ssl.create_default_context()`` """ self.host = host self.port = port self.use_ssl = use_ssl self.validate_cert = validate_cert self.ssl_kw = ssl_kw self.reader = None self.writer = None self._connected = False
[docs] def is_connected(self): return self._connected
def __enter__(self): if not self.is_connected(): msg = "Use ``async with``" raise ToxicClientException(msg) return self async def __aenter__(self): await self.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): self.disconnect() async def __aexit__(self, exc_type, exc_val, exc_tb): self.__exit__(exc_type, exc_val, exc_tb)
[docs] async def connect(self): """Connects to the server. .. note:: This is called by the asynchronous context manager (aka ``async with``) """ if self.use_ssl: ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, **self.ssl_kw) if not self.validate_cert: ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE kw = {'ssl': ssl_context} else: kw = {} self.reader, self.writer = await asyncio.open_connection( self.host, self.port, **kw) self._connected = True
[docs] def disconnect(self): """Disconnects from the server""" self.log('disconecting...', level='debug') self.writer.close() self._connected = False
[docs] async def write(self, data, timeout=None): """ Writes ``data`` to the server. :param data: Data to be sent to the server. Will be converted to json and enconded using utf-8. :param timeout: Timeout for the write operation. If None there is no timeout. """ data = json.dumps(data) await type(self).WRITE_STREAM_FN(self.writer, data, timeout=timeout)
[docs] async def read(self, timeout=None): """Reads data from the server. Expects a json. param timeout: Timeout for the read operation. If None there is no timeout """ # '{}' is decoded as an empty dict, so in json # context we can consider it as being a False json data = await type(self).READ_STREAM_FN(self.reader, timeout=timeout) data = data.decode() or '{}' try: json_data = json.loads(data, object_pairs_hook=OrderedDict) except Exception: msg = traceback.format_exc() self.log(msg, level='error') raise BadJsonData(data) return json_data
[docs] async def get_response(self, timeout=None): """Reads data from the server and raises and exception in case of error""" response = await self.read(timeout=timeout) if 'code' in response and int(response['code']) != 0: raise ToxicClientException(response['body']['error']) return response
[docs] async def request2server(self, action, body, token, timeout=None): """Performs a request to a toxicbuild server and server returns the response. :param action: The action to perform on the server. :param body: The body for the action. :param token: An authentication token. """ data = {'action': action, 'body': body, 'token': token} await self.write(data) response = await self.get_response(timeout=timeout) return response['body'][action]