# -*- coding: utf-8 -*-
# wasp_general/network/beacon/messenger.py
#
# Copyright (C) 2016 the wasp-general authors and contributors
# <see AUTHORS file>
#
# This file is part of wasp-general.
#
# Wasp-general is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Wasp-general 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with wasp-general. If not, see <http://www.gnu.org/licenses/>.
# TODO: Merge with wasp_general.network.messenger
# noinspection PyUnresolvedReferences
from wasp_general.version import __author__, __version__, __credits__, __license__, __copyright__, __email__
# noinspection PyUnresolvedReferences
from wasp_general.version import __status__
import re
from abc import ABCMeta, abstractmethod
from wasp_general.verify import verify_type, verify_value
from wasp_general.config import WConfig
from wasp_general.network.primitives import WIPV4SocketInfo, WIPPort
[docs]class WBeaconMessengerBase(metaclass=ABCMeta):
""" This is interface for classes, that implement communication (messaging) logic for beacons
see also: :class:`.WNetworkBeacon`
"""
message_maxsize = 512
[docs] @abstractmethod
@verify_type(beacon_config=WConfig)
def request(self, beacon_config):
""" Generate client request for beacon. It is calling from client side.
:param beacon_config: client beacon configuration.
:return: bytes
"""
raise NotImplementedError('This method is abstract')
[docs] @abstractmethod
@verify_type(beacon_config=WConfig, request=bytes, client_address=WIPV4SocketInfo)
def has_response(self, beacon_config, request, client_address):
""" Whether this messenger has response or it must skip the request. Return True if there is a response,
otherwise - False. If this method returns False, then calling a :meth:`.WBeaconMessengerBase.response`
method treats as error.
:param beacon_config:
:param request:
:param client_address:
:return: bool
"""
raise NotImplementedError('This method is abstract')
[docs] @abstractmethod
@verify_type(beacon_config=WConfig, request=bytes, client_address=WIPV4SocketInfo)
def response(self, beacon_config, request, client_address):
""" Generate server response for clients request. Obviously, it is calling from server side
:param beacon_config: server beacon configuration
:param request: client request message
:param client_address: client address
:return: bytes
"""
raise NotImplementedError('This method is abstract')
[docs] @abstractmethod
@verify_type(beacon_config=WConfig, request=bytes, client_address=WIPV4SocketInfo)
def response_address(self, beacon_config, request, client_address):
""" Return client address where server must send response. It is Possible, that address where server
must send response is different then the origin address. In that case, client address
is encoded in request message.
:param beacon_config: server configuration
:param request: client request message
:param client_address: original client address
:return: WIPV4SocketInfo
"""
raise NotImplementedError('This method is abstract')
[docs] @abstractmethod
@verify_type(beacon_config=WConfig, response=bytes, server_address=WIPV4SocketInfo)
def valid_response(self, beacon_config, response, server_address):
""" Return True if server response isn't junk.
:param beacon_config: client beacon configuration
:param response: server response
:param server_address: original server address
:return: bool
"""
raise NotImplementedError('This method is abstract')
[docs]class WBeaconMessenger(WBeaconMessengerBase):
""" Simple. Demo/debug messenger. Server side just return original hello message to the sender.
"""
__beacon_hello_msg__ = b'HELLO'
""" Client request
"""
[docs] @verify_type('paranoid', beacon_config=WConfig)
def request(self, beacon_config):
""" :meth:`.WBeaconMessengerBase.request` method implementation.
Sends :attr:`.WBeaconMessenger.__beacon_hello_msg__` to a server
"""
return self.__beacon_hello_msg__
[docs] @verify_type('paranoid', beacon_config=WConfig, client_address=WIPV4SocketInfo)
@verify_type(request=bytes)
def has_response(self, beacon_config, request, client_address):
""" :meth:`.WBeaconMessengerBase.has_response` method implementation. This class has a response only if
the request wasn't empty
"""
return True if len(request) > 0 else False
[docs] @verify_type('paranoid', beacon_config=WConfig, client_address=WIPV4SocketInfo)
@verify_type(request=bytes)
def response(self, beacon_config, request, client_address):
""" :meth:`.WBeaconMessengerBase.response` method implementation.
In response sends the same data as server has got
"""
return request
[docs] @verify_type('paranoid', beacon_config=WConfig, request=bytes)
@verify_type(client_address=WIPV4SocketInfo)
def response_address(self, beacon_config, request, client_address):
""" :meth:`.WBeaconMessengerBase.request` method implementation.
Return the same address, that server detects (assume that clients address is correct)
"""
return client_address
[docs] @verify_type('paranoid', beacon_config=WConfig, response=bytes, server_address=WIPV4SocketInfo)
@verify_type(response=bytes)
def valid_response(self, beacon_config, response, server_address):
""" :meth:`.WBeaconMessengerBase.valid_response` method implementation. Response is valid if it has
anything.
"""
return True if len(response) > 0 else False
[docs]class WBeaconGouverneurMessenger(WBeaconMessengerBase):
""" Basic and real messenger implementation. Request and response are generated the same way, but have different
meaning. Messages are generated the following way:
[Message header]<:[Address<:TCP/UDP port>]>
"Message header" is the first part of the message and it must be exactly the same for client or server.
If server has got message with header that doesn't match its own header, such messages will be omitted.
Next parts are "Address" and "TCP/UDP port". These parts are separated
by the :attr:`.WBeaconGouverneurMessenger.__message_splitter__` separator. "TCP/UDP port" can't be specified
without an "Address" An "Address" field can be an IP address or a domain name.
For the client "Address" and "UDP Port" are treated as address where server must send the response.
But for the server, they are treated as the address, that the server publish. For both type of messages
"Address" and "TCP/UDP port" are generated from a configuration. Options are located
in 'wasp-general::network::beacon' section. Configuration option "public_address" is used as "Address"
value and option "public_port" is used as "UDP Port"
"""
__message_splitter__ = b':'
""" Delimiter, that is used for header, IP address and port separation.
"""
@verify_type(hello_message=bytes, invert_hello=bool)
@verify_value(hello_message=lambda x: len(x.decode('ascii')) >= 0)
@verify_value(hello_message=lambda x: WBeaconGouverneurMessenger.__message_splitter__ not in x)
def __init__(self, hello_message, invert_hello=False):
""" Construct new messenger
:param hello_message: Message header
:param invert_hello: this flag defines whether response will have the original header \
('hello_message' value) or it will have reversed value. For example, when this flag is set to True \
and 'hello_message' is b'sample', then response will have b'elpmas' header.
"""
WBeaconMessengerBase.__init__(self)
self.__gouverneur_message = hello_message
self.__invert_hello = invert_hello
[docs] def invert_hello(self):
""" Return whether this messenger was constructed with 'invert_hello' or not.
:return: bool
"""
return self.__invert_hello
[docs] @verify_type(invert_hello=bool)
def hello_message(self, invert_hello=False):
""" Return message header.
:param invert_hello: whether to return the original header (in case of False value) or reversed \
one (in case of True value).
:return: bytes
"""
if invert_hello is False:
return self.__gouverneur_message
hello_message = []
for i in range(len(self.__gouverneur_message) - 1, -1, -1):
hello_message.append(self.__gouverneur_message[i])
return bytes(hello_message)
@verify_type('paranoid', beacon_config=WConfig, invert_hello=bool)
def _message(self, beacon_config, invert_hello=False):
""" Generate request/response message.
:param beacon_config: server or client configuration. Client configuration is used for request and \
server configuration for response
:param invert_hello: return message with reverse header when this argument is set to True.
:return: bytes
"""
message = self.hello_message(invert_hello=invert_hello) + self._message_address_generate(beacon_config)
return message
@verify_type(beacon_config=WConfig)
def _message_address_generate(self, beacon_config):
""" Generate address for request/response message.
:param beacon_config: server or client configuration. Client configuration is used for request and \
server configuration for response
:return: bytes
"""
address = None
if beacon_config['wasp-general::network::beacon']['public_address'] != '':
address = str(WIPV4SocketInfo.parse_address(
beacon_config['wasp-general::network::beacon']['public_address']
)).encode('ascii')
if address is not None:
address = WBeaconGouverneurMessenger.__message_splitter__ + address
if beacon_config['wasp-general::network::beacon']['public_port'] != '':
port = beacon_config.getint('wasp-general::network::beacon', 'public_port')
address += WBeaconGouverneurMessenger.__message_splitter__ + str(port).encode('ascii')
return address if address is not None else b''
@verify_type(message=bytes, invert_hello=bool)
def _message_address_parse(self, message, invert_hello=False):
""" Read address from beacon message. If no address is specified then "nullable" WIPV4SocketInfo returns
:param message: message to parse
:param invert_hello: defines whether message header is the original one or reversed.
:return: WIPV4SocketInfo
"""
message_header = self.hello_message(invert_hello=invert_hello)
if message[:len(message_header)] != message_header:
raise ValueError('Invalid message header')
message = message[len(message_header):]
message_parts = message.split(WBeaconGouverneurMessenger.__message_splitter__)
address = None
port = None
if len(message_parts) > 3:
raise ValueError('Invalid message. Too many separators')
elif len(message_parts) == 3:
address = WIPV4SocketInfo.parse_address(message_parts[1].decode('ascii'))
port = WIPPort(int(message_parts[2]))
elif len(message_parts) == 2 and len(message_parts[1]) > 0:
address = WIPV4SocketInfo.parse_address(message_parts[1].decode('ascii'))
return WIPV4SocketInfo(address, port)
[docs] @verify_type('paranoid', beacon_config=WConfig)
def request(self, beacon_config):
""" :meth:`.WBeaconMessengerBase.request` method implementation.
see :class:`.WBeaconGouverneurMessenger`
"""
return self._message(beacon_config)
[docs] @verify_type('paranoid', beacon_config=WConfig, request=bytes, client_address=WIPV4SocketInfo)
def has_response(self, beacon_config, request, client_address):
""" :meth:`.WBeaconMessengerBase.has_response` method implementation. This method compares request
header with internal one.
"""
try:
self._message_address_parse(request, invert_hello=self.__invert_hello)
return True
except ValueError:
pass
return False
[docs] @verify_type('paranoid', beacon_config=WConfig, request=bytes, client_address=WIPV4SocketInfo)
def response(self, beacon_config, request, client_address):
""" :meth:`.WBeaconMessengerBase.request` method implementation.
see :class:`.WBeaconGouverneurMessenger`
"""
return self._message(beacon_config, invert_hello=self.__invert_hello)
[docs] @verify_type('paranoid', beacon_config=WConfig, request=bytes)
@verify_type(client_address=WIPV4SocketInfo)
def response_address(self, beacon_config, request, client_address):
""" :meth:`.WBeaconMessengerBase.request` method implementation.
see :class:`.WBeaconGouverneurMessenger`
"""
si = self._message_address_parse(request, invert_hello=self.__invert_hello)
address = si.address()
port = si.port()
return WIPV4SocketInfo(
address if address is not None else client_address.address(),
port if port is not None else client_address.port()
)
[docs] @verify_type('paranoid', beacon_config=WConfig, response=bytes, server_address=WIPV4SocketInfo)
def valid_response(self, beacon_config, response, server_address):
""" :meth:`.WBeaconMessengerBase.valid_response` method implementation. Response when it has correct
header. Response header must be reversed if this messenger was constructed with 'invert_hello' flag.
"""
try:
self._message_address_parse(response, invert_hello=self.__invert_hello)
return True
except ValueError:
pass
return False
[docs]class WHostgroupBeaconMessenger(WBeaconGouverneurMessenger):
""" This messenger is based on :class:`.WBeaconGouverneurMessenger` class. This messenger extends
:class:`.WBeaconGouverneurMessenger` functionality by working with host group names. Also, this class doubles
maximum message size defined in :class:`.WBeaconMessengerBase`. In cases where no host group name are
specified this class works as its basic class :class:`.WBeaconGouverneurMessenger`.
Messages format is updated from this:
'[Message header]<:[Address<:TCP/UDP port>]>'
to this:
'[Message header]<:[Address<:TCP/UDP port>]><#<[Hostgroup name]>,<[Hostgroup name]...>'>
'Message header', 'Address' and 'TCP/UDP port' work the same way as they specified in
:class:`.WBeaconGouverneurMessenger` class. At the end of the original message new optional separator is
appended. This separator is defined at :attr:`.WHostgroupBeaconMessenger.__message_groups_splitter__`.
After this separator host group names can be defined. Host group names can contain latin letters and numbers
only. Host group names are separated by :attr:`.WHostgroupBeaconMessenger.__group_splitter__`
If this messenger is constructed with at least one host group name, then it won'not response to requests,
that do not have at least one host group name, that this messenger was constructed with.
"""
message_maxsize = 1024
""" Doubled message size for list of names
"""
__message_groups_splitter__ = b'#'
""" Bytes that separate original message from extended one
"""
__group_splitter__ = b','
""" Bytes that separate different names
"""
re_hostgroup_name = re.compile('^[a-zA-Z0-9]+$')
""" Regular expression for host group name validation
"""
@verify_type('paranoid', hello_message=bytes, invert_hello=bool)
@verify_value('paranoid', hello_message=lambda x: len(x.decode('ascii')) >= 0)
@verify_value('paranoid', hello_message=lambda x: WHostgroupBeaconMessenger.__message_groups_splitter__ not in x)
@verify_type(hostgroup_names=str)
@verify_value(hostgroup_names=lambda x: WHostgroupBeaconMessenger.re_hostgroup_name.match(x) is not None)
def __init__(self, hello_message, *hostgroup_names, invert_hello=False):
""" Create new messenger
:param hello_message: same as hello_message in :meth:`.WBeaconGouverneurMessenger.__init__`
:param hostgroup_names: list of host group names
"""
WBeaconGouverneurMessenger.__init__(self, hello_message, invert_hello=invert_hello)
self.__hostgroups = []
self.__hostgroups.extend([x.encode() for x in hostgroup_names])
[docs] def hostgroups(self):
""" Return list of host group names
:return: list of str
"""
return [x.decode() for x in self.__hostgroups]
@verify_type('paranoid', beacon_config=WConfig, invert_hello=bool)
def _message(self, beacon_config, invert_hello=False):
""" Overridden :meth:`.WBeaconGouverneurMessenger._message` method. Appends encoded host group names
to requests and responses.
:param beacon_config: beacon configuration
:return: bytes
"""
m = WBeaconGouverneurMessenger._message(self, beacon_config, invert_hello=invert_hello)
hostgroups = self._message_hostgroup_generate()
if len(hostgroups) > 0:
m += (WHostgroupBeaconMessenger.__message_groups_splitter__ + hostgroups)
return m
def _message_hostgroup_generate(self):
""" Encode messenger host group names
:return: bytes
"""
return b','.join(self.__hostgroups)
@verify_type(message=bytes)
def _message_hostgroup_parse(self, message):
""" Parse given message and return list of group names and socket information. Socket information
is parsed in :meth:`.WBeaconGouverneurMessenger._message_address_parse` method
:param message: bytes
:return: tuple of list of group names and WIPV4SocketInfo
"""
splitter_count = message.count(WHostgroupBeaconMessenger.__message_groups_splitter__)
if splitter_count == 0:
return [], WBeaconGouverneurMessenger._message_address_parse(self, message)
elif splitter_count == 1:
splitter_pos = message.find(WHostgroupBeaconMessenger.__message_groups_splitter__)
groups = []
group_splitter = WHostgroupBeaconMessenger.__group_splitter__
for group_name in message[(splitter_pos + 1):].split(group_splitter):
groups.append(group_name.strip())
address = WBeaconGouverneurMessenger._message_address_parse(self, message[:splitter_pos])
return groups, address
else:
raise ValueError('Invalid message. Too many separators')
[docs] @verify_type('paranoid', beacon_config=WConfig, request=bytes, client_address=WIPV4SocketInfo)
def has_response(self, beacon_config, request, client_address):
""" :meth:`.WBeaconMessengerBase.has_response` method implementation. This method compares request
headers as :meth:`.WBeaconGouverneurMessenger.has_response` do and compares specified group names
with internal names.
"""
try:
groups, address = self._message_hostgroup_parse(request)
if len(self.__hostgroups) == 0 or len(groups) == 0:
return True
for group_name in groups:
if group_name in self.__hostgroups:
return True
return False
except ValueError:
pass
return False
[docs] @verify_type('paranoid', beacon_config=WConfig, request=bytes)
@verify_type(client_address=WIPV4SocketInfo)
def response_address(self, beacon_config, request, client_address):
""" :meth:`.WBeaconMessengerBase.response_address` method implementation. It just removes host group names
part and return :meth:`.WBeaconMessengerBase.response_address` result
"""
si = self._message_hostgroup_parse(request)[1]
address = si.address()
port = si.port()
return WIPV4SocketInfo(
address if address is not None else client_address.address(),
port if port is not None else client_address.port()
)